Compare commits
No commits in common. "7aa1841b01a57a344ec31cc7bcc9261de19a0342" and "86425cdd7ee7cbb16e60399bac02946efe5b8e2f" have entirely different histories.
7aa1841b01
...
86425cdd7e
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
Flutter mobile frontend for the private Metatron-Drive file workspace.
|
Flutter mobile frontend for the private Metatron-Drive file workspace.
|
||||||
|
|
||||||
The app uses one global account with multiple organization memberships. New accounts apply with an organization code; organization administrators must approve access before login.
|
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
14
android/.gitignore
vendored
Normal file
14
android/.gitignore
vendored
Normal 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
|
||||||
44
android/app/build.gradle.kts
Normal file
44
android/app/build.gradle.kts
Normal 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 = "../.."
|
||||||
|
}
|
||||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
7
android/app/src/debug/AndroidManifest.xml
Normal 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>
|
||||||
52
android/app/src/main/AndroidManifest.xml
Normal file
52
android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.metatroncube.whats_drive_app
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal 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>
|
||||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
12
android/app/src/main/res/drawable/launch_background.xml
Normal 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>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
18
android/app/src/main/res/values-night/styles.xml
Normal 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>
|
||||||
4
android/app/src/main/res/values/colors.xml
Normal file
4
android/app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="splash_background">#101D42</color>
|
||||||
|
</resources>
|
||||||
18
android/app/src/main/res/values/styles.xml
Normal file
18
android/app/src/main/res/values/styles.xml
Normal 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>
|
||||||
7
android/app/src/profile/AndroidManifest.xml
Normal file
7
android/app/src/profile/AndroidManifest.xml
Normal 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
24
android/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
5
android/gradle.properties
Normal file
5
android/gradle.properties
Normal 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
|
||||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
||||||
26
android/settings.gradle.kts
Normal file
26
android/settings.gradle.kts
Normal 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")
|
||||||
@ -11,14 +11,6 @@ class SessionController extends ChangeNotifier {
|
|||||||
final TokenStore tokens;
|
final TokenStore tokens;
|
||||||
SessionStatus status = SessionStatus.loading;
|
SessionStatus status = SessionStatus.loading;
|
||||||
UserProfile? user;
|
UserProfile? user;
|
||||||
String? organizationId;
|
|
||||||
|
|
||||||
OrganizationMembership? get membership {
|
|
||||||
final matches = user?.approvedMemberships.where(
|
|
||||||
(item) => item.organization.id == organizationId,
|
|
||||||
);
|
|
||||||
return matches == null || matches.isEmpty ? null : matches.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
final minimumSplash = Future<void>.delayed(
|
final minimumSplash = Future<void>.delayed(
|
||||||
@ -30,10 +22,6 @@ class SessionController extends ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
user = await api.me();
|
user = await api.me();
|
||||||
await _selectInitialOrganization();
|
|
||||||
if (organizationId == null) {
|
|
||||||
throw StateError('No approved organization membership');
|
|
||||||
}
|
|
||||||
status = SessionStatus.signedIn;
|
status = SessionStatus.signedIn;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
await tokens.clear();
|
await tokens.clear();
|
||||||
@ -48,46 +36,21 @@ class SessionController extends ChangeNotifier {
|
|||||||
final result = await api.login(username, password);
|
final result = await api.login(username, password);
|
||||||
await tokens.write(result.token);
|
await tokens.write(result.token);
|
||||||
user = result.user;
|
user = result.user;
|
||||||
await _selectInitialOrganization();
|
|
||||||
status = SessionStatus.signedIn;
|
status = SessionStatus.signedIn;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> register(Map<String, dynamic> input) => api.register(input);
|
Future<void> register(Map<String, dynamic> input) async {
|
||||||
|
final result = await api.register(input);
|
||||||
Future<void> _selectInitialOrganization() async {
|
await tokens.write(result.token);
|
||||||
final approved = user?.approvedMemberships ?? const [];
|
user = result.user;
|
||||||
final saved = await tokens.readOrganization();
|
status = SessionStatus.signedIn;
|
||||||
final selected = approved.where((item) => item.organization.id == saved);
|
|
||||||
organizationId = selected.isNotEmpty
|
|
||||||
? selected.first.organization.id
|
|
||||||
: (approved.isEmpty ? null : approved.first.organization.id);
|
|
||||||
if (organizationId != null) {
|
|
||||||
await tokens.writeOrganization(organizationId!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> selectOrganization(String id) async {
|
|
||||||
if (!(user?.approvedMemberships.any((item) => item.organization.id == id) ??
|
|
||||||
false)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
organizationId = id;
|
|
||||||
await tokens.writeOrganization(id);
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> refreshProfile() async {
|
|
||||||
user = await api.me();
|
|
||||||
await _selectInitialOrganization();
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
await tokens.clear();
|
await tokens.clear();
|
||||||
await tokens.clearOrganization();
|
|
||||||
user = null;
|
user = null;
|
||||||
organizationId = null;
|
|
||||||
status = SessionStatus.signedOut;
|
status = SessionStatus.signedOut;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
@ -18,7 +17,6 @@ class UploadRecord {
|
|||||||
this.status = TaskStatus.enqueued,
|
this.status = TaskStatus.enqueued,
|
||||||
this.sizeBytes = -1,
|
this.sizeBytes = -1,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
this.organizationId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final String taskId;
|
final String taskId;
|
||||||
@ -29,7 +27,6 @@ class UploadRecord {
|
|||||||
final TaskStatus status;
|
final TaskStatus status;
|
||||||
final int sizeBytes;
|
final int sizeBytes;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final String? organizationId;
|
|
||||||
|
|
||||||
bool get isFinished => status.isFinalState;
|
bool get isFinished => status.isFinalState;
|
||||||
bool get canRetry => status == TaskStatus.failed;
|
bool get canRetry => status == TaskStatus.failed;
|
||||||
@ -48,7 +45,6 @@ class UploadRecord {
|
|||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
sizeBytes: sizeBytes ?? this.sizeBytes,
|
sizeBytes: sizeBytes ?? this.sizeBytes,
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
organizationId: organizationId,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,28 +64,15 @@ class UploadController extends ChangeNotifier {
|
|||||||
final Map<String, Task> _tasks = {};
|
final Map<String, Task> _tasks = {};
|
||||||
bool _notificationPermissionChecked = false;
|
bool _notificationPermissionChecked = false;
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
String? _organizationId;
|
|
||||||
|
|
||||||
List<UploadRecord> get uploads {
|
List<UploadRecord> get uploads {
|
||||||
final result =
|
final result = _uploads.values.toList()
|
||||||
_uploads.values
|
|
||||||
.where(
|
|
||||||
(item) =>
|
|
||||||
item.organizationId == null ||
|
|
||||||
item.organizationId == _organizationId,
|
|
||||||
)
|
|
||||||
.toList()
|
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int completedVersion = 0;
|
int completedVersion = 0;
|
||||||
|
|
||||||
void selectOrganization(String? organizationId) {
|
|
||||||
if (_organizationId == organizationId) return;
|
|
||||||
_organizationId = organizationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _restoreHistory() async {
|
Future<void> _restoreHistory() async {
|
||||||
// start() initializes tracking asynchronously during application startup.
|
// start() initializes tracking asynchronously during application startup.
|
||||||
// A short retry also covers slower devices on their first launch.
|
// A short retry also covers slower devices on their first launch.
|
||||||
@ -119,8 +102,6 @@ class UploadController extends ChangeNotifier {
|
|||||||
await _ensureNotificationPermission();
|
await _ensureNotificationPermission();
|
||||||
final token = await _tokens.read();
|
final token = await _tokens.read();
|
||||||
if (token == null) throw StateError('Your session has expired');
|
if (token == null) throw StateError('Your session has expired');
|
||||||
final organizationId = await _tokens.readOrganization();
|
|
||||||
if (organizationId == null) throw StateError('Select an organization');
|
|
||||||
final task = UploadTask(
|
final task = UploadTask(
|
||||||
url: '${AppConfig.apiBaseUrl}/files/upload',
|
url: '${AppConfig.apiBaseUrl}/files/upload',
|
||||||
filename: filename,
|
filename: filename,
|
||||||
@ -128,18 +109,12 @@ class UploadController extends ChangeNotifier {
|
|||||||
baseDirectory: BaseDirectory.applicationSupport,
|
baseDirectory: BaseDirectory.applicationSupport,
|
||||||
fileField: 'file',
|
fileField: 'file',
|
||||||
fields: folderId == null ? const {} : {'folderId': folderId},
|
fields: folderId == null ? const {} : {'folderId': folderId},
|
||||||
headers: {
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
'Authorization': 'Bearer $token',
|
|
||||||
'X-Organization-Id': organizationId,
|
|
||||||
},
|
|
||||||
group: 'uploads',
|
group: 'uploads',
|
||||||
updates: Updates.statusAndProgress,
|
updates: Updates.statusAndProgress,
|
||||||
retries: 2,
|
retries: 2,
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
metaData: jsonEncode({
|
metaData: displayName,
|
||||||
'name': displayName,
|
|
||||||
'organizationId': organizationId,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
_tasks[task.taskId] = task;
|
_tasks[task.taskId] = task;
|
||||||
_uploads[task.taskId] = UploadRecord(
|
_uploads[task.taskId] = UploadRecord(
|
||||||
@ -147,7 +122,6 @@ class UploadController extends ChangeNotifier {
|
|||||||
name: displayName,
|
name: displayName,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
createdAt: task.creationTime,
|
createdAt: task.creationTime,
|
||||||
organizationId: organizationId,
|
|
||||||
);
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
final accepted = await FileDownloader().enqueue(task);
|
final accepted = await FileDownloader().enqueue(task);
|
||||||
@ -206,40 +180,23 @@ class UploadController extends ChangeNotifier {
|
|||||||
_tasks[record.taskId] = record.task;
|
_tasks[record.taskId] = record.task;
|
||||||
_uploads[record.taskId] = UploadRecord(
|
_uploads[record.taskId] = UploadRecord(
|
||||||
taskId: record.taskId,
|
taskId: record.taskId,
|
||||||
name: _taskName(record.task),
|
name: record.task.displayName,
|
||||||
filename: record.task.filename,
|
filename: record.task.filename,
|
||||||
createdAt: record.task.creationTime,
|
createdAt: record.task.creationTime,
|
||||||
progress: record.progress.clamp(0, 1),
|
progress: record.progress.clamp(0, 1),
|
||||||
status: record.status,
|
status: record.status,
|
||||||
sizeBytes: record.expectedFileSize,
|
sizeBytes: record.expectedFileSize,
|
||||||
errorMessage: record.exception?.description,
|
errorMessage: record.exception?.description,
|
||||||
organizationId: _taskOrganization(record.task),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
UploadRecord _fromTask(Task task) => UploadRecord(
|
UploadRecord _fromTask(Task task) => UploadRecord(
|
||||||
taskId: task.taskId,
|
taskId: task.taskId,
|
||||||
name: _taskName(task),
|
name: task.displayName,
|
||||||
filename: task.filename,
|
filename: task.filename,
|
||||||
createdAt: task.creationTime,
|
createdAt: task.creationTime,
|
||||||
organizationId: _taskOrganization(task),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic>? _taskMetadata(Task task) {
|
|
||||||
try {
|
|
||||||
final value = jsonDecode(task.metaData);
|
|
||||||
return value is Map<String, dynamic> ? value : null;
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _taskName(Task task) =>
|
|
||||||
_taskMetadata(task)?['name'] as String? ?? task.displayName;
|
|
||||||
|
|
||||||
String? _taskOrganization(Task task) =>
|
|
||||||
_taskMetadata(task)?['organizationId'] as String?;
|
|
||||||
|
|
||||||
Future<void> _deleteStagedFile(Task task) async {
|
Future<void> _deleteStagedFile(Task task) async {
|
||||||
final file = File(await task.filePath());
|
final file = File(await task.filePath());
|
||||||
if (await file.exists()) await file.delete();
|
if (await file.exists()) await file.delete();
|
||||||
@ -261,14 +218,9 @@ class UploadController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
final token = await _tokens.read();
|
final token = await _tokens.read();
|
||||||
if (token == null) throw StateError('Your session has expired');
|
if (token == null) throw StateError('Your session has expired');
|
||||||
final organizationId = await _tokens.readOrganization();
|
|
||||||
if (organizationId == null) throw StateError('Select an organization');
|
|
||||||
final retried = original.copyWith(
|
final retried = original.copyWith(
|
||||||
taskId: DateTime.now().microsecondsSinceEpoch.toString(),
|
taskId: DateTime.now().microsecondsSinceEpoch.toString(),
|
||||||
headers: {
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
'Authorization': 'Bearer $token',
|
|
||||||
'X-Organization-Id': organizationId,
|
|
||||||
},
|
|
||||||
creationTime: DateTime.now(),
|
creationTime: DateTime.now(),
|
||||||
retriesRemaining: original.retries,
|
retriesRemaining: original.retries,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -73,9 +73,7 @@ class WhatsDriveApp extends StatelessWidget {
|
|||||||
builder: (context, session, _) => switch (session.status) {
|
builder: (context, session, _) => switch (session.status) {
|
||||||
SessionStatus.loading => const SplashScreen(),
|
SessionStatus.loading => const SplashScreen(),
|
||||||
SessionStatus.signedOut => const LoginScreen(),
|
SessionStatus.signedOut => const LoginScreen(),
|
||||||
SessionStatus.signedIn => DriveScreen(
|
SessionStatus.signedIn => const DriveScreen(),
|
||||||
key: ValueKey(session.organizationId),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -7,7 +7,6 @@ class UserProfile {
|
|||||||
required this.email,
|
required this.email,
|
||||||
required this.gender,
|
required this.gender,
|
||||||
required this.age,
|
required this.age,
|
||||||
this.memberships = const [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
@ -17,14 +16,6 @@ class UserProfile {
|
|||||||
final String email;
|
final String email;
|
||||||
final String gender;
|
final String gender;
|
||||||
final int age;
|
final int age;
|
||||||
final List<OrganizationMembership> memberships;
|
|
||||||
|
|
||||||
List<OrganizationMembership> get approvedMemberships => memberships
|
|
||||||
.where(
|
|
||||||
(item) =>
|
|
||||||
item.status == 'APPROVED' && item.organization.status == 'ACTIVE',
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
factory UserProfile.fromJson(Map<String, dynamic> json) => UserProfile(
|
factory UserProfile.fromJson(Map<String, dynamic> json) => UserProfile(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
@ -34,53 +25,5 @@ class UserProfile {
|
|||||||
email: json['email'] as String,
|
email: json['email'] as String,
|
||||||
gender: json['gender'] as String,
|
gender: json['gender'] as String,
|
||||||
age: json['age'] as int,
|
age: json['age'] as int,
|
||||||
memberships: (json['memberships'] as List? ?? const [])
|
|
||||||
.map((item) => OrganizationMembership.fromJson(item))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrganizationSummary {
|
|
||||||
const OrganizationSummary({
|
|
||||||
required this.id,
|
|
||||||
required this.code,
|
|
||||||
required this.name,
|
|
||||||
required this.status,
|
|
||||||
});
|
|
||||||
final String id;
|
|
||||||
final String code;
|
|
||||||
final String name;
|
|
||||||
final String status;
|
|
||||||
|
|
||||||
factory OrganizationSummary.fromJson(Map<String, dynamic> json) =>
|
|
||||||
OrganizationSummary(
|
|
||||||
id: json['id'] as String,
|
|
||||||
code: json['code'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
status: json['status'] as String,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrganizationMembership {
|
|
||||||
const OrganizationMembership({
|
|
||||||
required this.id,
|
|
||||||
required this.role,
|
|
||||||
required this.status,
|
|
||||||
required this.organization,
|
|
||||||
this.rejectionReason,
|
|
||||||
});
|
|
||||||
final String id;
|
|
||||||
final String role;
|
|
||||||
final String status;
|
|
||||||
final String? rejectionReason;
|
|
||||||
final OrganizationSummary organization;
|
|
||||||
|
|
||||||
factory OrganizationMembership.fromJson(Map<String, dynamic> json) =>
|
|
||||||
OrganizationMembership(
|
|
||||||
id: json['id'] as String,
|
|
||||||
role: json['role'] as String,
|
|
||||||
status: json['status'] as String,
|
|
||||||
rejectionReason: json['rejectionReason'] as String?,
|
|
||||||
organization: OrganizationSummary.fromJson(json['organization']),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,9 +44,6 @@ class _DriveScreenState extends State<DriveScreen> {
|
|||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
context.read<UploadController>().selectOrganization(
|
|
||||||
context.read<SessionController>().organizationId,
|
|
||||||
);
|
|
||||||
final controller = context.read<UploadController>();
|
final controller = context.read<UploadController>();
|
||||||
if (_uploadController == controller) return;
|
if (_uploadController == controller) return;
|
||||||
_uploadController?.removeListener(_onUploadChanged);
|
_uploadController?.removeListener(_onUploadChanged);
|
||||||
@ -253,9 +250,7 @@ class _DriveScreenState extends State<DriveScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final session = context.watch<SessionController>();
|
final user = context.watch<SessionController>().user!;
|
||||||
final user = session.user!;
|
|
||||||
final membership = session.membership;
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: _path.isEmpty,
|
canPop: _path.isEmpty,
|
||||||
onPopInvokedWithResult: (didPop, result) {
|
onPopInvokedWithResult: (didPop, result) {
|
||||||
@ -270,13 +265,11 @@ class _DriveScreenState extends State<DriveScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_path.isEmpty
|
_path.isEmpty ? 'Metatron-Drive' : _path.last.name,
|
||||||
? membership?.organization.name ?? 'Metatron-Drive'
|
|
||||||
: _path.last.name,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.w800),
|
style: const TextStyle(fontWeight: FontWeight.w800),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Hello, ${user.name.split(' ').first} • ${membership?.organization.code ?? ''}',
|
'Hello, ${user.name.split(' ').first}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
@ -286,28 +279,6 @@ class _DriveScreenState extends State<DriveScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
PopupMenuButton<String>(
|
|
||||||
tooltip: 'Switch organization',
|
|
||||||
icon: const Icon(Icons.business_outlined),
|
|
||||||
onSelected: session.selectOrganization,
|
|
||||||
itemBuilder: (_) => user.approvedMemberships
|
|
||||||
.map(
|
|
||||||
(item) => PopupMenuItem(
|
|
||||||
value: item.organization.id,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
if (item.organization.id == session.organizationId)
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(right: 8),
|
|
||||||
child: Icon(Icons.check, size: 18),
|
|
||||||
),
|
|
||||||
Text(item.organization.name),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.of(context).push(
|
onPressed: () => Navigator.of(context).push(
|
||||||
MaterialPageRoute(builder: (_) => const UploadHistoryScreen()),
|
MaterialPageRoute(builder: (_) => const UploadHistoryScreen()),
|
||||||
@ -441,7 +412,6 @@ class _DriveScreenState extends State<DriveScreen> {
|
|||||||
file: _files[index],
|
file: _files[index],
|
||||||
token: _token,
|
token: _token,
|
||||||
api: context.read<ApiClient>(),
|
api: context.read<ApiClient>(),
|
||||||
organizationId: session.organizationId,
|
|
||||||
onDelete: () => _deleteFile(_files[index]),
|
onDelete: () => _deleteFile(_files[index]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -487,15 +457,6 @@ class _DriveScreenState extends State<DriveScreen> {
|
|||||||
style: const TextStyle(color: Colors.black54),
|
style: const TextStyle(color: Colors.black54),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 22),
|
const SizedBox(height: 22),
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(sheetContext);
|
|
||||||
_joinOrganization();
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.group_add_outlined),
|
|
||||||
label: const Text('Join another organization'),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(sheetContext);
|
Navigator.pop(sheetContext);
|
||||||
@ -520,56 +481,6 @@ class _DriveScreenState extends State<DriveScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _joinOrganization() async {
|
|
||||||
final code = TextEditingController();
|
|
||||||
final submitted = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (dialogContext) => AlertDialog(
|
|
||||||
title: const Text('Join an organization'),
|
|
||||||
content: TextField(
|
|
||||||
controller: code,
|
|
||||||
textCapitalization: TextCapitalization.characters,
|
|
||||||
decoration: const InputDecoration(labelText: 'Organization code'),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(dialogContext, false),
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () => Navigator.pop(dialogContext, true),
|
|
||||||
child: const Text('Submit request'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (submitted != true || !mounted) {
|
|
||||||
code.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final message = await context.read<ApiClient>().joinOrganization(
|
|
||||||
code.text.trim().toUpperCase(),
|
|
||||||
);
|
|
||||||
if (mounted) {
|
|
||||||
await context.read<SessionController>().refreshProfile();
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text(message)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text(context.read<ApiClient>().messageFrom(error))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
code.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _confirmAccountDeletion() async {
|
Future<void> _confirmAccountDeletion() async {
|
||||||
final password = TextEditingController();
|
final password = TextEditingController();
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
@ -757,13 +668,11 @@ class _FileCard extends StatelessWidget {
|
|||||||
required this.file,
|
required this.file,
|
||||||
required this.token,
|
required this.token,
|
||||||
required this.api,
|
required this.api,
|
||||||
required this.organizationId,
|
|
||||||
required this.onDelete,
|
required this.onDelete,
|
||||||
});
|
});
|
||||||
final DriveFile file;
|
final DriveFile file;
|
||||||
final String? token;
|
final String? token;
|
||||||
final ApiClient api;
|
final ApiClient api;
|
||||||
final String? organizationId;
|
|
||||||
final VoidCallback onDelete;
|
final VoidCallback onDelete;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Card(
|
Widget build(BuildContext context) => Card(
|
||||||
@ -777,10 +686,7 @@ class _FileCard extends StatelessWidget {
|
|||||||
child: file.isImage && token != null
|
child: file.isImage && token != null
|
||||||
? Image.network(
|
? Image.network(
|
||||||
api.contentUrl(file.id),
|
api.contentUrl(file.id),
|
||||||
headers: {
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
'Authorization': 'Bearer $token',
|
|
||||||
..._organizationHeader(organizationId),
|
|
||||||
},
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, _, _) =>
|
errorBuilder: (_, _, _) =>
|
||||||
const Icon(Icons.broken_image_outlined, size: 42),
|
const Icon(Icons.broken_image_outlined, size: 42),
|
||||||
@ -835,9 +741,6 @@ class _FileCard extends StatelessWidget {
|
|||||||
: '${(bytes / 1024 / 1024).toStringAsFixed(1)} MB';
|
: '${(bytes / 1024 / 1024).toStringAsFixed(1)} MB';
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> _organizationHeader(String? organizationId) =>
|
|
||||||
organizationId == null ? const {} : {'X-Organization-Id': organizationId};
|
|
||||||
|
|
||||||
class _UploadStrip extends StatelessWidget {
|
class _UploadStrip extends StatelessWidget {
|
||||||
const _UploadStrip();
|
const _UploadStrip();
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -12,7 +12,6 @@ class RegisterScreen extends StatefulWidget {
|
|||||||
class _RegisterScreenState extends State<RegisterScreen> {
|
class _RegisterScreenState extends State<RegisterScreen> {
|
||||||
final _form = GlobalKey<FormState>();
|
final _form = GlobalKey<FormState>();
|
||||||
final _name = TextEditingController();
|
final _name = TextEditingController();
|
||||||
final _organizationCode = TextEditingController();
|
|
||||||
final _username = TextEditingController();
|
final _username = TextEditingController();
|
||||||
final _email = TextEditingController();
|
final _email = TextEditingController();
|
||||||
final _phone = TextEditingController();
|
final _phone = TextEditingController();
|
||||||
@ -21,14 +20,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
for (final value in [
|
for (final value in [_name, _username, _email, _phone, _password]) {
|
||||||
_organizationCode,
|
|
||||||
_name,
|
|
||||||
_username,
|
|
||||||
_email,
|
|
||||||
_phone,
|
|
||||||
_password,
|
|
||||||
]) {
|
|
||||||
value.dispose();
|
value.dispose();
|
||||||
}
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@ -38,8 +30,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
if (!_form.currentState!.validate()) return;
|
if (!_form.currentState!.validate()) return;
|
||||||
setState(() => _busy = true);
|
setState(() => _busy = true);
|
||||||
try {
|
try {
|
||||||
final message = await context.read<SessionController>().register({
|
await context.read<SessionController>().register({
|
||||||
'organizationCode': _organizationCode.text.trim().toUpperCase(),
|
|
||||||
'name': _name.text.trim(),
|
'name': _name.text.trim(),
|
||||||
'username': _username.text.trim().toLowerCase(),
|
'username': _username.text.trim().toLowerCase(),
|
||||||
'email': _email.text.trim().toLowerCase(),
|
'email': _email.text.trim().toLowerCase(),
|
||||||
@ -50,11 +41,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
'gender': 'Prefer not to say',
|
'gender': 'Prefer not to say',
|
||||||
'password': _password.text,
|
'password': _password.text,
|
||||||
});
|
});
|
||||||
if (mounted) {
|
if (mounted) Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
|
||||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
||||||
messenger.showSnackBar(SnackBar(content: Text(message)));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@ -96,18 +83,6 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
'We create your personal Google Drive folder automatically.',
|
'We create your personal Google Drive folder automatically.',
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
TextFormField(
|
|
||||||
controller: _organizationCode,
|
|
||||||
textCapitalization: TextCapitalization.characters,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Organization code',
|
|
||||||
prefixIcon: Icon(Icons.business_outlined),
|
|
||||||
),
|
|
||||||
validator: (value) => (value?.trim().length ?? 0) < 4
|
|
||||||
? 'Enter your organization code'
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _name,
|
controller: _name,
|
||||||
textCapitalization: TextCapitalization.words,
|
textCapitalization: TextCapitalization.words,
|
||||||
|
|||||||
@ -17,13 +17,7 @@ class ApiClient {
|
|||||||
InterceptorsWrapper(
|
InterceptorsWrapper(
|
||||||
onRequest: (options, handler) async {
|
onRequest: (options, handler) async {
|
||||||
final token = await _tokens.read();
|
final token = await _tokens.read();
|
||||||
if (token != null) {
|
if (token != null) options.headers['Authorization'] = 'Bearer $token';
|
||||||
options.headers['Authorization'] = 'Bearer $token';
|
|
||||||
}
|
|
||||||
final organizationId = await _tokens.readOrganization();
|
|
||||||
if (organizationId != null) {
|
|
||||||
options.headers['X-Organization-Id'] = organizationId;
|
|
||||||
}
|
|
||||||
handler.next(options);
|
handler.next(options);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -59,9 +53,14 @@ class ApiClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> register(Map<String, dynamic> input) async {
|
Future<({String token, UserProfile user})> register(
|
||||||
|
Map<String, dynamic> input,
|
||||||
|
) async {
|
||||||
final response = await dio.post('/auth/register', data: input);
|
final response = await dio.post('/auth/register', data: input);
|
||||||
return response.data['message'] as String;
|
return (
|
||||||
|
token: response.data['token'] as String,
|
||||||
|
user: UserProfile.fromJson(response.data['user']),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UserProfile> me() async {
|
Future<UserProfile> me() async {
|
||||||
@ -72,21 +71,6 @@ class ApiClient {
|
|||||||
Future<void> deleteAccount(String password) =>
|
Future<void> deleteAccount(String password) =>
|
||||||
dio.delete('/retention/account', data: {'password': password});
|
dio.delete('/retention/account', data: {'password': password});
|
||||||
|
|
||||||
Future<OrganizationSummary> lookupOrganization(String code) async {
|
|
||||||
final response = await dio.get(
|
|
||||||
'/organizations/lookup/${Uri.encodeComponent(code)}',
|
|
||||||
);
|
|
||||||
return OrganizationSummary.fromJson(response.data['organization']);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> joinOrganization(String code) async {
|
|
||||||
final response = await dio.post(
|
|
||||||
'/auth/join',
|
|
||||||
data: {'organizationCode': code},
|
|
||||||
);
|
|
||||||
return response.data['message'] as String;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<DriveFolder>> folders(String? parentId) async {
|
Future<List<DriveFolder>> folders(String? parentId) async {
|
||||||
final response = await dio.get(
|
final response = await dio.get(
|
||||||
'/folders',
|
'/folders',
|
||||||
|
|||||||
@ -2,15 +2,10 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||||||
|
|
||||||
class TokenStore {
|
class TokenStore {
|
||||||
static const _tokenKey = 'auth_token';
|
static const _tokenKey = 'auth_token';
|
||||||
static const _organizationKey = 'organization_id';
|
|
||||||
const TokenStore();
|
const TokenStore();
|
||||||
FlutterSecureStorage get _storage => const FlutterSecureStorage();
|
FlutterSecureStorage get _storage => const FlutterSecureStorage();
|
||||||
Future<String?> read() => _storage.read(key: _tokenKey);
|
Future<String?> read() => _storage.read(key: _tokenKey);
|
||||||
Future<void> write(String token) =>
|
Future<void> write(String token) =>
|
||||||
_storage.write(key: _tokenKey, value: token);
|
_storage.write(key: _tokenKey, value: token);
|
||||||
Future<void> clear() => _storage.delete(key: _tokenKey);
|
Future<void> clear() => _storage.delete(key: _tokenKey);
|
||||||
Future<String?> readOrganization() => _storage.read(key: _organizationKey);
|
|
||||||
Future<void> writeOrganization(String id) =>
|
|
||||||
_storage.write(key: _organizationKey, value: id);
|
|
||||||
Future<void> clearOrganization() => _storage.delete(key: _organizationKey);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user