Add Metatron-Drive Flutter frontend

This commit is contained in:
MOHAN 2026-06-25 03:26:31 +05:30
parent ad19d984f9
commit 2abde1758a
82 changed files with 5002 additions and 11 deletions

53
.gitignore vendored Normal file
View File

@ -0,0 +1,53 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
.build-tmp/
.gradle-home/
# Local device testing artifacts
/device-*.png
/device-*.xml
/login.xml
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
/android/build/

33
.metadata Normal file
View File

@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: android
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: ios
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@ -1,17 +1,24 @@
# whats_drive_app
# Metatron-Drive
A new Flutter project.
Flutter mobile frontend for the private Metatron-Drive file workspace.
## Getting Started
## Run
This project is a starting point for a Flutter application.
```bash
flutter pub get
flutter run
```
A few resources to get you started if this is your first Flutter project:
The production API base URL is configured in `lib/core/app_config.dart`.
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
## Build Android APK
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
```bash
flutter build apk --release --split-per-abi
```
The arm64 APK is generated at:
```text
build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
```

28
analysis_options.yaml Normal file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.metatroncube.whats_drive_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.metatroncube.whats_drive_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,52 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application
android:label="Metatron-Drive"
android:name="${applicationName}"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.metatroncube.whats_drive_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background">#101D42</color>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

24
android/build.gradle.kts Normal file
View File

@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,5 @@
org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.workers.max=2
kotlin.daemon.jvmargs=-Xmx1G
kotlin.incremental=false
android.useAndroidX=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip

View File

@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

34
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,620 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.metatroncube.whatsDriveApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.metatroncube.whatsDriveApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.metatroncube.whatsDriveApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.metatroncube.whatsDriveApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.metatroncube.whatsDriveApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.metatroncube.whatsDriveApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

72
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Metatron-Drive</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Metatron-Drive needs gallery access so you can select photos and videos to upload to your private workspace.</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>whats_drive_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/foundation.dart';
import '../models/user_profile.dart';
import '../services/api_client.dart';
import '../services/token_store.dart';
enum SessionStatus { loading, signedOut, signedIn }
class SessionController extends ChangeNotifier {
SessionController(this.api, this.tokens);
final ApiClient api;
final TokenStore tokens;
SessionStatus status = SessionStatus.loading;
UserProfile? user;
Future<void> initialize() async {
final minimumSplash = Future<void>.delayed(
const Duration(milliseconds: 1100),
);
final token = await tokens.read();
if (token == null) {
status = SessionStatus.signedOut;
} else {
try {
user = await api.me();
status = SessionStatus.signedIn;
} catch (_) {
await tokens.clear();
status = SessionStatus.signedOut;
}
}
await minimumSplash;
notifyListeners();
}
Future<void> login(String username, String password) async {
final result = await api.login(username, password);
await tokens.write(result.token);
user = result.user;
status = SessionStatus.signedIn;
notifyListeners();
}
Future<void> register(Map<String, dynamic> input) async {
final result = await api.register(input);
await tokens.write(result.token);
user = result.user;
status = SessionStatus.signedIn;
notifyListeners();
}
Future<void> logout() async {
await tokens.clear();
user = null;
status = SessionStatus.signedOut;
notifyListeners();
}
Future<void> deleteAccount(String password) async {
await api.deleteAccount(password);
await logout();
}
}

View File

@ -0,0 +1,264 @@
import 'dart:async';
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/foundation.dart';
import '../core/app_config.dart';
import '../services/token_store.dart';
class UploadRecord {
const UploadRecord({
required this.taskId,
required this.name,
required this.filename,
required this.createdAt,
this.progress = 0,
this.status = TaskStatus.enqueued,
this.sizeBytes = -1,
this.errorMessage,
});
final String taskId;
final String name;
final String filename;
final DateTime createdAt;
final double progress;
final TaskStatus status;
final int sizeBytes;
final String? errorMessage;
bool get isFinished => status.isFinalState;
bool get canRetry => status == TaskStatus.failed;
UploadRecord copyWith({
double? progress,
TaskStatus? status,
int? sizeBytes,
String? errorMessage,
}) => UploadRecord(
taskId: taskId,
name: name,
filename: filename,
createdAt: createdAt,
progress: progress ?? this.progress,
status: status ?? this.status,
sizeBytes: sizeBytes ?? this.sizeBytes,
errorMessage: errorMessage ?? this.errorMessage,
);
}
class UploadController extends ChangeNotifier {
UploadController(this._tokens) {
_taskSubscription = FileDownloader().updates.listen(_onUpdate);
_databaseSubscription = FileDownloader().database.updates.listen(
_onDatabaseUpdate,
);
unawaited(_restoreHistory());
}
final TokenStore _tokens;
late final StreamSubscription<TaskUpdate> _taskSubscription;
late final StreamSubscription<TaskRecord> _databaseSubscription;
final Map<String, UploadRecord> _uploads = {};
final Map<String, Task> _tasks = {};
bool _notificationPermissionChecked = false;
bool _disposed = false;
List<UploadRecord> get uploads {
final result = _uploads.values.toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
return result;
}
int completedVersion = 0;
Future<void> _restoreHistory() async {
// start() initializes tracking asynchronously during application startup.
// A short retry also covers slower devices on their first launch.
for (var attempt = 0; attempt < 3; attempt++) {
try {
final records = await FileDownloader().database.allRecords(
group: 'uploads',
);
for (final record in records) {
_storeDatabaseRecord(record);
}
if (!_disposed) notifyListeners();
return;
} catch (_) {
await Future<void>.delayed(Duration(milliseconds: 400 * (attempt + 1)));
}
}
}
Future<void> refreshHistory() => _restoreHistory();
Future<void> enqueue({
required String filename,
required String displayName,
String? folderId,
}) async {
await _ensureNotificationPermission();
final token = await _tokens.read();
if (token == null) throw StateError('Your session has expired');
final task = UploadTask(
url: '${AppConfig.apiBaseUrl}/files/upload',
filename: filename,
directory: 'upload_queue',
baseDirectory: BaseDirectory.applicationSupport,
fileField: 'file',
fields: folderId == null ? const {} : {'folderId': folderId},
headers: {'Authorization': 'Bearer $token'},
group: 'uploads',
updates: Updates.statusAndProgress,
retries: 2,
displayName: displayName,
metaData: displayName,
);
_tasks[task.taskId] = task;
_uploads[task.taskId] = UploadRecord(
taskId: task.taskId,
name: displayName,
filename: filename,
createdAt: task.creationTime,
);
notifyListeners();
final accepted = await FileDownloader().enqueue(task);
if (!accepted) {
_uploads[task.taskId] = _uploads[task.taskId]!.copyWith(
status: TaskStatus.failed,
errorMessage: 'The background upload queue rejected this file.',
);
notifyListeners();
throw StateError('The background upload queue rejected this file');
}
}
Future<void> _ensureNotificationPermission() async {
if (_notificationPermissionChecked) return;
_notificationPermissionChecked = true;
final status = await FileDownloader().permissions.status(
PermissionType.notifications,
);
if (status != PermissionStatus.granted) {
await FileDownloader().permissions.request(PermissionType.notifications);
}
}
void _onUpdate(TaskUpdate update) {
_tasks[update.task.taskId] = update.task;
final current = _uploads[update.task.taskId] ?? _fromTask(update.task);
switch (update) {
case TaskProgressUpdate():
_uploads[update.task.taskId] = current.copyWith(
progress: update.progress.clamp(0, 1),
);
case TaskStatusUpdate():
_uploads[update.task.taskId] = current.copyWith(
status: update.status,
progress: update.status == TaskStatus.complete ? 1 : current.progress,
errorMessage: update.exception?.description,
);
if (update.status == TaskStatus.complete) completedVersion++;
if (update.status == TaskStatus.complete ||
update.status == TaskStatus.canceled ||
update.status == TaskStatus.notFound) {
unawaited(_deleteStagedFile(update.task));
}
}
notifyListeners();
}
void _onDatabaseUpdate(TaskRecord record) {
if (record.group != 'uploads') return;
_storeDatabaseRecord(record);
if (!_disposed) notifyListeners();
}
void _storeDatabaseRecord(TaskRecord record) {
_tasks[record.taskId] = record.task;
_uploads[record.taskId] = UploadRecord(
taskId: record.taskId,
name: record.task.displayName,
filename: record.task.filename,
createdAt: record.task.creationTime,
progress: record.progress.clamp(0, 1),
status: record.status,
sizeBytes: record.expectedFileSize,
errorMessage: record.exception?.description,
);
}
UploadRecord _fromTask(Task task) => UploadRecord(
taskId: task.taskId,
name: task.displayName,
filename: task.filename,
createdAt: task.creationTime,
);
Future<void> _deleteStagedFile(Task task) async {
final file = File(await task.filePath());
if (await file.exists()) await file.delete();
}
Future<void> cancel(String taskId) =>
FileDownloader().cancelTaskWithId(taskId);
Future<void> retry(String taskId) async {
final original = _tasks[taskId];
if (original is! UploadTask) {
throw StateError('The original upload information is unavailable.');
}
final file = File(await original.filePath());
if (!await file.exists()) {
throw StateError(
'The staged file is no longer available. Select it again.',
);
}
final token = await _tokens.read();
if (token == null) throw StateError('Your session has expired');
final retried = original.copyWith(
taskId: DateTime.now().microsecondsSinceEpoch.toString(),
headers: {'Authorization': 'Bearer $token'},
creationTime: DateTime.now(),
retriesRemaining: original.retries,
);
_tasks[retried.taskId] = retried;
_uploads[retried.taskId] = _fromTask(retried);
notifyListeners();
if (!await FileDownloader().enqueue(retried)) {
_uploads[retried.taskId] = _uploads[retried.taskId]!.copyWith(
status: TaskStatus.failed,
errorMessage: 'The background upload queue rejected this retry.',
);
notifyListeners();
throw StateError('The background upload queue rejected this retry.');
}
}
Future<void> removeHistory(String taskId) async {
final task = _tasks.remove(taskId);
_uploads.remove(taskId);
await FileDownloader().database.deleteRecordWithId(taskId);
if (task != null && task.group == 'uploads') {
await _deleteStagedFile(task);
}
notifyListeners();
}
Future<void> clearFinished() async {
final finished = _uploads.values.where((item) => item.isFinished).toList();
for (final item in finished) {
await removeHistory(item.taskId);
}
}
@override
void dispose() {
_disposed = true;
_taskSubscription.cancel();
_databaseSubscription.cancel();
super.dispose();
}
}

6
lib/core/app_config.dart Normal file
View File

@ -0,0 +1,6 @@
class AppConfig {
static const apiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'https://filedriveapi.thedomainnest.com/api',
);
}

65
lib/core/app_theme.dart Normal file
View File

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
class AppTheme {
static const navy = Color(0xFF101D42);
static const blue = Color(0xFF2B59FF);
static const mint = Color(0xFF34D6A0);
static const background = Color(0xFFF6F8FC);
static ThemeData get light {
final scheme = ColorScheme.fromSeed(
seedColor: blue,
primary: blue,
secondary: mint,
surface: Colors.white,
);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
scaffoldBackgroundColor: background,
fontFamily: 'Roboto',
appBarTheme: const AppBarTheme(
backgroundColor: background,
foregroundColor: navy,
elevation: 0,
centerTitle: false,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFE5EAF3)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: blue, width: 1.5),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 18,
vertical: 16,
),
),
cardTheme: CardThemeData(
color: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: const BorderSide(color: Color(0xFFE9EDF5)),
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(54),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
);
}
}

82
lib/main.dart Normal file
View File

@ -0,0 +1,82 @@
import 'dart:async';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'controllers/session_controller.dart';
import 'controllers/upload_controller.dart';
import 'core/app_theme.dart';
import 'screens/drive_screen.dart';
import 'screens/login_screen.dart';
import 'screens/splash_screen.dart';
import 'services/api_client.dart';
import 'services/token_store.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
const tokens = TokenStore();
final api = ApiClient(tokens);
runApp(WhatsDriveApp(tokens: tokens, api: api));
unawaited(_initializeBackgroundUploads());
}
Future<void> _initializeBackgroundUploads() async {
try {
await FileDownloader().configure(
globalConfig: [(Config.requestTimeout, const Duration(minutes: 30))],
);
FileDownloader().configureNotificationForGroup(
'uploads',
running: const TaskNotification(
'Uploading {displayName}',
'{progress} • {networkSpeed} • {timeRemaining} remaining',
),
complete: const TaskNotification(
'Upload complete',
'{displayName} is now in your drive',
),
error: const TaskNotification(
'Upload failed',
'Could not upload {displayName}',
),
canceled: const TaskNotification(
'Upload canceled',
'{displayName} was canceled',
),
progressBar: true,
);
await FileDownloader().start();
} catch (error, stackTrace) {
debugPrint('Background upload initialization failed: $error\n$stackTrace');
}
}
class WhatsDriveApp extends StatelessWidget {
const WhatsDriveApp({super.key, required this.tokens, required this.api});
final TokenStore tokens;
final ApiClient api;
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => SessionController(api, tokens)..initialize(),
),
ChangeNotifierProvider(create: (_) => UploadController(tokens)),
Provider.value(value: api),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Metatron-Drive',
theme: AppTheme.light,
home: Consumer<SessionController>(
builder: (context, session, _) => switch (session.status) {
SessionStatus.loading => const SplashScreen(),
SessionStatus.signedOut => const LoginScreen(),
SessionStatus.signedIn => const DriveScreen(),
},
),
),
);
}
}

View File

@ -0,0 +1,47 @@
class DriveFolder {
const DriveFolder({
required this.id,
required this.name,
this.childCount = 0,
this.fileCount = 0,
});
final String id;
final String name;
final int childCount;
final int fileCount;
factory DriveFolder.fromJson(Map<String, dynamic> json) {
final count = json['_count'] as Map<String, dynamic>?;
return DriveFolder(
id: json['id'] as String,
name: json['name'] as String,
childCount: count?['children'] as int? ?? 0,
fileCount: count?['files'] as int? ?? 0,
);
}
}
class DriveFile {
const DriveFile({
required this.id,
required this.name,
required this.mimeType,
required this.sizeBytes,
required this.createdAt,
});
final String id;
final String name;
final String mimeType;
final int sizeBytes;
final DateTime createdAt;
bool get isImage => mimeType.startsWith('image/');
bool get isVideo => mimeType.startsWith('video/');
factory DriveFile.fromJson(Map<String, dynamic> json) => DriveFile(
id: json['id'] as String,
name: json['originalName'] as String,
mimeType: json['mimeType'] as String,
sizeBytes: json['sizeBytes'] as int,
createdAt: DateTime.parse(json['createdAt'] as String),
);
}

View File

@ -0,0 +1,29 @@
class UserProfile {
const UserProfile({
required this.id,
required this.username,
required this.name,
required this.phone,
required this.email,
required this.gender,
required this.age,
});
final String id;
final String username;
final String name;
final String phone;
final String email;
final String gender;
final int age;
factory UserProfile.fromJson(Map<String, dynamic> json) => UserProfile(
id: json['id'] as String,
username: json['username'] as String,
name: json['name'] as String,
phone: json['phone'] as String,
email: json['email'] as String,
gender: json['gender'] as String,
age: json['age'] as int,
);
}

View File

@ -0,0 +1,885 @@
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import '../controllers/session_controller.dart';
import '../controllers/upload_controller.dart';
import '../core/app_theme.dart';
import '../models/drive_models.dart';
import '../services/api_client.dart';
import '../services/token_store.dart';
import 'gallery_screen.dart';
import 'upload_history_screen.dart';
class DriveScreen extends StatefulWidget {
const DriveScreen({super.key});
@override
State<DriveScreen> createState() => _DriveScreenState();
}
class _DriveScreenState extends State<DriveScreen> {
final List<DriveFolder> _path = [];
List<DriveFolder> _folders = [];
List<DriveFile> _files = [];
bool _loading = true;
String? _error;
String? _token;
UploadController? _uploadController;
int _completedVersion = 0;
int _loadGeneration = 0;
bool _uploadRefreshScheduled = false;
String? get _folderId => _path.isEmpty ? null : _path.last.id;
@override
void initState() {
super.initState();
const TokenStore().read().then((value) {
if (mounted) setState(() => _token = value);
});
_load();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final controller = context.read<UploadController>();
if (_uploadController == controller) return;
_uploadController?.removeListener(_onUploadChanged);
_uploadController = controller..addListener(_onUploadChanged);
_completedVersion = controller.completedVersion;
}
void _onUploadChanged() {
final version = _uploadController?.completedVersion ?? 0;
if (version == _completedVersion || _uploadRefreshScheduled) return;
_completedVersion = version;
_uploadRefreshScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_uploadRefreshScheduled = false;
if (mounted) _load();
});
}
@override
void dispose() {
_uploadController?.removeListener(_onUploadChanged);
super.dispose();
}
Future<void> _load() async {
if (!mounted) return;
final generation = ++_loadGeneration;
setState(() {
_loading = true;
_error = null;
});
try {
final api = context.read<ApiClient>();
final values = await Future.wait([
api.folders(_folderId),
api.files(_folderId),
]);
if (!mounted || generation != _loadGeneration) return;
setState(() {
_folders = values[0] as List<DriveFolder>;
_files = values[1] as List<DriveFile>;
});
} catch (error) {
if (mounted && generation == _loadGeneration) {
setState(() => _error = context.read<ApiClient>().messageFrom(error));
}
} finally {
if (mounted && generation == _loadGeneration) {
setState(() => _loading = false);
}
}
}
Future<void> _createFolder() async {
final name = await showDialog<String>(
context: context,
builder: (_) => const _CreateFolderDialog(),
);
if (name == null || name.isEmpty || !mounted) return;
try {
await context.read<ApiClient>().createFolder(name, _folderId);
await _load();
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.read<ApiClient>().messageFrom(error))),
);
}
}
}
Future<void> _deleteFile(DriveFile file) async {
if (!await _confirm('Delete ${file.name}?')) return;
if (!mounted) return;
try {
await context.read<ApiClient>().deleteFile(file.id);
await _load();
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.read<ApiClient>().messageFrom(error))),
);
}
}
}
Future<void> _deleteFolder(DriveFolder folder) async {
if (!await _confirm('Delete the empty folder “${folder.name}”?')) return;
if (!mounted) return;
try {
await context.read<ApiClient>().deleteFolder(folder.id);
await _load();
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.read<ApiClient>().messageFrom(error))),
);
}
}
}
Future<bool> _confirm(String text) async =>
await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirm deletion'),
content: Text(text),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Keep'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete'),
),
],
),
) ??
false;
Future<void> _openGallery() async {
await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => GalleryScreen(folderId: _folderId)),
);
if (mounted) _load();
}
Future<void> _pickFiles() async {
final result = await FilePicker.pickFiles(
allowMultiple: true,
type: FileType.any,
withData: false,
);
if (result == null || !mounted) return;
final support = await getApplicationSupportDirectory();
final queue = Directory(
'${support.path}${Platform.pathSeparator}upload_queue',
);
await queue.create(recursive: true);
var queued = 0;
for (var index = 0; index < result.files.length; index++) {
final selected = result.files[index];
if (selected.path == null) continue;
final original = selected.name.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_');
final stagedName =
'${DateTime.now().microsecondsSinceEpoch}_$index-$original';
await File(
selected.path!,
).copy('${queue.path}${Platform.pathSeparator}$stagedName');
if (!mounted) return;
await context.read<UploadController>().enqueue(
filename: stagedName,
displayName: original,
folderId: _folderId,
);
queued++;
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$queued file(s) added to the upload queue.')),
);
}
}
void _showUploadSources() {
showModalBottomSheet(
context: context,
showDragHandle: true,
builder: (sheetContext) => SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.photo_library_outlined),
title: const Text('Photos and videos'),
subtitle: const Text(
'Browse the gallery inside Metatron-Drive',
),
onTap: () {
Navigator.pop(sheetContext);
_openGallery();
},
),
ListTile(
leading: const Icon(Icons.attach_file_rounded),
title: const Text('Any file'),
subtitle: const Text(
'Documents, audio, archives, PDFs, and more',
),
onTap: () {
Navigator.pop(sheetContext);
_pickFiles();
},
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
final user = context.watch<SessionController>().user!;
return PopScope(
canPop: _path.isEmpty,
onPopInvokedWithResult: (didPop, result) {
if (!didPop && _path.isNotEmpty) {
setState(() => _path.removeLast());
_load();
}
},
child: Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_path.isEmpty ? 'Metatron-Drive' : _path.last.name,
style: const TextStyle(fontWeight: FontWeight.w800),
),
Text(
'Hello, ${user.name.split(' ').first}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: Colors.black54,
),
),
],
),
actions: [
IconButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const UploadHistoryScreen()),
),
tooltip: 'Upload history',
icon: const Icon(Icons.history_rounded),
),
IconButton(
onPressed: _load,
tooltip: 'Refresh',
icon: const Icon(Icons.refresh_rounded),
),
IconButton(
onPressed: () =>
_showProfile(user.name, user.username, user.email),
tooltip: 'Profile',
icon: CircleAvatar(
radius: 16,
backgroundColor: AppTheme.navy,
child: Text(
user.name.substring(0, 1).toUpperCase(),
style: const TextStyle(color: Colors.white),
),
),
),
const SizedBox(width: 8),
],
),
body: RefreshIndicator(
onRefresh: _load,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: _DriveHeader(
path: _path,
onRoot: () {
setState(_path.clear);
_load();
},
onCreate: _createFolder,
onFiles: _pickFiles,
),
),
if (_loading)
const SliverFillRemaining(
child: Center(child: CircularProgressIndicator()),
)
else if (_error != null)
SliverFillRemaining(
child: _EmptyState(
icon: Icons.cloud_off_rounded,
title: 'Could not load your drive',
subtitle: _error!,
action: _load,
),
)
else if (_folders.isEmpty && _files.isEmpty)
SliverFillRemaining(
hasScrollBody: false,
child: _EmptyState(
icon: Icons.cloud_upload_outlined,
title: 'This folder is empty',
subtitle:
'Choose media from your gallery or create a folder.',
action: _openGallery,
),
)
else ...[
if (_folders.isNotEmpty)
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.fromLTRB(20, 12, 20, 8),
child: Text(
'Folders',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
),
),
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverGrid.builder(
gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 220,
childAspectRatio: 1.55,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
itemCount: _folders.length,
itemBuilder: (context, index) {
final folder = _folders[index];
return _FolderCard(
folder: folder,
onOpen: () {
setState(() => _path.add(folder));
_load();
},
onDelete: () => _deleteFolder(folder),
);
},
),
),
if (_files.isNotEmpty)
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.fromLTRB(20, 20, 20, 8),
child: Text(
'Files',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
),
),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 120),
sliver: SliverGrid.builder(
gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 190,
childAspectRatio: .82,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
itemCount: _files.length,
itemBuilder: (context, index) => _FileCard(
file: _files[index],
token: _token,
api: context.read<ApiClient>(),
onDelete: () => _deleteFile(_files[index]),
),
),
),
],
],
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _showUploadSources,
icon: const Icon(Icons.cloud_upload_outlined),
label: const Text('Upload'),
),
bottomSheet: const _UploadStrip(),
),
);
}
void _showProfile(String name, String username, String email) {
showModalBottomSheet(
context: context,
showDragHandle: true,
builder: (sheetContext) => Padding(
padding: const EdgeInsets.fromLTRB(24, 8, 24, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 34,
backgroundColor: AppTheme.navy,
child: Text(
name.substring(0, 1).toUpperCase(),
style: const TextStyle(fontSize: 26, color: Colors.white),
),
),
const SizedBox(height: 12),
Text(
name,
style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w700),
),
Text(
'@$username$email',
style: const TextStyle(color: Colors.black54),
),
const SizedBox(height: 22),
OutlinedButton.icon(
onPressed: () {
Navigator.pop(sheetContext);
context.read<SessionController>().logout();
},
icon: const Icon(Icons.logout),
label: const Text('Sign out'),
),
const SizedBox(height: 8),
TextButton.icon(
onPressed: () {
Navigator.pop(sheetContext);
_confirmAccountDeletion();
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
icon: const Icon(Icons.person_remove_outlined),
label: const Text('Delete database account'),
),
],
),
),
);
}
Future<void> _confirmAccountDeletion() async {
final password = TextEditingController();
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Delete account data?'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Your profile, folders, and file records will be removed from the database. Existing Google Drive files will be retained.',
),
const SizedBox(height: 16),
TextField(
controller: password,
obscureText: true,
decoration: const InputDecoration(labelText: 'Confirm password'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(dialogContext, true),
style: FilledButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Delete database data'),
),
],
),
);
if (confirmed != true || !mounted) {
password.dispose();
return;
}
try {
await context.read<SessionController>().deleteAccount(password.text);
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.read<ApiClient>().messageFrom(error))),
);
}
} finally {
password.dispose();
}
}
}
class _DriveHeader extends StatelessWidget {
const _DriveHeader({
required this.path,
required this.onRoot,
required this.onCreate,
required this.onFiles,
});
final List<DriveFolder> path;
final VoidCallback onRoot;
final VoidCallback onCreate;
final VoidCallback onFiles;
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 6),
child: Column(
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Row(
children: [
const CircleAvatar(
backgroundColor: Color(0xFFE8EEFF),
child: Icon(Icons.lock_outline_rounded, color: AppTheme.blue),
),
const SizedBox(width: 14),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Private workspace',
style: TextStyle(fontWeight: FontWeight.w700),
),
Text(
'Original quality • protected access',
style: TextStyle(fontSize: 12, color: Colors.black54),
),
],
),
),
IconButton(
onPressed: onFiles,
tooltip: 'Upload any file',
icon: const Icon(Icons.attach_file_rounded),
),
IconButton(
onPressed: onCreate,
tooltip: 'Create folder',
icon: const Icon(Icons.create_new_folder_outlined),
),
],
),
),
),
if (path.isNotEmpty)
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
onPressed: onRoot,
icon: const Icon(Icons.home_outlined, size: 18),
label: Text(
'Metatron-Drive / ${path.map((item) => item.name).join(' / ')}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
);
}
class _FolderCard extends StatelessWidget {
const _FolderCard({
required this.folder,
required this.onOpen,
required this.onDelete,
});
final DriveFolder folder;
final VoidCallback onOpen;
final VoidCallback onDelete;
@override
Widget build(BuildContext context) => Card(
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: onOpen,
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
const Icon(
Icons.folder_rounded,
color: Color(0xFFFFB547),
size: 38,
),
const SizedBox(width: 10),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
folder.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w700),
),
Text(
'${folder.childCount} folders • ${folder.fileCount} files',
style: const TextStyle(fontSize: 11, color: Colors.black54),
),
],
),
),
PopupMenuButton(
onSelected: (_) => onDelete(),
itemBuilder: (_) => const [
PopupMenuItem(
value: 'delete',
child: Text('Delete empty folder'),
),
],
),
],
),
),
),
);
}
class _FileCard extends StatelessWidget {
const _FileCard({
required this.file,
required this.token,
required this.api,
required this.onDelete,
});
final DriveFile file;
final String? token;
final ApiClient api;
final VoidCallback onDelete;
@override
Widget build(BuildContext context) => Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: ColoredBox(
color: const Color(0xFFF0F3F9),
child: file.isImage && token != null
? Image.network(
api.contentUrl(file.id),
headers: {'Authorization': 'Bearer $token'},
fit: BoxFit.cover,
errorBuilder: (_, _, _) =>
const Icon(Icons.broken_image_outlined, size: 42),
)
: Icon(
file.isVideo
? Icons.play_circle_outline_rounded
: Icons.insert_drive_file_outlined,
size: 48,
color: AppTheme.blue,
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 4, 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
file.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
Text(
_size(file.sizeBytes),
style: const TextStyle(
fontSize: 11,
color: Colors.black54,
),
),
],
),
),
PopupMenuButton(
onSelected: (_) => onDelete(),
itemBuilder: (_) => const [
PopupMenuItem(value: 'delete', child: Text('Delete')),
],
),
],
),
),
],
),
);
static String _size(int bytes) => bytes < 1024 * 1024
? '${(bytes / 1024).toStringAsFixed(1)} KB'
: '${(bytes / 1024 / 1024).toStringAsFixed(1)} MB';
}
class _UploadStrip extends StatelessWidget {
const _UploadStrip();
@override
Widget build(BuildContext context) => Consumer<UploadController>(
builder: (context, uploads, _) {
final active = uploads.uploads
.where(
(item) => !const {
TaskStatus.complete,
TaskStatus.failed,
TaskStatus.canceled,
}.contains(item.status),
)
.toList();
if (active.isEmpty) return const SizedBox.shrink();
final item = active.first;
return Material(
elevation: 10,
color: AppTheme.navy,
child: InkWell(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const UploadHistoryScreen()),
),
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 12, 10),
child: Row(
children: [
const Icon(Icons.cloud_upload_outlined, color: Colors.white),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${active.length} upload${active.length == 1 ? '' : 's'} active',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: item.progress,
minHeight: 4,
backgroundColor: Colors.white24,
color: AppTheme.mint,
),
],
),
),
IconButton(
onPressed: () => uploads.cancel(item.taskId),
icon: const Icon(Icons.close, color: Colors.white70),
),
],
),
),
),
),
);
},
);
}
class _CreateFolderDialog extends StatefulWidget {
const _CreateFolderDialog();
@override
State<_CreateFolderDialog> createState() => _CreateFolderDialogState();
}
class _CreateFolderDialogState extends State<_CreateFolderDialog> {
String _name = '';
bool _submitted = false;
void _submit() {
final value = _name.trim();
if (_submitted || value.isEmpty) return;
_submitted = true;
Navigator.of(context).pop(value);
}
@override
Widget build(BuildContext context) => AlertDialog(
title: const Text('New folder'),
content: TextFormField(
autofocus: true,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(labelText: 'Folder name'),
onChanged: (value) => _name = value,
onFieldSubmitted: (_) => _submit(),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(onPressed: _submit, child: const Text('Create')),
],
);
}
class _EmptyState extends StatelessWidget {
const _EmptyState({
required this.icon,
required this.title,
required this.subtitle,
required this.action,
});
final IconData icon;
final String title;
final String subtitle;
final VoidCallback action;
@override
Widget build(BuildContext context) => Center(
child: Padding(
padding: const EdgeInsets.all(36),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 62, color: AppTheme.blue),
const SizedBox(height: 16),
Text(
title,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
Text(
subtitle,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.black54),
),
const SizedBox(height: 18),
OutlinedButton(onPressed: action, child: const Text('Try again')),
],
),
),
);
}

View File

@ -0,0 +1,268 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
import 'package:provider/provider.dart';
import '../controllers/upload_controller.dart';
import '../core/app_theme.dart';
class GalleryScreen extends StatefulWidget {
const GalleryScreen({super.key, required this.folderId});
final String? folderId;
@override
State<GalleryScreen> createState() => _GalleryScreenState();
}
class _GalleryScreenState extends State<GalleryScreen> {
final _scroll = ScrollController();
final List<AssetEntity> _assets = [];
final Set<String> _selected = {};
AssetPathEntity? _album;
int _page = 0;
bool _loading = true;
bool _more = true;
bool _queueing = false;
String? _error;
@override
void initState() {
super.initState();
_scroll.addListener(() {
if (_scroll.position.extentAfter < 800) _loadMore();
});
_initialize();
}
@override
void dispose() {
_scroll.dispose();
super.dispose();
}
Future<void> _initialize() async {
final permission = await PhotoManager.requestPermissionExtend();
if (!permission.hasAccess) {
setState(() {
_loading = false;
_error = 'Gallery access is required to choose your media.';
});
return;
}
final albums = await PhotoManager.getAssetPathList(
type: RequestType.common,
onlyAll: true,
);
if (albums.isEmpty) {
setState(() {
_loading = false;
_error = 'No photos or videos were found.';
});
return;
}
_album = albums.first;
await _loadMore();
}
Future<void> _loadMore() async {
if (!_more ||
(_loading && _assets.isNotEmpty) ||
_album == null && !_loading) {
return;
}
final album = _album;
if (album == null) return;
if (_assets.isNotEmpty) setState(() => _loading = true);
final page = await album.getAssetListPaged(page: _page, size: 80);
if (!mounted) return;
setState(() {
_assets.addAll(page);
_page++;
_more = page.length == 80;
_loading = false;
});
}
Future<void> _queueUploads() async {
if (_selected.isEmpty) return;
setState(() => _queueing = true);
var queued = 0;
try {
final support = await getApplicationSupportDirectory();
final queue = Directory(
'${support.path}${Platform.pathSeparator}upload_queue',
);
await queue.create(recursive: true);
final chosen = _assets
.where((asset) => _selected.contains(asset.id))
.toList();
for (var index = 0; index < chosen.length; index++) {
final asset = chosen[index];
final source = await asset.originFile;
if (source == null) continue;
final original = (asset.title ?? source.uri.pathSegments.last)
.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_');
final stagedName =
'${DateTime.now().microsecondsSinceEpoch}_$index-$original';
await source.copy('${queue.path}${Platform.pathSeparator}$stagedName');
if (!mounted) return;
await context.read<UploadController>().enqueue(
filename: stagedName,
displayName: original,
folderId: widget.folderId,
);
queued++;
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'$queued file${queued == 1 ? '' : 's'} added to the background upload queue.',
),
),
);
Navigator.pop(context, true);
}
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not queue all files: $error')),
);
}
} finally {
if (mounted) setState(() => _queueing = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Choose from gallery'),
actions: [
if (_selected.isNotEmpty)
TextButton(
onPressed: () => setState(_selected.clear),
child: const Text('Clear'),
),
],
),
body: _error != null
? Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.photo_library_outlined,
size: 62,
color: AppTheme.blue,
),
const SizedBox(height: 14),
Text(_error!, textAlign: TextAlign.center),
const SizedBox(height: 14),
OutlinedButton(
onPressed: PhotoManager.openSetting,
child: const Text('Open settings'),
),
],
),
),
)
: _assets.isEmpty && _loading
? const Center(child: CircularProgressIndicator())
: GridView.builder(
controller: _scroll,
padding: const EdgeInsets.fromLTRB(3, 3, 3, 110),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 3,
crossAxisSpacing: 3,
),
itemCount: _assets.length + (_loading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= _assets.length) {
return const Center(
child: CircularProgressIndicator(strokeWidth: 2),
);
}
final asset = _assets[index];
final selected = _selected.contains(asset.id);
return GestureDetector(
onTap: () => setState(
() => selected
? _selected.remove(asset.id)
: _selected.add(asset.id),
),
child: Stack(
fit: StackFit.expand,
children: [
AssetEntityImage(
asset,
isOriginal: false,
thumbnailSize: const ThumbnailSize.square(350),
fit: BoxFit.cover,
),
if (selected)
Container(color: AppTheme.blue.withValues(alpha: .30)),
if (asset.type == AssetType.video)
const Positioned(
left: 7,
bottom: 7,
child: Icon(
Icons.videocam_rounded,
color: Colors.white,
),
),
Positioned(
right: 7,
top: 7,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 25,
height: 25,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: selected ? AppTheme.blue : Colors.black38,
border: Border.all(color: Colors.white, width: 2),
),
child: selected
? const Icon(
Icons.check,
color: Colors.white,
size: 17,
)
: null,
),
),
],
),
);
},
),
bottomNavigationBar: SafeArea(
child: Padding(
padding: const EdgeInsets.all(14),
child: FilledButton.icon(
onPressed: _selected.isEmpty || _queueing ? null : _queueUploads,
icon: _queueing
? const SizedBox.square(
dimension: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.cloud_upload_outlined),
label: Text(
_selected.isEmpty
? 'Select photos or videos'
: 'Upload ${_selected.length} selected',
),
),
),
),
);
}
}

View File

@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../controllers/session_controller.dart';
import '../core/app_theme.dart';
import '../services/api_client.dart';
import 'register_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _form = GlobalKey<FormState>();
final _username = TextEditingController();
final _password = TextEditingController();
bool _busy = false;
bool _hidden = true;
@override
void dispose() {
_username.dispose();
_password.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_form.currentState!.validate()) return;
setState(() => _busy = true);
try {
await context.read<SessionController>().login(
_username.text.trim(),
_password.text,
);
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.read<ApiClient>().messageFrom(error))),
);
}
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 440),
child: Form(
key: _form,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 68,
height: 68,
decoration: BoxDecoration(
color: AppTheme.blue,
borderRadius: BorderRadius.circular(22),
),
child: const Icon(
Icons.cloud_rounded,
color: Colors.white,
size: 38,
),
),
const SizedBox(height: 28),
const Text(
'Your private drive',
style: TextStyle(
fontSize: 31,
fontWeight: FontWeight.w800,
color: AppTheme.navy,
),
),
const SizedBox(height: 8),
const Text(
'Sign in to upload and organize your full-quality media.',
style: TextStyle(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 32),
TextFormField(
controller: _username,
textInputAction: TextInputAction.next,
autocorrect: false,
decoration: const InputDecoration(
labelText: 'Username',
prefixIcon: Icon(Icons.person_outline),
),
validator: (value) => (value?.trim().length ?? 0) < 3
? 'Enter your username'
: null,
),
const SizedBox(height: 14),
TextFormField(
controller: _password,
obscureText: _hidden,
onFieldSubmitted: (_) => _submit(),
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
onPressed: () => setState(() => _hidden = !_hidden),
icon: Icon(
_hidden
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
),
),
validator: (value) => (value?.isEmpty ?? true)
? 'Enter your password'
: null,
),
const SizedBox(height: 22),
FilledButton(
onPressed: _busy ? null : _submit,
child: _busy
? const SizedBox.square(
dimension: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text('Sign in'),
),
const SizedBox(height: 14),
TextButton(
onPressed: _busy
? null
: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const RegisterScreen(),
),
),
child: const Text('New to the family? Create an account'),
),
],
),
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../controllers/session_controller.dart';
import '../services/api_client.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _form = GlobalKey<FormState>();
final _name = TextEditingController();
final _username = TextEditingController();
final _email = TextEditingController();
final _phone = TextEditingController();
final _password = TextEditingController();
bool _busy = false;
@override
void dispose() {
for (final value in [_name, _username, _email, _phone, _password]) {
value.dispose();
}
super.dispose();
}
Future<void> _submit() async {
if (!_form.currentState!.validate()) return;
setState(() => _busy = true);
try {
await context.read<SessionController>().register({
'name': _name.text.trim(),
'username': _username.text.trim().toLowerCase(),
'email': _email.text.trim().toLowerCase(),
'phone': _phone.text.trim().isEmpty
? 'wd${DateTime.now().microsecondsSinceEpoch}'
: _phone.text.trim(),
'age': 18,
'gender': 'Prefer not to say',
'password': _password.text,
});
if (mounted) Navigator.of(context).popUntil((route) => route.isFirst);
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.read<ApiClient>().messageFrom(error))),
);
}
} finally {
if (mounted) setState(() => _busy = false);
}
}
String? _required(String? value) =>
(value?.trim().isEmpty ?? true) ? 'Required' : null;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Join the digital family')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Form(
key: _form,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Create your private workspace',
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 8),
const Text(
'We create your personal Google Drive folder automatically.',
),
const SizedBox(height: 24),
TextFormField(
controller: _name,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(labelText: 'Full name'),
validator: _required,
),
const SizedBox(height: 12),
TextFormField(
controller: _username,
autocorrect: false,
decoration: const InputDecoration(
labelText: 'Username',
helperText:
'Letters, numbers, dot, dash, or underscore',
),
validator: (value) =>
RegExp(
r'^[a-zA-Z0-9._-]{3,30}$',
).hasMatch(value?.trim() ?? '')
? null
: 'Enter a valid username',
),
const SizedBox(height: 12),
TextFormField(
controller: _email,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email address',
),
validator: (value) =>
RegExp(r'^.+@.+\..+$').hasMatch(value?.trim() ?? '')
? null
: 'Enter a valid email',
),
const SizedBox(height: 12),
TextFormField(
controller: _phone,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Phone number (optional)',
),
validator: (value) {
final phone = value?.trim() ?? '';
return phone.isEmpty || phone.length >= 7
? null
: 'Enter at least 7 characters or leave it blank';
},
),
const SizedBox(height: 12),
TextFormField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
helperText: 'At least 8 characters',
),
validator: (value) => (value?.length ?? 0) >= 8
? null
: 'Use at least 8 characters',
),
const SizedBox(height: 24),
FilledButton(
onPressed: _busy ? null : _submit,
child: _busy
? const SizedBox.square(
dimension: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text('Create Metatron-Drive'),
),
],
),
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,87 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../core/app_theme.dart';
class SplashScreen extends StatelessWidget {
const SplashScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.navy,
body: Stack(
fit: StackFit.expand,
children: [
CustomPaint(painter: _FamilyCanvasPainter()),
SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 88,
height: 88,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .12),
borderRadius: BorderRadius.circular(28),
),
child: const Icon(
Icons.cloud_upload_rounded,
color: Colors.white,
size: 46,
),
),
const SizedBox(height: 28),
const Text(
'Welcome to',
style: TextStyle(color: Colors.white70, fontSize: 18),
),
const SizedBox(height: 8),
const Text(
'Metatroncube\nDigital Family',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 32,
height: 1.1,
),
),
const SizedBox(height: 34),
const SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: AppTheme.mint,
),
),
],
),
),
),
),
],
),
);
}
}
class _FamilyCanvasPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..style = PaintingStyle.fill;
for (var i = 0; i < 18; i++) {
final x = (math.sin(i * 2.7) + 1) * size.width / 2;
final y = (math.cos(i * 1.9) + 1) * size.height / 2;
paint.color = (i.isEven ? AppTheme.blue : AppTheme.mint).withValues(
alpha: .12,
);
canvas.drawCircle(Offset(x, y), 12 + (i % 5) * 8, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -0,0 +1,520 @@
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../controllers/upload_controller.dart';
import '../core/app_theme.dart';
class UploadHistoryScreen extends StatefulWidget {
const UploadHistoryScreen({super.key});
@override
State<UploadHistoryScreen> createState() => _UploadHistoryScreenState();
}
class _UploadHistoryScreenState extends State<UploadHistoryScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) context.read<UploadController>().refreshHistory();
});
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text(
'Upload history',
style: TextStyle(fontWeight: FontWeight.w800),
),
actions: [
Consumer<UploadController>(
builder: (context, controller, _) =>
controller.uploads.any((item) => item.isFinished)
? TextButton(
onPressed: () => _clearFinished(controller),
child: const Text('Clear finished'),
)
: const SizedBox.shrink(),
),
const SizedBox(width: 6),
],
),
body: Consumer<UploadController>(
builder: (context, controller, _) {
final uploads = controller.uploads;
if (uploads.isEmpty) {
return const _HistoryEmptyState();
}
final active = uploads.where((item) => !item.isFinished).length;
final complete = uploads
.where((item) => item.status == TaskStatus.complete)
.length;
final failed = uploads
.where(
(item) =>
item.status == TaskStatus.failed ||
item.status == TaskStatus.notFound,
)
.length;
return RefreshIndicator(
onRefresh: controller.refreshHistory,
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 28),
children: [
_Summary(active: active, complete: complete, failed: failed),
const SizedBox(height: 18),
for (final item in uploads) ...[
_UploadCard(
item: item,
onOpen: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => UploadDetailScreen(taskId: item.taskId),
),
),
onCancel: () => controller.cancel(item.taskId),
onRetry: () => _retry(controller, item.taskId),
onRemove: () => controller.removeHistory(item.taskId),
),
const SizedBox(height: 10),
],
],
),
);
},
),
);
Future<void> _retry(UploadController controller, String taskId) async {
try {
await controller.retry(taskId);
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(_cleanError(error))));
}
}
}
Future<void> _clearFinished(UploadController controller) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Clear finished uploads?'),
content: const Text(
'This removes completed, failed, and canceled entries from local history. Files already in Google Drive are not deleted.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Keep'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Clear'),
),
],
),
);
if (confirmed == true) await controller.clearFinished();
}
}
class UploadDetailScreen extends StatelessWidget {
const UploadDetailScreen({super.key, required this.taskId});
final String taskId;
@override
Widget build(BuildContext context) => Consumer<UploadController>(
builder: (context, controller, _) {
final matches = controller.uploads.where((item) => item.taskId == taskId);
if (matches.isEmpty) {
return Scaffold(
appBar: AppBar(title: const Text('Upload details')),
body: const Center(child: Text('This history entry was removed.')),
);
}
final item = matches.first;
final visual = _statusVisual(item.status);
return Scaffold(
appBar: AppBar(
title: const Text(
'Upload details',
style: TextStyle(fontWeight: FontWeight.w800),
),
),
body: ListView(
padding: const EdgeInsets.all(20),
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(22),
child: Column(
children: [
CircleAvatar(
radius: 31,
backgroundColor: visual.color.withValues(alpha: .12),
child: Icon(visual.icon, color: visual.color, size: 32),
),
const SizedBox(height: 14),
Text(
item.name,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 19,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 5),
Text(
visual.label,
style: TextStyle(
color: visual.color,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 18),
LinearProgressIndicator(
value: item.progress.clamp(0, 1),
minHeight: 8,
borderRadius: BorderRadius.circular(8),
color: visual.color,
backgroundColor: visual.color.withValues(alpha: .12),
),
const SizedBox(height: 8),
Text('${(item.progress.clamp(0, 1) * 100).round()}%'),
],
),
),
),
const SizedBox(height: 14),
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
children: [
_DetailRow(
label: 'Started',
value: _dateTime(item.createdAt),
),
_DetailRow(
label: 'File size',
value: _size(item.sizeBytes),
),
_DetailRow(label: 'Status', value: visual.label),
_DetailRow(label: 'Task ID', value: item.taskId),
if (item.errorMessage != null)
_DetailRow(label: 'Error', value: item.errorMessage!),
],
),
),
),
const SizedBox(height: 18),
if (!item.isFinished)
OutlinedButton.icon(
onPressed: () => controller.cancel(item.taskId),
icon: const Icon(Icons.close_rounded),
label: const Text('Cancel upload'),
),
if (item.canRetry)
FilledButton.icon(
onPressed: () async {
try {
await controller.retry(item.taskId);
} catch (error) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(_cleanError(error))),
);
}
}
},
icon: const Icon(Icons.refresh_rounded),
label: const Text('Retry upload'),
),
],
),
);
},
);
}
class _Summary extends StatelessWidget {
const _Summary({
required this.active,
required this.complete,
required this.failed,
});
final int active;
final int complete;
final int failed;
@override
Widget build(BuildContext context) => Row(
children: [
Expanded(
child: _SummaryItem(
value: active,
label: 'Active',
color: AppTheme.blue,
),
),
const SizedBox(width: 8),
Expanded(
child: _SummaryItem(
value: complete,
label: 'Done',
color: const Color(0xFF15805A),
),
),
const SizedBox(width: 8),
Expanded(
child: _SummaryItem(
value: failed,
label: 'Failed',
color: const Color(0xFFC43B3B),
),
),
],
);
}
class _SummaryItem extends StatelessWidget {
const _SummaryItem({
required this.value,
required this.label,
required this.color,
});
final int value;
final String label;
final Color color;
@override
Widget build(BuildContext context) => Card(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15),
child: Column(
children: [
Text(
'$value',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w800,
color: color,
),
),
Text(
label,
style: const TextStyle(fontSize: 12, color: Colors.black54),
),
],
),
),
);
}
class _UploadCard extends StatelessWidget {
const _UploadCard({
required this.item,
required this.onOpen,
required this.onCancel,
required this.onRetry,
required this.onRemove,
});
final UploadRecord item;
final VoidCallback onOpen;
final VoidCallback onCancel;
final VoidCallback onRetry;
final VoidCallback onRemove;
@override
Widget build(BuildContext context) {
final visual = _statusVisual(item.status);
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onOpen,
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 14, 8, 14),
child: Row(
children: [
CircleAvatar(
backgroundColor: visual.color.withValues(alpha: .12),
child: Icon(visual.icon, color: visual.color),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w700),
),
const SizedBox(height: 3),
Text(
'${visual.label}${_dateTime(item.createdAt)}',
style: TextStyle(fontSize: 11, color: visual.color),
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: item.progress.clamp(0, 1),
minHeight: 5,
borderRadius: BorderRadius.circular(5),
color: visual.color,
backgroundColor: visual.color.withValues(alpha: .12),
),
],
),
),
PopupMenuButton<String>(
onSelected: (value) {
if (value == 'cancel') onCancel();
if (value == 'retry') onRetry();
if (value == 'remove') onRemove();
},
itemBuilder: (_) => [
if (!item.isFinished)
const PopupMenuItem(value: 'cancel', child: Text('Cancel')),
if (item.canRetry)
const PopupMenuItem(value: 'retry', child: Text('Retry')),
if (item.isFinished)
const PopupMenuItem(
value: 'remove',
child: Text('Remove history'),
),
],
),
],
),
),
),
);
}
}
class _DetailRow extends StatelessWidget {
const _DetailRow({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 82,
child: Text(label, style: const TextStyle(color: Colors.black54)),
),
Expanded(
child: SelectableText(
value,
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
],
),
);
}
class _HistoryEmptyState extends StatelessWidget {
const _HistoryEmptyState();
@override
Widget build(BuildContext context) => const Center(
child: Padding(
padding: EdgeInsets.all(36),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.history_rounded, size: 64, color: AppTheme.blue),
SizedBox(height: 16),
Text(
'No uploads yet',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800),
),
SizedBox(height: 6),
Text(
'Uploads and their progress will appear here.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black54),
),
],
),
),
);
}
({String label, IconData icon, Color color}) _statusVisual(TaskStatus status) =>
switch (status) {
TaskStatus.enqueued => (
label: 'Queued',
icon: Icons.schedule_rounded,
color: AppTheme.blue,
),
TaskStatus.running => (
label: 'Uploading',
icon: Icons.cloud_upload_rounded,
color: AppTheme.blue,
),
TaskStatus.complete => (
label: 'Uploaded',
icon: Icons.check_circle_rounded,
color: const Color(0xFF15805A),
),
TaskStatus.failed => (
label: 'Failed',
icon: Icons.error_rounded,
color: const Color(0xFFC43B3B),
),
TaskStatus.notFound => (
label: 'Not found',
icon: Icons.error_rounded,
color: const Color(0xFFC43B3B),
),
TaskStatus.canceled => (
label: 'Canceled',
icon: Icons.cancel_rounded,
color: Colors.blueGrey,
),
TaskStatus.waitingToRetry => (
label: 'Waiting to retry',
icon: Icons.refresh_rounded,
color: const Color(0xFFB36B00),
),
TaskStatus.paused => (
label: 'Paused',
icon: Icons.pause_circle_rounded,
color: const Color(0xFFB36B00),
),
};
String _dateTime(DateTime value) {
final local = value.toLocal();
String two(int number) => number.toString().padLeft(2, '0');
return '${two(local.day)}/${two(local.month)}/${local.year} ${two(local.hour)}:${two(local.minute)}';
}
String _size(int bytes) {
if (bytes < 0) return 'Calculating';
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / 1024 / 1024).toStringAsFixed(1)} MB';
}
return '${(bytes / 1024 / 1024 / 1024).toStringAsFixed(2)} GB';
}
String _cleanError(Object error) => error
.toString()
.replaceFirst('Bad state: ', '')
.replaceFirst('Exception: ', '');

View File

@ -0,0 +1,99 @@
import 'package:dio/dio.dart';
import '../core/app_config.dart';
import '../models/drive_models.dart';
import '../models/user_profile.dart';
import 'token_store.dart';
class ApiClient {
ApiClient(this._tokens) {
dio = Dio(
BaseOptions(
baseUrl: AppConfig.apiBaseUrl,
connectTimeout: const Duration(seconds: 20),
receiveTimeout: const Duration(minutes: 5),
),
);
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await _tokens.read();
if (token != null) options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
),
);
}
final TokenStore _tokens;
late final Dio dio;
String messageFrom(Object error) {
if (error is DioException) {
final data = error.response?.data;
if (data is Map && data['error'] is String) {
return data['error'] as String;
}
if (error.type == DioExceptionType.connectionError) {
return 'Cannot reach the server. Check the API address and network.';
}
}
return 'Something went wrong. Please try again.';
}
Future<({String token, UserProfile user})> login(
String username,
String password,
) async {
final response = await dio.post(
'/auth/login',
data: {'username': username, 'password': password},
);
return (
token: response.data['token'] as String,
user: UserProfile.fromJson(response.data['user']),
);
}
Future<({String token, UserProfile user})> register(
Map<String, dynamic> input,
) async {
final response = await dio.post('/auth/register', data: input);
return (
token: response.data['token'] as String,
user: UserProfile.fromJson(response.data['user']),
);
}
Future<UserProfile> me() async {
final response = await dio.get('/auth/me');
return UserProfile.fromJson(response.data['user']);
}
Future<void> deleteAccount(String password) =>
dio.delete('/retention/account', data: {'password': password});
Future<List<DriveFolder>> folders(String? parentId) async {
final response = await dio.get(
'/folders',
queryParameters: parentId == null ? null : {'parentId': parentId},
);
return (response.data['folders'] as List)
.map((item) => DriveFolder.fromJson(item))
.toList();
}
Future<List<DriveFile>> files(String? folderId) async {
final response = await dio.get(
'/files',
queryParameters: folderId == null ? null : {'folderId': folderId},
);
return (response.data['files'] as List)
.map((item) => DriveFile.fromJson(item))
.toList();
}
Future<void> createFolder(String name, String? parentId) =>
dio.post('/folders', data: {'name': name, 'parentId': parentId});
Future<void> deleteFolder(String id) => dio.delete('/folders/$id');
Future<void> deleteFile(String id) => dio.delete('/files/$id');
String contentUrl(String id) => '${AppConfig.apiBaseUrl}/files/$id/content';
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class TokenStore {
static const _tokenKey = 'auth_token';
const TokenStore();
FlutterSecureStorage get _storage => const FlutterSecureStorage();
Future<String?> read() => _storage.read(key: _tokenKey);
Future<void> write(String token) =>
_storage.write(key: _tokenKey, value: token);
Future<void> clear() => _storage.delete(key: _tokenKey);
}

610
pubspec.lock Normal file
View File

@ -0,0 +1,610 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
background_downloader:
dependency: "direct main"
description:
name: background_downloader
sha256: aceacec2b2a72ec3a8862ab5895fcbbc71ab33765f3619d57963f3110dd268e3
url: "https://pub.dev"
source: hosted
version: "9.5.5"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.2.1"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
url: "https://pub.dev"
source: hosted
version: "0.3.5+2"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
dbus:
dependency: transitive
description:
name: dbus
sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645"
url: "https://pub.dev"
source: hosted
version: "0.7.14"
dio:
dependency: "direct main"
description:
name: dio
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
url: "https://pub.dev"
source: hosted
version: "5.9.2"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387
url: "https://pub.dev"
source: hosted
version: "11.0.2"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev"
source: hosted
version: "2.0.35"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
url: "https://pub.dev"
source: hosted
version: "10.3.1"
flutter_secure_storage_darwin:
dependency: transitive
description:
name: flutter_secure_storage_darwin
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
url: "https://pub.dev"
source: hosted
version: "3.0.1"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a"
url: "https://pub.dev"
source: hosted
version: "17.3.0"
hooks:
dependency: transitive
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
mime:
dependency: "direct main"
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
source: hosted
version: "9.4.1"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev"
source: hosted
version: "2.1.6"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
photo_manager:
dependency: "direct main"
description:
name: photo_manager
sha256: fb3bc8ea653370f88742b3baa304700107c83d12748aa58b2b9f2ed3ef15e6c2
url: "https://pub.dev"
source: hosted
version: "3.9.0"
photo_manager_image_provider:
dependency: "direct main"
description:
name: photo_manager_image_provider
sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f
url: "https://pub.dev"
source: hosted
version: "2.2.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.11.4 <4.0.0"
flutter: ">=3.38.4"

100
pubspec.yaml Normal file
View File

@ -0,0 +1,100 @@
name: whats_drive_app
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.11.4
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
dio: ^5.9.2
flutter_secure_storage: ^10.3.1
go_router: ^17.3.0
provider: ^6.1.5+1
photo_manager: ^3.9.0
photo_manager_image_provider: ^2.2.0
background_downloader: ^9.5.5
path_provider: ^2.1.6
mime: ^2.0.0
intl: ^0.20.2
file_picker: 11.0.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package

12
test/widget_test.dart Normal file
View File

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:whats_drive_app/screens/splash_screen.dart';
void main() {
testWidgets('splash identifies the organization', (tester) async {
await tester.pumpWidget(const MaterialApp(home: SplashScreen()));
expect(find.text('Welcome to'), findsOneWidget);
expect(find.text('Metatroncube\nDigital Family'), findsOneWidget);
expect(find.byIcon(Icons.cloud_upload_rounded), findsOneWidget);
});
}