Compare commits
33 Commits
3.3.0+144
...
7957e4894a
| Author | SHA1 | Date | |
|---|---|---|---|
|
7957e4894a
|
|||
|
f94f80c375
|
|||
|
74fa2215a6
|
|||
|
0d11435feb
|
|||
|
e22598b0a6
|
|||
|
84cfe643f5
|
|||
|
05ac04e9a2
|
|||
|
66f283d6e8
|
|||
|
c779c7523c
|
|||
|
ac7cb29afe
|
|||
|
935aa77223
|
|||
|
24e5b3b824
|
|||
|
0391893b32
|
|||
|
b8d24876c8
|
|||
|
0493661f9a
|
|||
|
b40afde00f
|
|||
|
78a4022531
|
|||
|
8a291c80b7
|
|||
|
1395d65b76
|
|||
|
eb4942e0ed
|
|||
|
f254cfa81e
|
|||
|
4927795260
|
|||
|
e4019dadc8
|
|||
|
5e7d77e1a1
|
|||
|
bfcbed035c
|
|||
|
5ebefae961
|
|||
|
d4758674bb
|
|||
|
f5f1ddc0ea
|
|||
|
2720b59485
|
|||
|
29b1ac7fce
|
|||
|
83ca5551ad
|
|||
| 611cb024a9 | |||
|
74fb56891d
|
@@ -43,6 +43,16 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- App protocol -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
|
||||
<data android:scheme="solian" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deeplinking -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -163,6 +163,7 @@
|
||||
"accountConnectionProviderDiscord": "Discord",
|
||||
"accountConnectionProviderAfdian": "Afdian",
|
||||
"accountConnectionProviderSpotify": "Spotify",
|
||||
"accountConnectionProviderSteam": "Steam",
|
||||
"checkIn": "Check In",
|
||||
"checkInNone": "Not checked-in yet",
|
||||
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
||||
@@ -1086,6 +1087,7 @@
|
||||
"levelingStage10": "Immortal",
|
||||
"levelingStage11": "Divine",
|
||||
"levelingStage12": "Transcendent",
|
||||
"uploadTasks": "Upload Tasks",
|
||||
"uploadAttachment": "Upload Attachment",
|
||||
"attachmentPreview": "Attachment Preview",
|
||||
"selectPool": "Select Pool",
|
||||
|
||||
BIN
assets/icons/icon-tray.png
Normal file
BIN
assets/icons/icon-tray.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
1
assets/images/oidc/steam.svg
Normal file
1
assets/images/oidc/steam.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="2471" height="2500" viewBox="0 0 256 259" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M127.779 0C60.42 0 5.24 52.412 0 119.014l68.724 28.674a35.812 35.812 0 0 1 20.426-6.366c.682 0 1.356.019 2.02.056l30.566-44.71v-.626c0-26.903 21.69-48.796 48.353-48.796 26.662 0 48.352 21.893 48.352 48.796 0 26.902-21.69 48.804-48.352 48.804-.37 0-.73-.009-1.098-.018l-43.593 31.377c.028.582.046 1.163.046 1.735 0 20.204-16.283 36.636-36.294 36.636-17.566 0-32.263-12.658-35.584-29.412L4.41 164.654c15.223 54.313 64.673 94.132 123.369 94.132 70.818 0 128.221-57.938 128.221-129.393C256 57.93 198.597 0 127.779 0zM80.352 196.332l-15.749-6.568c2.787 5.867 7.621 10.775 14.033 13.47 13.857 5.83 29.836-.803 35.612-14.799a27.555 27.555 0 0 0 .046-21.035c-2.768-6.79-7.999-12.086-14.706-14.909-6.67-2.795-13.811-2.694-20.085-.304l16.275 6.79c10.222 4.3 15.056 16.145 10.794 26.46-4.253 10.314-15.998 15.195-26.22 10.895zm121.957-100.29c0-17.925-14.457-32.52-32.217-32.52-17.769 0-32.226 14.595-32.226 32.52 0 17.926 14.457 32.512 32.226 32.512 17.76 0 32.217-14.586 32.217-32.512zm-56.37-.055c0-13.488 10.84-24.42 24.2-24.42 13.368 0 24.208 10.932 24.208 24.42 0 13.488-10.84 24.421-24.209 24.421-13.359 0-24.2-10.933-24.2-24.42z" fill="#1A1918"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +1,6 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
- drift: true
|
||||
- provider: true
|
||||
- shared_preferences: true
|
||||
@@ -1,7 +1,5 @@
|
||||
PODS:
|
||||
- Alamofire (5.10.2)
|
||||
- app_links (6.4.1):
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- croppy (0.0.1):
|
||||
@@ -52,18 +50,18 @@ PODS:
|
||||
- Firebase/Messaging (12.4.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 12.4.0)
|
||||
- firebase_analytics (12.0.3):
|
||||
- firebase_analytics (12.0.4):
|
||||
- firebase_core
|
||||
- FirebaseAnalytics (= 12.4.0)
|
||||
- Flutter
|
||||
- firebase_core (4.2.0):
|
||||
- firebase_core (4.2.1):
|
||||
- Firebase/CoreOnly (= 12.4.0)
|
||||
- Flutter
|
||||
- firebase_crashlytics (5.0.3):
|
||||
- firebase_crashlytics (5.0.4):
|
||||
- Firebase/Crashlytics (= 12.4.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_messaging (16.0.3):
|
||||
- firebase_messaging (16.0.4):
|
||||
- Firebase/Messaging (= 12.4.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
@@ -265,6 +263,8 @@ PODS:
|
||||
- PromisesObjC (2.4.0)
|
||||
- PromisesSwift (2.4.0):
|
||||
- PromisesObjC (= 2.4.0)
|
||||
- protocol_handler_ios (0.0.1):
|
||||
- Flutter
|
||||
- receive_sharing_intent (1.8.1):
|
||||
- Flutter
|
||||
- record_ios (1.1.0):
|
||||
@@ -323,7 +323,6 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- Alamofire
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
@@ -358,6 +357,7 @@ DEPENDENCIES:
|
||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
||||
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
@@ -404,8 +404,6 @@ SPEC REPOS:
|
||||
- WebRTC-SDK
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: ".symlinks/plugins/app_links/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
croppy:
|
||||
@@ -470,6 +468,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
pointer_interceptor_ios:
|
||||
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
||||
protocol_handler_ios:
|
||||
:path: ".symlinks/plugins/protocol_handler_ios/ios"
|
||||
receive_sharing_intent:
|
||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||
record_ios:
|
||||
@@ -497,7 +497,6 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
@@ -506,10 +505,10 @@ SPEC CHECKSUMS:
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||
firebase_analytics: 1d024068b1d4707d5ba7a42a12976ddf3316d835
|
||||
firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464
|
||||
firebase_crashlytics: f3a9a4338ab99b67042f64e9e22e1bf349cb44ed
|
||||
firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4
|
||||
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
|
||||
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
||||
firebase_crashlytics: 83c7467d7534975a4d779af43bd226d0a4616464
|
||||
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
|
||||
@@ -553,6 +552,7 @@ SPEC CHECKSUMS:
|
||||
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
|
||||
@@ -1,108 +1,111 @@
|
||||
<?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>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>BUNDLE_ID</key>
|
||||
<string>dev.solsynth.solian</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Solian</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>solian</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>CLIENT_ID</key>
|
||||
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INStartCallIntent</string>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>PLIST_VERSION</key>
|
||||
<string>1</string>
|
||||
<key>REVERSED_CLIENT_ID</key>
|
||||
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>audio</string>
|
||||
<string>remote-notification</string>
|
||||
<string>voip</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>BUNDLE_ID</key>
|
||||
<string>dev.solsynth.solian</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true />
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Solian</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>solian</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>CLIENT_ID</key>
|
||||
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false />
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Allow the Solar Network verify your ownership of the logged in account and continue
|
||||
your action quickly.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INStartCallIntent</string>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>PLIST_VERSION</key>
|
||||
<string>1</string>
|
||||
<key>REVERSED_CLIENT_ID</key>
|
||||
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true />
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>audio</string>
|
||||
<string>remote-notification</string>
|
||||
<string>voip</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false />
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -48,3 +48,11 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Placeholder Implementations for Preview ---
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import 'package:talker_flutter/talker_flutter.dart';
|
||||
import 'package:talker_riverpod_logger/talker_riverpod_logger.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:protocol_handler/protocol_handler.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
@@ -50,8 +51,16 @@ void main() async {
|
||||
GoRouter.optionURLReflectsImperativeAPIs = true;
|
||||
}
|
||||
|
||||
if (!kIsWeb && (Platform.isLinux || Platform.isMacOS || Platform.isWindows)) {
|
||||
talker.info("[SplashScreen] Initializing desktop window manager...");
|
||||
await protocolHandler.register('myprotocol');
|
||||
talker.info("[SplashScreen] Desktop window manager is ready!");
|
||||
}
|
||||
|
||||
try {
|
||||
await EasyLocalization.ensureInitialized();
|
||||
// Disable logs
|
||||
EasyLocalization.logger.enableBuildModes = [];
|
||||
|
||||
if (kIsWeb || !Platform.isLinux) {
|
||||
await Firebase.initializeApp(
|
||||
|
||||
57
lib/models/drive_task.dart
Normal file
57
lib/models/drive_task.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
|
||||
part 'drive_task.freezed.dart';
|
||||
part 'drive_task.g.dart';
|
||||
|
||||
enum DriveTaskStatus {
|
||||
pending,
|
||||
inProgress,
|
||||
paused,
|
||||
completed,
|
||||
failed,
|
||||
expired,
|
||||
cancelled,
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class DriveTask with _$DriveTask {
|
||||
const DriveTask._();
|
||||
|
||||
const factory DriveTask({
|
||||
required String id,
|
||||
required String taskId,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
required int fileSize,
|
||||
required int uploadedBytes,
|
||||
required int totalChunks,
|
||||
required int uploadedChunks,
|
||||
required DriveTaskStatus status,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required String type, // Task type (e.g., 'FileUpload')
|
||||
double? transmissionProgress, // Local file upload progress (0.0-1.0)
|
||||
String? errorMessage,
|
||||
String? statusMessage,
|
||||
SnCloudFile? result,
|
||||
String? poolId,
|
||||
String? bundleId,
|
||||
String? encryptPassword,
|
||||
String? expiredAt,
|
||||
}) = _DriveTask;
|
||||
|
||||
factory DriveTask.fromJson(Map<String, dynamic> json) =>
|
||||
_$DriveTaskFromJson(json);
|
||||
|
||||
double get progress => totalChunks > 0 ? uploadedChunks / totalChunks : 0.0;
|
||||
|
||||
Duration get estimatedTimeRemaining {
|
||||
if (uploadedBytes == 0 || fileSize == 0) return Duration.zero;
|
||||
final remainingBytes = fileSize - uploadedBytes;
|
||||
final uploadRate =
|
||||
uploadedBytes / createdAt.difference(DateTime.now()).inSeconds.abs();
|
||||
if (uploadRate == 0) return Duration.zero;
|
||||
return Duration(seconds: (remainingBytes / uploadRate).round());
|
||||
}
|
||||
}
|
||||
356
lib/models/drive_task.freezed.dart
Normal file
356
lib/models/drive_task.freezed.dart
Normal file
@@ -0,0 +1,356 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'drive_task.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$DriveTask {
|
||||
|
||||
String get id; String get taskId; String get fileName; String get contentType; int get fileSize; int get uploadedBytes; int get totalChunks; int get uploadedChunks; DriveTaskStatus get status; DateTime get createdAt; DateTime get updatedAt; String get type;// Task type (e.g., 'FileUpload')
|
||||
double? get transmissionProgress;// Local file upload progress (0.0-1.0)
|
||||
String? get errorMessage; String? get statusMessage; SnCloudFile? get result; String? get poolId; String? get bundleId; String? get encryptPassword; String? get expiredAt;
|
||||
/// Create a copy of DriveTask
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$DriveTaskCopyWith<DriveTask> get copyWith => _$DriveTaskCopyWithImpl<DriveTask>(this as DriveTask, _$identity);
|
||||
|
||||
/// Serializes this DriveTask to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is DriveTask&&(identical(other.id, id) || other.id == id)&&(identical(other.taskId, taskId) || other.taskId == taskId)&&(identical(other.fileName, fileName) || other.fileName == fileName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.fileSize, fileSize) || other.fileSize == fileSize)&&(identical(other.uploadedBytes, uploadedBytes) || other.uploadedBytes == uploadedBytes)&&(identical(other.totalChunks, totalChunks) || other.totalChunks == totalChunks)&&(identical(other.uploadedChunks, uploadedChunks) || other.uploadedChunks == uploadedChunks)&&(identical(other.status, status) || other.status == status)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.type, type) || other.type == type)&&(identical(other.transmissionProgress, transmissionProgress) || other.transmissionProgress == transmissionProgress)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.statusMessage, statusMessage) || other.statusMessage == statusMessage)&&(identical(other.result, result) || other.result == result)&&(identical(other.poolId, poolId) || other.poolId == poolId)&&(identical(other.bundleId, bundleId) || other.bundleId == bundleId)&&(identical(other.encryptPassword, encryptPassword) || other.encryptPassword == encryptPassword)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hashAll([runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,type,transmissionProgress,errorMessage,statusMessage,result,poolId,bundleId,encryptPassword,expiredAt]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DriveTask(id: $id, taskId: $taskId, fileName: $fileName, contentType: $contentType, fileSize: $fileSize, uploadedBytes: $uploadedBytes, totalChunks: $totalChunks, uploadedChunks: $uploadedChunks, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, type: $type, transmissionProgress: $transmissionProgress, errorMessage: $errorMessage, statusMessage: $statusMessage, result: $result, poolId: $poolId, bundleId: $bundleId, encryptPassword: $encryptPassword, expiredAt: $expiredAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $DriveTaskCopyWith<$Res> {
|
||||
factory $DriveTaskCopyWith(DriveTask value, $Res Function(DriveTask) _then) = _$DriveTaskCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt
|
||||
});
|
||||
|
||||
|
||||
$SnCloudFileCopyWith<$Res>? get result;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$DriveTaskCopyWithImpl<$Res>
|
||||
implements $DriveTaskCopyWith<$Res> {
|
||||
_$DriveTaskCopyWithImpl(this._self, this._then);
|
||||
|
||||
final DriveTask _self;
|
||||
final $Res Function(DriveTask) _then;
|
||||
|
||||
/// Create a copy of DriveTask
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? taskId = null,Object? fileName = null,Object? contentType = null,Object? fileSize = null,Object? uploadedBytes = null,Object? totalChunks = null,Object? uploadedChunks = null,Object? status = null,Object? createdAt = null,Object? updatedAt = null,Object? type = null,Object? transmissionProgress = freezed,Object? errorMessage = freezed,Object? statusMessage = freezed,Object? result = freezed,Object? poolId = freezed,Object? bundleId = freezed,Object? encryptPassword = freezed,Object? expiredAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,taskId: null == taskId ? _self.taskId : taskId // ignore: cast_nullable_to_non_nullable
|
||||
as String,fileName: null == fileName ? _self.fileName : fileName // ignore: cast_nullable_to_non_nullable
|
||||
as String,contentType: null == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
|
||||
as String,fileSize: null == fileSize ? _self.fileSize : fileSize // ignore: cast_nullable_to_non_nullable
|
||||
as int,uploadedBytes: null == uploadedBytes ? _self.uploadedBytes : uploadedBytes // ignore: cast_nullable_to_non_nullable
|
||||
as int,totalChunks: null == totalChunks ? _self.totalChunks : totalChunks // ignore: cast_nullable_to_non_nullable
|
||||
as int,uploadedChunks: null == uploadedChunks ? _self.uploadedChunks : uploadedChunks // ignore: cast_nullable_to_non_nullable
|
||||
as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||
as DriveTaskStatus,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,transmissionProgress: freezed == transmissionProgress ? _self.transmissionProgress : transmissionProgress // ignore: cast_nullable_to_non_nullable
|
||||
as double?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,statusMessage: freezed == statusMessage ? _self.statusMessage : statusMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,result: freezed == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile?,poolId: freezed == poolId ? _self.poolId : poolId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,bundleId: freezed == bundleId ? _self.bundleId : bundleId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,encryptPassword: freezed == encryptPassword ? _self.encryptPassword : encryptPassword // ignore: cast_nullable_to_non_nullable
|
||||
as String?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of DriveTask
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCloudFileCopyWith<$Res>? get result {
|
||||
if (_self.result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnCloudFileCopyWith<$Res>(_self.result!, (value) {
|
||||
return _then(_self.copyWith(result: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [DriveTask].
|
||||
extension DriveTaskPatterns on DriveTask {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _DriveTask value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DriveTask() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _DriveTask value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DriveTask():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _DriveTask value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DriveTask() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DriveTask() when $default != null:
|
||||
return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fileSize,_that.uploadedBytes,_that.totalChunks,_that.uploadedChunks,_that.status,_that.createdAt,_that.updatedAt,_that.type,_that.transmissionProgress,_that.errorMessage,_that.statusMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DriveTask():
|
||||
return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fileSize,_that.uploadedBytes,_that.totalChunks,_that.uploadedChunks,_that.status,_that.createdAt,_that.updatedAt,_that.type,_that.transmissionProgress,_that.errorMessage,_that.statusMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DriveTask() when $default != null:
|
||||
return $default(_that.id,_that.taskId,_that.fileName,_that.contentType,_that.fileSize,_that.uploadedBytes,_that.totalChunks,_that.uploadedChunks,_that.status,_that.createdAt,_that.updatedAt,_that.type,_that.transmissionProgress,_that.errorMessage,_that.statusMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _DriveTask extends DriveTask {
|
||||
const _DriveTask({required this.id, required this.taskId, required this.fileName, required this.contentType, required this.fileSize, required this.uploadedBytes, required this.totalChunks, required this.uploadedChunks, required this.status, required this.createdAt, required this.updatedAt, required this.type, this.transmissionProgress, this.errorMessage, this.statusMessage, this.result, this.poolId, this.bundleId, this.encryptPassword, this.expiredAt}): super._();
|
||||
factory _DriveTask.fromJson(Map<String, dynamic> json) => _$DriveTaskFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String taskId;
|
||||
@override final String fileName;
|
||||
@override final String contentType;
|
||||
@override final int fileSize;
|
||||
@override final int uploadedBytes;
|
||||
@override final int totalChunks;
|
||||
@override final int uploadedChunks;
|
||||
@override final DriveTaskStatus status;
|
||||
@override final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
@override final String type;
|
||||
// Task type (e.g., 'FileUpload')
|
||||
@override final double? transmissionProgress;
|
||||
// Local file upload progress (0.0-1.0)
|
||||
@override final String? errorMessage;
|
||||
@override final String? statusMessage;
|
||||
@override final SnCloudFile? result;
|
||||
@override final String? poolId;
|
||||
@override final String? bundleId;
|
||||
@override final String? encryptPassword;
|
||||
@override final String? expiredAt;
|
||||
|
||||
/// Create a copy of DriveTask
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$DriveTaskCopyWith<_DriveTask> get copyWith => __$DriveTaskCopyWithImpl<_DriveTask>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$DriveTaskToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DriveTask&&(identical(other.id, id) || other.id == id)&&(identical(other.taskId, taskId) || other.taskId == taskId)&&(identical(other.fileName, fileName) || other.fileName == fileName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.fileSize, fileSize) || other.fileSize == fileSize)&&(identical(other.uploadedBytes, uploadedBytes) || other.uploadedBytes == uploadedBytes)&&(identical(other.totalChunks, totalChunks) || other.totalChunks == totalChunks)&&(identical(other.uploadedChunks, uploadedChunks) || other.uploadedChunks == uploadedChunks)&&(identical(other.status, status) || other.status == status)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.type, type) || other.type == type)&&(identical(other.transmissionProgress, transmissionProgress) || other.transmissionProgress == transmissionProgress)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.statusMessage, statusMessage) || other.statusMessage == statusMessage)&&(identical(other.result, result) || other.result == result)&&(identical(other.poolId, poolId) || other.poolId == poolId)&&(identical(other.bundleId, bundleId) || other.bundleId == bundleId)&&(identical(other.encryptPassword, encryptPassword) || other.encryptPassword == encryptPassword)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hashAll([runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,type,transmissionProgress,errorMessage,statusMessage,result,poolId,bundleId,encryptPassword,expiredAt]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DriveTask(id: $id, taskId: $taskId, fileName: $fileName, contentType: $contentType, fileSize: $fileSize, uploadedBytes: $uploadedBytes, totalChunks: $totalChunks, uploadedChunks: $uploadedChunks, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, type: $type, transmissionProgress: $transmissionProgress, errorMessage: $errorMessage, statusMessage: $statusMessage, result: $result, poolId: $poolId, bundleId: $bundleId, encryptPassword: $encryptPassword, expiredAt: $expiredAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$DriveTaskCopyWith<$Res> implements $DriveTaskCopyWith<$Res> {
|
||||
factory _$DriveTaskCopyWith(_DriveTask value, $Res Function(_DriveTask) _then) = __$DriveTaskCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, DriveTaskStatus status, DateTime createdAt, DateTime updatedAt, String type, double? transmissionProgress, String? errorMessage, String? statusMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt
|
||||
});
|
||||
|
||||
|
||||
@override $SnCloudFileCopyWith<$Res>? get result;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$DriveTaskCopyWithImpl<$Res>
|
||||
implements _$DriveTaskCopyWith<$Res> {
|
||||
__$DriveTaskCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _DriveTask _self;
|
||||
final $Res Function(_DriveTask) _then;
|
||||
|
||||
/// Create a copy of DriveTask
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? taskId = null,Object? fileName = null,Object? contentType = null,Object? fileSize = null,Object? uploadedBytes = null,Object? totalChunks = null,Object? uploadedChunks = null,Object? status = null,Object? createdAt = null,Object? updatedAt = null,Object? type = null,Object? transmissionProgress = freezed,Object? errorMessage = freezed,Object? statusMessage = freezed,Object? result = freezed,Object? poolId = freezed,Object? bundleId = freezed,Object? encryptPassword = freezed,Object? expiredAt = freezed,}) {
|
||||
return _then(_DriveTask(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,taskId: null == taskId ? _self.taskId : taskId // ignore: cast_nullable_to_non_nullable
|
||||
as String,fileName: null == fileName ? _self.fileName : fileName // ignore: cast_nullable_to_non_nullable
|
||||
as String,contentType: null == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
|
||||
as String,fileSize: null == fileSize ? _self.fileSize : fileSize // ignore: cast_nullable_to_non_nullable
|
||||
as int,uploadedBytes: null == uploadedBytes ? _self.uploadedBytes : uploadedBytes // ignore: cast_nullable_to_non_nullable
|
||||
as int,totalChunks: null == totalChunks ? _self.totalChunks : totalChunks // ignore: cast_nullable_to_non_nullable
|
||||
as int,uploadedChunks: null == uploadedChunks ? _self.uploadedChunks : uploadedChunks // ignore: cast_nullable_to_non_nullable
|
||||
as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||
as DriveTaskStatus,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,transmissionProgress: freezed == transmissionProgress ? _self.transmissionProgress : transmissionProgress // ignore: cast_nullable_to_non_nullable
|
||||
as double?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,statusMessage: freezed == statusMessage ? _self.statusMessage : statusMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,result: freezed == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile?,poolId: freezed == poolId ? _self.poolId : poolId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,bundleId: freezed == bundleId ? _self.bundleId : bundleId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,encryptPassword: freezed == encryptPassword ? _self.encryptPassword : encryptPassword // ignore: cast_nullable_to_non_nullable
|
||||
as String?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of DriveTask
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCloudFileCopyWith<$Res>? get result {
|
||||
if (_self.result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnCloudFileCopyWith<$Res>(_self.result!, (value) {
|
||||
return _then(_self.copyWith(result: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
67
lib/models/drive_task.g.dart
Normal file
67
lib/models/drive_task.g.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'drive_task.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_DriveTask _$DriveTaskFromJson(Map<String, dynamic> json) => _DriveTask(
|
||||
id: json['id'] as String,
|
||||
taskId: json['task_id'] as String,
|
||||
fileName: json['file_name'] as String,
|
||||
contentType: json['content_type'] as String,
|
||||
fileSize: (json['file_size'] as num).toInt(),
|
||||
uploadedBytes: (json['uploaded_bytes'] as num).toInt(),
|
||||
totalChunks: (json['total_chunks'] as num).toInt(),
|
||||
uploadedChunks: (json['uploaded_chunks'] as num).toInt(),
|
||||
status: $enumDecode(_$DriveTaskStatusEnumMap, json['status']),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
type: json['type'] as String,
|
||||
transmissionProgress: (json['transmission_progress'] as num?)?.toDouble(),
|
||||
errorMessage: json['error_message'] as String?,
|
||||
statusMessage: json['status_message'] as String?,
|
||||
result:
|
||||
json['result'] == null
|
||||
? null
|
||||
: SnCloudFile.fromJson(json['result'] as Map<String, dynamic>),
|
||||
poolId: json['pool_id'] as String?,
|
||||
bundleId: json['bundle_id'] as String?,
|
||||
encryptPassword: json['encrypt_password'] as String?,
|
||||
expiredAt: json['expired_at'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DriveTaskToJson(_DriveTask instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'task_id': instance.taskId,
|
||||
'file_name': instance.fileName,
|
||||
'content_type': instance.contentType,
|
||||
'file_size': instance.fileSize,
|
||||
'uploaded_bytes': instance.uploadedBytes,
|
||||
'total_chunks': instance.totalChunks,
|
||||
'uploaded_chunks': instance.uploadedChunks,
|
||||
'status': _$DriveTaskStatusEnumMap[instance.status]!,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'type': instance.type,
|
||||
'transmission_progress': instance.transmissionProgress,
|
||||
'error_message': instance.errorMessage,
|
||||
'status_message': instance.statusMessage,
|
||||
'result': instance.result?.toJson(),
|
||||
'pool_id': instance.poolId,
|
||||
'bundle_id': instance.bundleId,
|
||||
'encrypt_password': instance.encryptPassword,
|
||||
'expired_at': instance.expiredAt,
|
||||
};
|
||||
|
||||
const _$DriveTaskStatusEnumMap = {
|
||||
DriveTaskStatus.pending: 'pending',
|
||||
DriveTaskStatus.inProgress: 'inProgress',
|
||||
DriveTaskStatus.paused: 'paused',
|
||||
DriveTaskStatus.completed: 'completed',
|
||||
DriveTaskStatus.failed: 'failed',
|
||||
DriveTaskStatus.expired: 'expired',
|
||||
DriveTaskStatus.cancelled: 'cancelled',
|
||||
};
|
||||
@@ -60,3 +60,19 @@ sealed class SnCloudFile with _$SnCloudFile {
|
||||
factory SnCloudFile.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnCloudFileFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnCloudFileIndex with _$SnCloudFileIndex {
|
||||
const factory SnCloudFileIndex({
|
||||
required String id,
|
||||
required String path,
|
||||
required String fileId,
|
||||
required SnCloudFile file,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
}) = _SnCloudFileIndex;
|
||||
|
||||
factory SnCloudFileIndex.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnCloudFileIndexFromJson(json);
|
||||
}
|
||||
|
||||
@@ -622,4 +622,297 @@ $SnFilePoolCopyWith<$Res>? get pool {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnCloudFileIndex {
|
||||
|
||||
String get id; String get path; String get fileId; SnCloudFile get file; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnCloudFileIndex
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCloudFileIndexCopyWith<SnCloudFileIndex> get copyWith => _$SnCloudFileIndexCopyWithImpl<SnCloudFileIndex>(this as SnCloudFileIndex, _$identity);
|
||||
|
||||
/// Serializes this SnCloudFileIndex to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFileIndex&&(identical(other.id, id) || other.id == id)&&(identical(other.path, path) || other.path == path)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,path,fileId,file,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCloudFileIndex(id: $id, path: $path, fileId: $fileId, file: $file, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnCloudFileIndexCopyWith<$Res> {
|
||||
factory $SnCloudFileIndexCopyWith(SnCloudFileIndex value, $Res Function(SnCloudFileIndex) _then) = _$SnCloudFileIndexCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String path, String fileId, SnCloudFile file, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
$SnCloudFileCopyWith<$Res> get file;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnCloudFileIndexCopyWithImpl<$Res>
|
||||
implements $SnCloudFileIndexCopyWith<$Res> {
|
||||
_$SnCloudFileIndexCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnCloudFileIndex _self;
|
||||
final $Res Function(SnCloudFileIndex) _then;
|
||||
|
||||
/// Create a copy of SnCloudFileIndex
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? path = null,Object? fileId = null,Object? file = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,path: null == path ? _self.path : path // ignore: cast_nullable_to_non_nullable
|
||||
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
|
||||
as String,file: null == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of SnCloudFileIndex
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCloudFileCopyWith<$Res> get file {
|
||||
|
||||
return $SnCloudFileCopyWith<$Res>(_self.file, (value) {
|
||||
return _then(_self.copyWith(file: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnCloudFileIndex].
|
||||
extension SnCloudFileIndexPatterns on SnCloudFileIndex {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnCloudFileIndex value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFileIndex() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnCloudFileIndex value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFileIndex():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnCloudFileIndex value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFileIndex() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String path, String fileId, SnCloudFile file, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFileIndex() when $default != null:
|
||||
return $default(_that.id,_that.path,_that.fileId,_that.file,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String path, String fileId, SnCloudFile file, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFileIndex():
|
||||
return $default(_that.id,_that.path,_that.fileId,_that.file,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String path, String fileId, SnCloudFile file, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFileIndex() when $default != null:
|
||||
return $default(_that.id,_that.path,_that.fileId,_that.file,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnCloudFileIndex implements SnCloudFileIndex {
|
||||
const _SnCloudFileIndex({required this.id, required this.path, required this.fileId, required this.file, required this.createdAt, required this.updatedAt, required this.deletedAt});
|
||||
factory _SnCloudFileIndex.fromJson(Map<String, dynamic> json) => _$SnCloudFileIndexFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String path;
|
||||
@override final String fileId;
|
||||
@override final SnCloudFile file;
|
||||
@override final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
@override final DateTime? deletedAt;
|
||||
|
||||
/// Create a copy of SnCloudFileIndex
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnCloudFileIndexCopyWith<_SnCloudFileIndex> get copyWith => __$SnCloudFileIndexCopyWithImpl<_SnCloudFileIndex>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnCloudFileIndexToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFileIndex&&(identical(other.id, id) || other.id == id)&&(identical(other.path, path) || other.path == path)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,path,fileId,file,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCloudFileIndex(id: $id, path: $path, fileId: $fileId, file: $file, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnCloudFileIndexCopyWith<$Res> implements $SnCloudFileIndexCopyWith<$Res> {
|
||||
factory _$SnCloudFileIndexCopyWith(_SnCloudFileIndex value, $Res Function(_SnCloudFileIndex) _then) = __$SnCloudFileIndexCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String path, String fileId, SnCloudFile file, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@override $SnCloudFileCopyWith<$Res> get file;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnCloudFileIndexCopyWithImpl<$Res>
|
||||
implements _$SnCloudFileIndexCopyWith<$Res> {
|
||||
__$SnCloudFileIndexCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnCloudFileIndex _self;
|
||||
final $Res Function(_SnCloudFileIndex) _then;
|
||||
|
||||
/// Create a copy of SnCloudFileIndex
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? path = null,Object? fileId = null,Object? file = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnCloudFileIndex(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,path: null == path ? _self.path : path // ignore: cast_nullable_to_non_nullable
|
||||
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
|
||||
as String,file: null == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnCloudFileIndex
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCloudFileCopyWith<$Res> get file {
|
||||
|
||||
return $SnCloudFileCopyWith<$Res>(_self.file, (value) {
|
||||
return _then(_self.copyWith(file: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
||||
@@ -78,3 +78,28 @@ Map<String, dynamic> _$SnCloudFileToJson(_SnCloudFile instance) =>
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_SnCloudFileIndex _$SnCloudFileIndexFromJson(Map<String, dynamic> json) =>
|
||||
_SnCloudFileIndex(
|
||||
id: json['id'] as String,
|
||||
path: json['path'] as String,
|
||||
fileId: json['file_id'] as String,
|
||||
file: SnCloudFile.fromJson(json['file'] as Map<String, dynamic>),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt:
|
||||
json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnCloudFileIndexToJson(_SnCloudFileIndex instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'path': instance.path,
|
||||
'file_id': instance.fileId,
|
||||
'file': instance.file.toJson(),
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
12
lib/models/file_list_item.dart
Normal file
12
lib/models/file_list_item.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
|
||||
part 'file_list_item.freezed.dart';
|
||||
|
||||
@freezed
|
||||
sealed class FileListItem with _$FileListItem {
|
||||
const factory FileListItem.file(SnCloudFileIndex fileIndex) = FileItem;
|
||||
const factory FileListItem.folder(String folderName) = FolderItem;
|
||||
const factory FileListItem.unindexedFile(SnCloudFile file) =
|
||||
UnindexedFileItem;
|
||||
}
|
||||
396
lib/models/file_list_item.freezed.dart
Normal file
396
lib/models/file_list_item.freezed.dart
Normal file
@@ -0,0 +1,396 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'file_list_item.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$FileListItem {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is FileListItem);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FileListItem()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class $FileListItemCopyWith<$Res> {
|
||||
$FileListItemCopyWith(FileListItem _, $Res Function(FileListItem) __);
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [FileListItem].
|
||||
extension FileListItemPatterns on FileListItem {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( FileItem value)? file,TResult Function( FolderItem value)? folder,TResult Function( UnindexedFileItem value)? unindexedFile,required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case FileItem() when file != null:
|
||||
return file(_that);case FolderItem() when folder != null:
|
||||
return folder(_that);case UnindexedFileItem() when unindexedFile != null:
|
||||
return unindexedFile(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( FileItem value) file,required TResult Function( FolderItem value) folder,required TResult Function( UnindexedFileItem value) unindexedFile,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case FileItem():
|
||||
return file(_that);case FolderItem():
|
||||
return folder(_that);case UnindexedFileItem():
|
||||
return unindexedFile(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( FileItem value)? file,TResult? Function( FolderItem value)? folder,TResult? Function( UnindexedFileItem value)? unindexedFile,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case FileItem() when file != null:
|
||||
return file(_that);case FolderItem() when folder != null:
|
||||
return folder(_that);case UnindexedFileItem() when unindexedFile != null:
|
||||
return unindexedFile(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( SnCloudFileIndex fileIndex)? file,TResult Function( String folderName)? folder,TResult Function( SnCloudFile file)? unindexedFile,required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case FileItem() when file != null:
|
||||
return file(_that.fileIndex);case FolderItem() when folder != null:
|
||||
return folder(_that.folderName);case UnindexedFileItem() when unindexedFile != null:
|
||||
return unindexedFile(_that.file);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( SnCloudFileIndex fileIndex) file,required TResult Function( String folderName) folder,required TResult Function( SnCloudFile file) unindexedFile,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case FileItem():
|
||||
return file(_that.fileIndex);case FolderItem():
|
||||
return folder(_that.folderName);case UnindexedFileItem():
|
||||
return unindexedFile(_that.file);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( SnCloudFileIndex fileIndex)? file,TResult? Function( String folderName)? folder,TResult? Function( SnCloudFile file)? unindexedFile,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case FileItem() when file != null:
|
||||
return file(_that.fileIndex);case FolderItem() when folder != null:
|
||||
return folder(_that.folderName);case UnindexedFileItem() when unindexedFile != null:
|
||||
return unindexedFile(_that.file);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class FileItem implements FileListItem {
|
||||
const FileItem(this.fileIndex);
|
||||
|
||||
|
||||
final SnCloudFileIndex fileIndex;
|
||||
|
||||
/// Create a copy of FileListItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$FileItemCopyWith<FileItem> get copyWith => _$FileItemCopyWithImpl<FileItem>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is FileItem&&(identical(other.fileIndex, fileIndex) || other.fileIndex == fileIndex));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,fileIndex);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FileListItem.file(fileIndex: $fileIndex)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $FileItemCopyWith<$Res> implements $FileListItemCopyWith<$Res> {
|
||||
factory $FileItemCopyWith(FileItem value, $Res Function(FileItem) _then) = _$FileItemCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
SnCloudFileIndex fileIndex
|
||||
});
|
||||
|
||||
|
||||
$SnCloudFileIndexCopyWith<$Res> get fileIndex;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$FileItemCopyWithImpl<$Res>
|
||||
implements $FileItemCopyWith<$Res> {
|
||||
_$FileItemCopyWithImpl(this._self, this._then);
|
||||
|
||||
final FileItem _self;
|
||||
final $Res Function(FileItem) _then;
|
||||
|
||||
/// Create a copy of FileListItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? fileIndex = null,}) {
|
||||
return _then(FileItem(
|
||||
null == fileIndex ? _self.fileIndex : fileIndex // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFileIndex,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of FileListItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCloudFileIndexCopyWith<$Res> get fileIndex {
|
||||
|
||||
return $SnCloudFileIndexCopyWith<$Res>(_self.fileIndex, (value) {
|
||||
return _then(_self.copyWith(fileIndex: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class FolderItem implements FileListItem {
|
||||
const FolderItem(this.folderName);
|
||||
|
||||
|
||||
final String folderName;
|
||||
|
||||
/// Create a copy of FileListItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$FolderItemCopyWith<FolderItem> get copyWith => _$FolderItemCopyWithImpl<FolderItem>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is FolderItem&&(identical(other.folderName, folderName) || other.folderName == folderName));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,folderName);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FileListItem.folder(folderName: $folderName)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $FolderItemCopyWith<$Res> implements $FileListItemCopyWith<$Res> {
|
||||
factory $FolderItemCopyWith(FolderItem value, $Res Function(FolderItem) _then) = _$FolderItemCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String folderName
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$FolderItemCopyWithImpl<$Res>
|
||||
implements $FolderItemCopyWith<$Res> {
|
||||
_$FolderItemCopyWithImpl(this._self, this._then);
|
||||
|
||||
final FolderItem _self;
|
||||
final $Res Function(FolderItem) _then;
|
||||
|
||||
/// Create a copy of FileListItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? folderName = null,}) {
|
||||
return _then(FolderItem(
|
||||
null == folderName ? _self.folderName : folderName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class UnindexedFileItem implements FileListItem {
|
||||
const UnindexedFileItem(this.file);
|
||||
|
||||
|
||||
final SnCloudFile file;
|
||||
|
||||
/// Create a copy of FileListItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$UnindexedFileItemCopyWith<UnindexedFileItem> get copyWith => _$UnindexedFileItemCopyWithImpl<UnindexedFileItem>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is UnindexedFileItem&&(identical(other.file, file) || other.file == file));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,file);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FileListItem.unindexedFile(file: $file)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $UnindexedFileItemCopyWith<$Res> implements $FileListItemCopyWith<$Res> {
|
||||
factory $UnindexedFileItemCopyWith(UnindexedFileItem value, $Res Function(UnindexedFileItem) _then) = _$UnindexedFileItemCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
SnCloudFile file
|
||||
});
|
||||
|
||||
|
||||
$SnCloudFileCopyWith<$Res> get file;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$UnindexedFileItemCopyWithImpl<$Res>
|
||||
implements $UnindexedFileItemCopyWith<$Res> {
|
||||
_$UnindexedFileItemCopyWithImpl(this._self, this._then);
|
||||
|
||||
final UnindexedFileItem _self;
|
||||
final $Res Function(UnindexedFileItem) _then;
|
||||
|
||||
/// Create a copy of FileListItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? file = null,}) {
|
||||
return _then(UnindexedFileItem(
|
||||
null == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of FileListItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCloudFileCopyWith<$Res> get file {
|
||||
|
||||
return $SnCloudFileCopyWith<$Res>(_self.file, (value) {
|
||||
return _then(_self.copyWith(file: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
19
lib/models/folder.dart
Normal file
19
lib/models/folder.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'folder.freezed.dart';
|
||||
part 'folder.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class SnCloudFolder with _$SnCloudFolder {
|
||||
const factory SnCloudFolder({
|
||||
required String id,
|
||||
required String name,
|
||||
required String? parentFolderId,
|
||||
required String accountId,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
}) = _SnCloudFolder;
|
||||
|
||||
factory SnCloudFolder.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnCloudFolderFromJson(json);
|
||||
}
|
||||
286
lib/models/folder.freezed.dart
Normal file
286
lib/models/folder.freezed.dart
Normal file
@@ -0,0 +1,286 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'folder.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnCloudFolder {
|
||||
|
||||
String get id; String get name; String? get parentFolderId; String get accountId; DateTime get createdAt; DateTime get updatedAt;
|
||||
/// Create a copy of SnCloudFolder
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCloudFolderCopyWith<SnCloudFolder> get copyWith => _$SnCloudFolderCopyWithImpl<SnCloudFolder>(this as SnCloudFolder, _$identity);
|
||||
|
||||
/// Serializes this SnCloudFolder to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFolder&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.parentFolderId, parentFolderId) || other.parentFolderId == parentFolderId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,parentFolderId,accountId,createdAt,updatedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCloudFolder(id: $id, name: $name, parentFolderId: $parentFolderId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnCloudFolderCopyWith<$Res> {
|
||||
factory $SnCloudFolderCopyWith(SnCloudFolder value, $Res Function(SnCloudFolder) _then) = _$SnCloudFolderCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String name, String? parentFolderId, String accountId, DateTime createdAt, DateTime updatedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnCloudFolderCopyWithImpl<$Res>
|
||||
implements $SnCloudFolderCopyWith<$Res> {
|
||||
_$SnCloudFolderCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnCloudFolder _self;
|
||||
final $Res Function(SnCloudFolder) _then;
|
||||
|
||||
/// Create a copy of SnCloudFolder
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? parentFolderId = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,parentFolderId: freezed == parentFolderId ? _self.parentFolderId : parentFolderId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnCloudFolder].
|
||||
extension SnCloudFolderPatterns on SnCloudFolder {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnCloudFolder value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFolder() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnCloudFolder value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFolder():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnCloudFolder value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFolder() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String? parentFolderId, String accountId, DateTime createdAt, DateTime updatedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFolder() when $default != null:
|
||||
return $default(_that.id,_that.name,_that.parentFolderId,_that.accountId,_that.createdAt,_that.updatedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String? parentFolderId, String accountId, DateTime createdAt, DateTime updatedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFolder():
|
||||
return $default(_that.id,_that.name,_that.parentFolderId,_that.accountId,_that.createdAt,_that.updatedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String? parentFolderId, String accountId, DateTime createdAt, DateTime updatedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFolder() when $default != null:
|
||||
return $default(_that.id,_that.name,_that.parentFolderId,_that.accountId,_that.createdAt,_that.updatedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnCloudFolder implements SnCloudFolder {
|
||||
const _SnCloudFolder({required this.id, required this.name, required this.parentFolderId, required this.accountId, required this.createdAt, required this.updatedAt});
|
||||
factory _SnCloudFolder.fromJson(Map<String, dynamic> json) => _$SnCloudFolderFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String name;
|
||||
@override final String? parentFolderId;
|
||||
@override final String accountId;
|
||||
@override final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
|
||||
/// Create a copy of SnCloudFolder
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnCloudFolderCopyWith<_SnCloudFolder> get copyWith => __$SnCloudFolderCopyWithImpl<_SnCloudFolder>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnCloudFolderToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFolder&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.parentFolderId, parentFolderId) || other.parentFolderId == parentFolderId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,parentFolderId,accountId,createdAt,updatedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCloudFolder(id: $id, name: $name, parentFolderId: $parentFolderId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnCloudFolderCopyWith<$Res> implements $SnCloudFolderCopyWith<$Res> {
|
||||
factory _$SnCloudFolderCopyWith(_SnCloudFolder value, $Res Function(_SnCloudFolder) _then) = __$SnCloudFolderCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String name, String? parentFolderId, String accountId, DateTime createdAt, DateTime updatedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnCloudFolderCopyWithImpl<$Res>
|
||||
implements _$SnCloudFolderCopyWith<$Res> {
|
||||
__$SnCloudFolderCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnCloudFolder _self;
|
||||
final $Res Function(_SnCloudFolder) _then;
|
||||
|
||||
/// Create a copy of SnCloudFolder
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? parentFolderId = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,}) {
|
||||
return _then(_SnCloudFolder(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,parentFolderId: freezed == parentFolderId ? _self.parentFolderId : parentFolderId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
27
lib/models/folder.g.dart
Normal file
27
lib/models/folder.g.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'folder.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SnCloudFolder _$SnCloudFolderFromJson(Map<String, dynamic> json) =>
|
||||
_SnCloudFolder(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
parentFolderId: json['parent_folder_id'] as String?,
|
||||
accountId: json['account_id'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnCloudFolderToJson(_SnCloudFolder instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'parent_folder_id': instance.parentFolderId,
|
||||
'account_id': instance.accountId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
};
|
||||
@@ -120,9 +120,11 @@ class ActivityRpcServer {
|
||||
};
|
||||
|
||||
// Set up IPC close handler
|
||||
_ipcServer!.onSocketClose = (socket) {
|
||||
handlers['close']?.call(socket);
|
||||
};
|
||||
if (!kIsWeb) {
|
||||
(_ipcServer as dynamic).onSocketClose = (socket) {
|
||||
handlers['close']?.call(socket);
|
||||
};
|
||||
}
|
||||
|
||||
await _ipcServer!.start();
|
||||
} catch (e) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import "package:dio/dio.dart";
|
||||
import "package:drift/drift.dart" show Variable;
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:island/database/drift_db.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
@@ -28,7 +29,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
late final SnChatMember _identity;
|
||||
|
||||
final Map<String, LocalChatMessage> _pendingMessages = {};
|
||||
final Map<String, Map<int, double>> _fileUploadProgress = {};
|
||||
final Map<String, Map<int, double?>> _fileUploadProgress = {};
|
||||
int? _totalCount;
|
||||
String? _searchQuery;
|
||||
bool? _withLinks;
|
||||
@@ -433,12 +434,13 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
}
|
||||
|
||||
Future<void> sendMessage(
|
||||
WidgetRef ref,
|
||||
String content,
|
||||
List<UniversalFile> attachments, {
|
||||
SnChatMessage? editingTo,
|
||||
SnChatMessage? forwardingTo,
|
||||
SnChatMessage? replyingTo,
|
||||
Function(String, Map<int, double>)? onProgress,
|
||||
Function(String, Map<int, double?>)? onProgress,
|
||||
}) async {
|
||||
final nonce = const Uuid().v4();
|
||||
talker.log('Sending message with nonce $nonce');
|
||||
@@ -471,10 +473,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
for (var idx = 0; idx < attachments.length; idx++) {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
ref: ref,
|
||||
fileData: attachments[idx],
|
||||
client: ref.read(apiClientProvider),
|
||||
onProgress: (progress, _) {
|
||||
_fileUploadProgress[localMessage.id]?[idx] = progress;
|
||||
_fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0;
|
||||
onProgress?.call(
|
||||
localMessage.id,
|
||||
_fileUploadProgress[localMessage.id] ?? {},
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$messagesNotifierHash() => r'6adefd9152cdd686c2a863964993f24c42d405b5';
|
||||
String _$messagesNotifierHash() => r'c009eb8598e8b5fbcece2d0b5213b2e434edb3b2';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
104
lib/pods/file_list.dart
Normal file
104
lib/pods/file_list.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/file_list_item.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
|
||||
part 'file_list.g.dart';
|
||||
|
||||
@riverpod
|
||||
class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||
with CursorPagingNotifierMixin<FileListItem> {
|
||||
String _currentPath = '/';
|
||||
|
||||
void setPath(String path) {
|
||||
_currentPath = path;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<FileListItem>> fetch({
|
||||
required String? cursor,
|
||||
}) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/index/browse',
|
||||
queryParameters: {'path': _currentPath},
|
||||
);
|
||||
|
||||
final List<String> folders =
|
||||
(response.data['folders'] as List).map((e) => e as String).toList();
|
||||
final List<SnCloudFileIndex> files =
|
||||
(response.data['files'] as List)
|
||||
.map((e) => SnCloudFileIndex.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final List<FileListItem> items = [
|
||||
...folders.map((folderName) => FileListItem.folder(folderName)),
|
||||
...files.map((file) => FileListItem.file(file)),
|
||||
];
|
||||
|
||||
// The new API returns all files in the path, no pagination
|
||||
return CursorPagingData(items: items, hasMore: false, nextCursor: null);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/drive/billing/usage');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
||||
with CursorPagingNotifierMixin<FileListItem> {
|
||||
@override
|
||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<FileListItem>> fetch({
|
||||
required String? cursor,
|
||||
}) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
|
||||
const take = 50; // Default page size
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/index/unindexed',
|
||||
queryParameters: {'take': take.toString(), 'offset': offset.toString()},
|
||||
);
|
||||
|
||||
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
||||
|
||||
final List<SnCloudFile> files =
|
||||
(response.data as List)
|
||||
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final List<FileListItem> items =
|
||||
files.map((file) => FileListItem.unindexedFile(file)).toList();
|
||||
|
||||
final hasMore = offset + take < total;
|
||||
final nextCursor = hasMore ? (offset + take).toString() : null;
|
||||
|
||||
return CursorPagingData(
|
||||
items: items,
|
||||
hasMore: hasMore,
|
||||
nextCursor: nextCursor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<Map<String, dynamic>?> billingQuota(Ref ref) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/drive/billing/quota');
|
||||
return response.data;
|
||||
}
|
||||
@@ -45,13 +45,13 @@ final billingQuotaProvider =
|
||||
// ignore: unused_element
|
||||
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||
String _$cloudFileListNotifierHash() =>
|
||||
r'22c45a8ea23147a3835ba870ad2f0bb833f853ea';
|
||||
r'5f2f80357cb31ac6473df5ac2101f9a462004f81';
|
||||
|
||||
/// See also [CloudFileListNotifier].
|
||||
@ProviderFor(CloudFileListNotifier)
|
||||
final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
||||
CloudFileListNotifier,
|
||||
CursorPagingData<SnCloudFile>
|
||||
CursorPagingData<FileListItem>
|
||||
>.internal(
|
||||
CloudFileListNotifier.new,
|
||||
name: r'cloudFileListNotifierProvider',
|
||||
@@ -64,6 +64,27 @@ final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
||||
);
|
||||
|
||||
typedef _$CloudFileListNotifier =
|
||||
AutoDisposeAsyncNotifier<CursorPagingData<SnCloudFile>>;
|
||||
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
||||
String _$unindexedFileListNotifierHash() =>
|
||||
r'48fc92432a50a562190da5fe8ed0920d171b07b6';
|
||||
|
||||
/// See also [UnindexedFileListNotifier].
|
||||
@ProviderFor(UnindexedFileListNotifier)
|
||||
final unindexedFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
||||
UnindexedFileListNotifier,
|
||||
CursorPagingData<FileListItem>
|
||||
>.internal(
|
||||
UnindexedFileListNotifier.new,
|
||||
name: r'unindexedFileListNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$unindexedFileListNotifierHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$UnindexedFileListNotifier =
|
||||
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
494
lib/pods/upload_tasks.dart
Normal file
494
lib/pods/upload_tasks.dart
Normal file
@@ -0,0 +1,494 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/drive_task.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/talker.dart';
|
||||
|
||||
final uploadTasksProvider =
|
||||
StateNotifierProvider<UploadTasksNotifier, List<DriveTask>>(
|
||||
(ref) => UploadTasksNotifier(ref),
|
||||
);
|
||||
|
||||
class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
||||
final Ref ref;
|
||||
StreamSubscription? _websocketSubscription;
|
||||
final Map<String, Map<String, dynamic>> _pendingUploads = {};
|
||||
|
||||
UploadTasksNotifier(this.ref) : super([]) {
|
||||
_listenToWebSocket();
|
||||
}
|
||||
|
||||
void _listenToWebSocket() {
|
||||
final WebSocketService websocketService = ref.read(websocketProvider);
|
||||
_websocketSubscription = websocketService.dataStream.listen(
|
||||
_handleWebSocketPacket,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleWebSocketPacket(dynamic packet) {
|
||||
if (packet.type.startsWith('task.')) {
|
||||
final data = packet.data;
|
||||
if (data == null) return;
|
||||
|
||||
// Debug logging
|
||||
talker.info(
|
||||
'[UploadTasks] Received WebSocket packet: ${packet.type}, data: $data',
|
||||
);
|
||||
|
||||
final taskId = data['task_id'] as String?;
|
||||
if (taskId == null) return;
|
||||
|
||||
switch (packet.type) {
|
||||
case 'task.created':
|
||||
_handleTaskCreated(taskId, data);
|
||||
break;
|
||||
case 'task.progress':
|
||||
_handleProgressUpdate(taskId, data);
|
||||
break;
|
||||
case 'task.completed':
|
||||
_handleUploadCompleted(taskId, data);
|
||||
break;
|
||||
case 'task.failed':
|
||||
_handleUploadFailed(taskId, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTaskCreated(String taskId, Map<String, dynamic> data) {
|
||||
talker.info('[UploadTasks] Handling task.created for taskId: $taskId');
|
||||
|
||||
// Check if task already exists (might have been created locally)
|
||||
final existingTask =
|
||||
state.where((task) => task.taskId == taskId).firstOrNull;
|
||||
if (existingTask != null) {
|
||||
talker.info('[UploadTasks] Task already exists, updating status');
|
||||
// Task already exists, just update its status to confirm server creation
|
||||
state =
|
||||
state.map((task) {
|
||||
if (task.taskId == taskId) {
|
||||
return task.copyWith(
|
||||
status: DriveTaskStatus.pending,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return task;
|
||||
}).toList();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have stored metadata for this task
|
||||
final metadata = _pendingUploads[taskId];
|
||||
talker.info('[UploadTasks] Metadata for taskId $taskId: $metadata');
|
||||
|
||||
if (metadata != null) {
|
||||
talker.info('[UploadTasks] Creating task with full metadata');
|
||||
// Create task with full metadata
|
||||
final uploadTask = DriveTask(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
taskId: taskId,
|
||||
fileName: metadata['file_name'] as String,
|
||||
contentType: metadata['mime_type'] as String,
|
||||
fileSize: metadata['file_size'] as int,
|
||||
uploadedBytes: 0,
|
||||
totalChunks: metadata['total_chunks'] as int,
|
||||
uploadedChunks: 0,
|
||||
status: DriveTaskStatus.pending,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
type: 'FileUpload',
|
||||
poolId: metadata['pool_id'] as String?,
|
||||
bundleId: metadata['bundleId'] as String?,
|
||||
encryptPassword: metadata['encrypt_password'] as String?,
|
||||
expiredAt: metadata['expired_at'] as String?,
|
||||
);
|
||||
|
||||
state = [...state, uploadTask];
|
||||
talker.info(
|
||||
'[UploadTasks] Task created successfully. Total tasks: ${state.length}',
|
||||
);
|
||||
// Clean up stored metadata
|
||||
_pendingUploads.remove(taskId);
|
||||
} else {
|
||||
talker.info('[UploadTasks] No metadata found, creating minimal task');
|
||||
// Create minimal task if no metadata is stored
|
||||
final params = data['parameters'];
|
||||
final uploadTask = DriveTask(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
taskId: taskId,
|
||||
fileName: params['file_name'] as String? ?? 'Unknown file',
|
||||
contentType: params['content_type'],
|
||||
fileSize: params['file_size'],
|
||||
uploadedBytes:
|
||||
(params['chunk_size'] as int) * (params['chunks_uploaded'] as int),
|
||||
totalChunks: params['chunks_count'],
|
||||
uploadedChunks: params['chunks_uploaded'],
|
||||
status: DriveTaskStatus.pending,
|
||||
createdAt: DateTime.tryParse(data['created_at']) ?? DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
type: data['type'],
|
||||
);
|
||||
|
||||
state = [...state, uploadTask];
|
||||
talker.info(
|
||||
'[UploadTasks] Minimal task created. Total tasks: ${state.length}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleProgressUpdate(String taskId, Map<String, dynamic> data) {
|
||||
final progress = data['progress'] as num? ?? 0.0;
|
||||
|
||||
state =
|
||||
state.map((task) {
|
||||
if (task.taskId == taskId) {
|
||||
final uploadedBytes = (progress / 100.0 * task.fileSize).toInt();
|
||||
return task.copyWith(
|
||||
statusMessage: data['status'],
|
||||
uploadedBytes: uploadedBytes,
|
||||
status: DriveTaskStatus.inProgress,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return task;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void _handleUploadCompleted(String taskId, Map<String, dynamic> data) {
|
||||
final results = data['results'] as Map<String, dynamic>?;
|
||||
|
||||
state =
|
||||
state.map((task) {
|
||||
if (task.taskId == taskId) {
|
||||
return task.copyWith(
|
||||
status: DriveTaskStatus.completed,
|
||||
uploadedChunks: task.totalChunks,
|
||||
uploadedBytes: task.fileSize,
|
||||
// Update file information from Results if available
|
||||
fileName: results?['file_name'] as String? ?? task.fileName,
|
||||
fileSize: results?['file_size'] as int? ?? task.fileSize,
|
||||
contentType: results?['mime_type'] as String? ?? task.contentType,
|
||||
result:
|
||||
results?['file_info'] != null
|
||||
? SnCloudFile.fromJson(results!['file_info'])
|
||||
: null,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return task;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void _handleUploadFailed(String taskId, Map<String, dynamic> data) {
|
||||
final errorMessage = data['error_message'] as String? ?? 'Upload failed';
|
||||
|
||||
state =
|
||||
state.map((task) {
|
||||
if (task.taskId == taskId) {
|
||||
return task.copyWith(
|
||||
status: DriveTaskStatus.failed,
|
||||
errorMessage: errorMessage,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return task;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void addUploadTask(DriveTask task) {
|
||||
state = [...state, task];
|
||||
}
|
||||
|
||||
void storeUploadMetadata(
|
||||
String taskId, {
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
required int fileSize,
|
||||
required int totalChunks,
|
||||
String? poolId,
|
||||
String? bundleId,
|
||||
String? encryptPassword,
|
||||
String? expiredAt,
|
||||
}) {
|
||||
_pendingUploads[taskId] = {
|
||||
'file_name': fileName,
|
||||
'mime_type': contentType,
|
||||
'file_size': fileSize,
|
||||
'total_chunks': totalChunks,
|
||||
'pool_id': poolId,
|
||||
'bundleId': bundleId,
|
||||
'encrypt_password': encryptPassword,
|
||||
'expired_at': expiredAt,
|
||||
};
|
||||
}
|
||||
|
||||
void updateTaskStatus(
|
||||
String taskId,
|
||||
DriveTaskStatus status, {
|
||||
String? errorMessage,
|
||||
}) {
|
||||
state =
|
||||
state.map((task) {
|
||||
if (task.taskId == taskId) {
|
||||
return task.copyWith(
|
||||
status: status,
|
||||
errorMessage: errorMessage,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return task;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void updateTransmissionProgress(String taskId, double progress) {
|
||||
state =
|
||||
state.map((task) {
|
||||
if (task.taskId == taskId) {
|
||||
return task.copyWith(
|
||||
transmissionProgress: progress,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return task;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void removeTask(String taskId) {
|
||||
state = state.where((task) => task.taskId != taskId).toList();
|
||||
}
|
||||
|
||||
void clearCompletedTasks() {
|
||||
state =
|
||||
state
|
||||
.where(
|
||||
(task) =>
|
||||
task.status != DriveTaskStatus.completed &&
|
||||
task.status != DriveTaskStatus.failed &&
|
||||
task.status != DriveTaskStatus.cancelled &&
|
||||
task.status != DriveTaskStatus.expired,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
DriveTask? getTask(String taskId) {
|
||||
return state.where((task) => task.taskId == taskId).firstOrNull;
|
||||
}
|
||||
|
||||
List<DriveTask> getActiveTasks() {
|
||||
return state
|
||||
.where(
|
||||
(task) =>
|
||||
task.status == DriveTaskStatus.pending ||
|
||||
task.status == DriveTaskStatus.inProgress ||
|
||||
task.status == DriveTaskStatus.paused ||
|
||||
task.status == DriveTaskStatus.completed,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_websocketSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Provider for the enhanced FileUploader that integrates with upload tasks
|
||||
final enhancedFileUploaderProvider = Provider<EnhancedFileUploader>((ref) {
|
||||
final dio = ref.watch(apiClientProvider);
|
||||
return EnhancedFileUploader(dio, ref);
|
||||
});
|
||||
|
||||
class EnhancedFileUploader extends FileUploader {
|
||||
final Ref ref;
|
||||
|
||||
EnhancedFileUploader(super.client, this.ref);
|
||||
|
||||
/// Reads the next chunk from a stream subscription.
|
||||
Future<Uint8List> _readNextChunkFromStream(
|
||||
StreamSubscription<List<int>> subscription,
|
||||
int size,
|
||||
) async {
|
||||
final completer = Completer<Uint8List>();
|
||||
final buffer = <int>[];
|
||||
int remaining = size;
|
||||
|
||||
void onData(List<int> data) {
|
||||
buffer.addAll(data);
|
||||
remaining -= data.length;
|
||||
if (remaining <= 0) {
|
||||
subscription.pause();
|
||||
completer.complete(Uint8List.fromList(buffer.sublist(0, size)));
|
||||
}
|
||||
}
|
||||
|
||||
void onDone() {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(Uint8List.fromList(buffer));
|
||||
}
|
||||
}
|
||||
|
||||
subscription.onData(onData);
|
||||
subscription.onDone(onDone);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SnCloudFile> uploadFile({
|
||||
required dynamic fileData,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
String? poolId,
|
||||
String? bundleId,
|
||||
String? encryptPassword,
|
||||
String? expiredAt,
|
||||
int? customChunkSize,
|
||||
String? path,
|
||||
Function(double? progress, Duration estimate)? onProgress,
|
||||
}) async {
|
||||
// Step 1: Create upload task
|
||||
onProgress?.call(null, Duration.zero);
|
||||
final createResponse = await createUploadTask(
|
||||
fileData: fileData,
|
||||
fileName: fileName,
|
||||
contentType: contentType,
|
||||
poolId: poolId,
|
||||
bundleId: bundleId,
|
||||
encryptPassword: encryptPassword,
|
||||
expiredAt: expiredAt,
|
||||
chunkSize: customChunkSize,
|
||||
path: path,
|
||||
);
|
||||
|
||||
int totalSize;
|
||||
if (fileData is XFile) {
|
||||
totalSize = await fileData.length();
|
||||
} else if (fileData is Uint8List) {
|
||||
totalSize = fileData.length;
|
||||
} else {
|
||||
throw ArgumentError('Invalid fileData type');
|
||||
}
|
||||
|
||||
if (createResponse['file_exists'] == true) {
|
||||
// File already exists, create a local task to show it was found
|
||||
final existingFile = SnCloudFile.fromJson(createResponse['file']);
|
||||
|
||||
// Create a task that shows as completed immediately
|
||||
// Use a generated taskId since the server might not provide one for existing files
|
||||
final taskId =
|
||||
createResponse['task_id'] as String? ??
|
||||
'existing-${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
final uploadTask = DriveTask(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
taskId: taskId,
|
||||
fileName: fileName,
|
||||
contentType: contentType,
|
||||
fileSize: totalSize,
|
||||
uploadedBytes: totalSize,
|
||||
totalChunks: 1, // For existing files, we consider it as 1 chunk
|
||||
uploadedChunks: 1,
|
||||
status: DriveTaskStatus.completed,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
type: 'FileUpload',
|
||||
poolId: poolId,
|
||||
bundleId: bundleId,
|
||||
encryptPassword: encryptPassword,
|
||||
expiredAt: expiredAt,
|
||||
);
|
||||
|
||||
ref.read(uploadTasksProvider.notifier).addUploadTask(uploadTask);
|
||||
|
||||
return existingFile;
|
||||
}
|
||||
|
||||
final taskId = createResponse['task_id'] as String;
|
||||
final chunkSize = createResponse['chunk_size'] as int;
|
||||
final chunksCount = createResponse['chunks_count'] as int;
|
||||
|
||||
// Store upload metadata for when task.created event arrives
|
||||
talker.info('[UploadTasks] Storing metadata for taskId: $taskId');
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.storeUploadMetadata(
|
||||
taskId,
|
||||
fileName: fileName,
|
||||
contentType: contentType,
|
||||
fileSize: totalSize,
|
||||
totalChunks: chunksCount,
|
||||
poolId: poolId,
|
||||
bundleId: bundleId,
|
||||
encryptPassword: encryptPassword,
|
||||
expiredAt: expiredAt,
|
||||
);
|
||||
|
||||
// Step 2: Upload chunks
|
||||
int bytesUploaded = 0;
|
||||
if (fileData is XFile) {
|
||||
// Use stream for XFile
|
||||
final subscription = fileData.openRead().listen(null);
|
||||
subscription.pause();
|
||||
for (int i = 0; i < chunksCount; i++) {
|
||||
subscription.resume();
|
||||
final chunkData = await _readNextChunkFromStream(
|
||||
subscription,
|
||||
chunkSize,
|
||||
);
|
||||
await uploadChunk(
|
||||
taskId: taskId,
|
||||
chunkIndex: i,
|
||||
chunkData: chunkData,
|
||||
onSendProgress: (sent, total) {
|
||||
final overallProgress = (bytesUploaded + sent) / totalSize;
|
||||
onProgress?.call(overallProgress, Duration.zero);
|
||||
// Update transmission progress in UI
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.updateTransmissionProgress(taskId, overallProgress);
|
||||
},
|
||||
);
|
||||
bytesUploaded += chunkData.length;
|
||||
}
|
||||
subscription.cancel();
|
||||
} else if (fileData is Uint8List) {
|
||||
// Use old way for Uint8List
|
||||
final chunks = <Uint8List>[];
|
||||
for (int i = 0; i < fileData.length; i += chunkSize) {
|
||||
final end =
|
||||
i + chunkSize > fileData.length ? fileData.length : i + chunkSize;
|
||||
chunks.add(Uint8List.fromList(fileData.sublist(i, end)));
|
||||
}
|
||||
|
||||
// Upload each chunk
|
||||
for (int i = 0; i < chunks.length; i++) {
|
||||
await uploadChunk(
|
||||
taskId: taskId,
|
||||
chunkIndex: i,
|
||||
chunkData: chunks[i],
|
||||
onSendProgress: (sent, total) {
|
||||
final overallProgress = (bytesUploaded + sent) / totalSize;
|
||||
onProgress?.call(overallProgress, Duration.zero);
|
||||
// Update transmission progress in UI
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.updateTransmissionProgress(taskId, overallProgress);
|
||||
},
|
||||
);
|
||||
bytesUploaded += chunks[i].length;
|
||||
}
|
||||
} else {
|
||||
throw ArgumentError('Invalid fileData type');
|
||||
}
|
||||
|
||||
// Step 3: Complete upload
|
||||
onProgress?.call(null, Duration.zero);
|
||||
return await completeUpload(taskId);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,9 @@ import 'package:island/screens/developers/hub.dart';
|
||||
import 'package:island/screens/developers/edit_project.dart';
|
||||
import 'package:island/screens/developers/new_project.dart';
|
||||
import 'package:island/screens/discovery/articles.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/screens/files/file_list.dart';
|
||||
import 'package:island/screens/files/file_detail.dart';
|
||||
import 'package:island/screens/posts/post_categories_list.dart';
|
||||
import 'package:island/screens/posts/post_category_detail.dart';
|
||||
import 'package:island/screens/posts/post_search.dart';
|
||||
@@ -42,7 +44,6 @@ import 'package:island/screens/stickers/pack_detail.dart';
|
||||
import 'package:island/screens/discovery/feeds/feed_marketplace.dart';
|
||||
import 'package:island/screens/discovery/feeds/feed_detail.dart';
|
||||
import 'package:island/screens/creators/poll/poll_list.dart';
|
||||
import 'package:island/screens/creators/publishers_form.dart';
|
||||
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
||||
import 'package:island/screens/poll/poll_editor.dart';
|
||||
import 'package:island/screens/posts/compose.dart';
|
||||
@@ -396,11 +397,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
path: '/account/wallet',
|
||||
builder: (context, state) => const WalletScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'files',
|
||||
path: '/account/files',
|
||||
builder: (context, state) => const FileListScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'relationships',
|
||||
path: '/account/relationships',
|
||||
@@ -445,6 +441,31 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
return AccountProfileScreen(name: name);
|
||||
},
|
||||
),
|
||||
|
||||
// Files tab
|
||||
GoRoute(
|
||||
name: 'files',
|
||||
path: '/files',
|
||||
builder: (context, state) => const FileListScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'fileDetail',
|
||||
path: ':id',
|
||||
builder: (context, state) {
|
||||
// For now, we'll need to pass the file object through extra
|
||||
// This will be updated when we modify the file list navigation
|
||||
final file = state.extra as SnCloudFile?;
|
||||
if (file != null) {
|
||||
return FileDetailScreen(item: file);
|
||||
}
|
||||
// Fallback - this shouldn't happen in normal flow
|
||||
Navigator.of(context).pop();
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Creator hub tab
|
||||
GoRoute(
|
||||
name: 'creatorHub',
|
||||
@@ -507,19 +528,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
return StickersScreen(pubName: name);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorNew',
|
||||
path: 'new',
|
||||
builder: (context, state) => const NewPublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorEdit',
|
||||
path: ':name/edit',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return EditPublisherScreen(name: name);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ class AccountScreen extends HookConsumerWidget {
|
||||
pathParameters: {'name': user.value!.name},
|
||||
);
|
||||
},
|
||||
),
|
||||
).padding(bottom: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -375,6 +375,16 @@ class AccountScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.files),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('files').tr(),
|
||||
onTap: () {
|
||||
context.goNamed('files');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.wallet),
|
||||
@@ -385,16 +395,6 @@ class AccountScreen extends HookConsumerWidget {
|
||||
context.pushNamed('wallet');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.files),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('files').tr(),
|
||||
onTap: () {
|
||||
context.pushNamed('files');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.people),
|
||||
|
||||
@@ -84,9 +84,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
|
||||
'accountPasswordChange'.tr(),
|
||||
);
|
||||
if (!confirm || !context.mounted) return;
|
||||
final captchaTk = await Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
|
||||
final captchaTk = await CaptchaScreen.show(context);
|
||||
if (captchaTk == null) return;
|
||||
try {
|
||||
if (context.mounted) showLoadingModal(context);
|
||||
|
||||
@@ -76,7 +76,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
try {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
client: ref.read(apiClientProvider),
|
||||
ref: ref,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
||||
@@ -30,6 +30,7 @@ Widget getProviderIcon(String provider, {double size = 24, Color? color}) {
|
||||
case 'github':
|
||||
case 'discord':
|
||||
case 'afdian':
|
||||
case 'steam':
|
||||
return SvgPicture.asset(
|
||||
'assets/images/oidc/$providerLower.svg',
|
||||
width: size,
|
||||
@@ -64,6 +65,8 @@ String getLocalizedProviderName(String provider) {
|
||||
return 'accountConnectionProviderAfdian'.tr();
|
||||
case 'spotify':
|
||||
return 'accountConnectionProviderSpotify'.tr();
|
||||
case 'steam':
|
||||
return 'accountConnectionProviderSteam'.tr();
|
||||
default:
|
||||
return provider;
|
||||
}
|
||||
@@ -164,6 +167,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
||||
'discord',
|
||||
'afdian',
|
||||
'spotify',
|
||||
'steam',
|
||||
];
|
||||
|
||||
Future<void> addConnection() async {
|
||||
@@ -199,12 +203,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
case 'microsoft':
|
||||
case 'google':
|
||||
case 'github':
|
||||
case 'discord':
|
||||
case 'afdian':
|
||||
case 'spotify':
|
||||
default:
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final accessToken = ref.watch(tokenProvider);
|
||||
launchUrlString(
|
||||
@@ -212,9 +211,6 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
||||
);
|
||||
if (context.mounted) Navigator.pop(context, true);
|
||||
break;
|
||||
default:
|
||||
showSnackBar('accountConnectionAddError'.tr());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,17 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:island/screens/auth/captcha.config.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
|
||||
class CaptchaScreen extends ConsumerWidget {
|
||||
static Future<String?> show(BuildContext context) {
|
||||
return showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => const CaptchaScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
const CaptchaScreen({super.key});
|
||||
|
||||
@override
|
||||
@@ -13,9 +21,9 @@ class CaptchaScreen extends ConsumerWidget {
|
||||
|
||||
if (!captchaUrl.hasValue) return Center(child: CircularProgressIndicator());
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: Text("Anti-Robot")),
|
||||
body: InAppWebView(
|
||||
return SheetScaffold(
|
||||
titleText: "Anti-Robot",
|
||||
child: InAppWebView(
|
||||
initialUrlRequest: URLRequest(
|
||||
url: WebUri('${captchaUrl.value}?redirect_uri=solian://captcha'),
|
||||
),
|
||||
|
||||
@@ -4,11 +4,19 @@ import 'dart:ui_web' as ui;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/screens/auth/captcha.config.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:web/web.dart' as web;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CaptchaScreen extends ConsumerStatefulWidget {
|
||||
static Future<String?> show(BuildContext context) {
|
||||
return showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => const CaptchaScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
const CaptchaScreen({super.key});
|
||||
|
||||
@override
|
||||
@@ -61,9 +69,9 @@ class _CaptchaScreenState extends ConsumerState<CaptchaScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: Text("Anti-Robot")),
|
||||
body:
|
||||
return SheetScaffold(
|
||||
titleText: "Anti-Robot",
|
||||
child:
|
||||
_isInitialized
|
||||
? HtmlElementView(viewType: 'captcha-iframe')
|
||||
: Center(child: CircularProgressIndicator()),
|
||||
|
||||
@@ -38,9 +38,7 @@ class CreateAccountScreen extends HookConsumerWidget {
|
||||
void performAction() async {
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
|
||||
final captchaTk = await Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
|
||||
final captchaTk = await CaptchaScreen.show(context);
|
||||
if (captchaTk == null) return;
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
@@ -523,9 +523,7 @@ class _LoginLookupScreen extends HookConsumerWidget {
|
||||
showErrorAlert('loginResetPasswordHint'.tr());
|
||||
return;
|
||||
}
|
||||
final captchaTk = await Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
|
||||
final captchaTk = await CaptchaScreen.show(context);
|
||||
if (captchaTk == null) return;
|
||||
isBusy.value = true;
|
||||
try {
|
||||
|
||||
@@ -99,7 +99,7 @@ class EditChatScreen extends HookConsumerWidget {
|
||||
try {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
client: ref.read(apiClientProvider),
|
||||
ref: ref,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
||||
@@ -2,6 +2,7 @@ import "dart:async";
|
||||
import "dart:math" as math;
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:file_picker/file_picker.dart";
|
||||
import "package:image_picker/image_picker.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:go_router/go_router.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
@@ -148,7 +149,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
final messageForwardingTo = useState<SnChatMessage?>(null);
|
||||
final messageEditingTo = useState<SnChatMessage?>(null);
|
||||
final attachments = useState<List<UniversalFile>>([]);
|
||||
final attachmentProgress = useState<Map<String, Map<int, double>>>({});
|
||||
final attachmentProgress = useState<Map<String, Map<int, double?>>>({});
|
||||
|
||||
// Selection mode state
|
||||
final isSelectionMode = useState<bool>(false);
|
||||
@@ -181,16 +182,13 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
}, [scrollController]);
|
||||
|
||||
Future<void> pickPhotoMedia() async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.image,
|
||||
allowMultiple: true,
|
||||
allowCompression: false,
|
||||
);
|
||||
if (result == null || result.count == 0) return;
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final List<XFile> results = await picker.pickMultiImage();
|
||||
if (results.isEmpty) return;
|
||||
attachments.value = [
|
||||
...attachments.value,
|
||||
...result.files.map(
|
||||
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image),
|
||||
...results.map(
|
||||
(xfile) => UniversalFile(data: xfile, type: UniversalFileType.image),
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -267,6 +265,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
if (messageController.text.trim().isNotEmpty ||
|
||||
attachments.value.isNotEmpty) {
|
||||
messagesNotifier.sendMessage(
|
||||
ref,
|
||||
messageController.text.trim(),
|
||||
attachments.value,
|
||||
editingTo: messageEditingTo.value,
|
||||
@@ -563,7 +562,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
client: ref.read(apiClientProvider),
|
||||
ref: ref,
|
||||
fileData: attachment,
|
||||
poolId: config.poolId,
|
||||
mode:
|
||||
@@ -573,7 +572,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
onProgress: (progress, _) {
|
||||
attachmentProgress.value = {
|
||||
...attachmentProgress.value,
|
||||
'chat-upload': {index: progress},
|
||||
'chat-upload': {index: progress ?? 0.0},
|
||||
};
|
||||
},
|
||||
).future;
|
||||
|
||||
@@ -261,7 +261,11 @@ class _PublisherUnselectedWidget extends HookConsumerWidget {
|
||||
subtitle: Text('createPublisherHint').tr(),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context.pushNamed('creatorNew').then((value) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => const NewPublisherScreen(),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(publishersManagedProvider);
|
||||
}
|
||||
@@ -285,19 +289,18 @@ class CreatorHubScreen extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
void updatePublisher() {
|
||||
context
|
||||
.pushNamed(
|
||||
'creatorEdit',
|
||||
pathParameters: {'name': currentPublisher.value!.name},
|
||||
)
|
||||
.then((value) async {
|
||||
if (value == null) return;
|
||||
final data = await ref.refresh(publishersManagedProvider.future);
|
||||
currentPublisher.value =
|
||||
data
|
||||
.where((e) => e.id == currentPublisher.value!.id)
|
||||
.firstOrNull;
|
||||
});
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) =>
|
||||
EditPublisherScreen(name: currentPublisher.value!.name),
|
||||
).then((value) async {
|
||||
if (value == null) return;
|
||||
final data = await ref.refresh(publishersManagedProvider.future);
|
||||
currentPublisher.value =
|
||||
data.where((e) => e.id == currentPublisher.value!.id).firstOrNull;
|
||||
});
|
||||
}
|
||||
|
||||
void deletePublisher() {
|
||||
|
||||
@@ -16,8 +16,8 @@ import 'package:island/screens/realm/realms.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
@@ -95,11 +95,11 @@ class EditPublisherScreen extends HookConsumerWidget {
|
||||
try {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
ref: ref,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
client: ref.read(apiClientProvider),
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
@@ -177,13 +177,11 @@ class EditPublisherScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text(name == null ? 'createPublisher' : 'editPublisher').tr(),
|
||||
leading: const PageBackButton(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
final titleText = (name == null ? 'createPublisher' : 'editPublisher').tr();
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: titleText,
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
|
||||
@@ -141,7 +141,7 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
try {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
client: ref.read(apiClientProvider),
|
||||
ref: ref,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
||||
@@ -127,11 +127,11 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
try {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
ref: ref,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
client: ref.read(apiClientProvider),
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
|
||||
@@ -19,7 +19,6 @@ import 'package:island/widgets/check_in.dart';
|
||||
import 'package:island/widgets/navigation/fab_menu.dart';
|
||||
import 'package:island/widgets/post/post_featured.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:island/widgets/post/compose_card.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
@@ -341,7 +340,6 @@ class ExploreScreen extends HookConsumerWidget {
|
||||
margin: EdgeInsets.zero,
|
||||
),
|
||||
PostFeaturedList(),
|
||||
const PostComposeCard(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
538
lib/screens/files/file_detail.dart
Normal file
538
lib/screens/files/file_detail.dart
Normal file
@@ -0,0 +1,538 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/utils/format.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/audio.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||
import 'package:island/widgets/content/video.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:path/path.dart' show extension;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||
|
||||
class FileDetailScreen extends HookConsumerWidget {
|
||||
final SnCloudFile item;
|
||||
|
||||
const FileDetailScreen({super.key, required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
// Animation controller for the drawer
|
||||
final animationController = useAnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
final animation = useMemoized(
|
||||
() => Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(parent: animationController, curve: Curves.easeInOut),
|
||||
),
|
||||
[animationController],
|
||||
);
|
||||
|
||||
final showDrawer = useState(false);
|
||||
|
||||
void showInfoSheet() {
|
||||
if (isWide) {
|
||||
// Show as animated right panel on wide screens
|
||||
showDrawer.value = !showDrawer.value;
|
||||
if (showDrawer.value) {
|
||||
animationController.forward();
|
||||
} else {
|
||||
animationController.reverse();
|
||||
}
|
||||
} else {
|
||||
// Show as bottom sheet on narrow screens
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen to drawer state changes
|
||||
useEffect(() {
|
||||
void listener() {
|
||||
if (!animationController.isAnimating) {
|
||||
if (animationController.value == 0) {
|
||||
showDrawer.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animationController.addListener(listener);
|
||||
return () => animationController.removeListener(listener);
|
||||
}, [animationController]);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: true,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: Text(item.name.isEmpty ? 'File Details' : item.name),
|
||||
actions: _buildAppBarActions(context, ref, showInfoSheet),
|
||||
),
|
||||
body: AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return Row(
|
||||
children: [
|
||||
// Main content area
|
||||
Expanded(child: _buildContent(context, ref, serverUrl)),
|
||||
// Animated drawer panel
|
||||
if (isWide)
|
||||
SizedBox(
|
||||
height: double.infinity,
|
||||
width: animation.value * 400, // Max width of 400px
|
||||
child: Container(
|
||||
child:
|
||||
animation.value > 0.1
|
||||
? FileInfoSheet(item: item, onClose: showInfoSheet)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildAppBarActions(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
VoidCallback showInfoSheet,
|
||||
) {
|
||||
final actions = <Widget>[];
|
||||
|
||||
// Add content-specific actions
|
||||
switch (item.mimeType?.split('/').firstOrNull) {
|
||||
case 'image':
|
||||
if (!kIsWeb) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: Icon(Icons.save_alt),
|
||||
onPressed: () async => _saveToGallery(ref),
|
||||
),
|
||||
);
|
||||
}
|
||||
// HD/SD toggle will be handled in the image content overlay
|
||||
break;
|
||||
default:
|
||||
if (!kIsWeb) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: Icon(Icons.save_alt),
|
||||
onPressed: () async => _downloadFile(ref),
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Always add info button
|
||||
actions.add(
|
||||
IconButton(icon: Icon(Icons.info_outline), onPressed: showInfoSheet),
|
||||
);
|
||||
|
||||
actions.add(const Gap(8));
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
Future<void> _saveToGallery(WidgetRef ref) async {
|
||||
try {
|
||||
showSnackBar('Saving image...');
|
||||
|
||||
final client = ref.read(apiClientProvider);
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
var extName = extension(item.name).trim();
|
||||
if (extName.isEmpty) {
|
||||
extName = item.mimeType?.split('/').lastOrNull ?? 'jpeg';
|
||||
}
|
||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
||||
|
||||
await client.download(
|
||||
'/drive/files/${item.id}',
|
||||
filePath,
|
||||
queryParameters: {'original': true},
|
||||
);
|
||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||
await Gal.putImage(filePath, album: 'Solar Network');
|
||||
showSnackBar('Image saved to gallery');
|
||||
} else {
|
||||
await FileSaver.instance.saveFile(
|
||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||
file: File(filePath),
|
||||
);
|
||||
showSnackBar('Image saved to $filePath');
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _downloadFile(WidgetRef ref) async {
|
||||
try {
|
||||
showSnackBar('Downloading file...');
|
||||
|
||||
final client = ref.read(apiClientProvider);
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
var extName = extension(item.name).trim();
|
||||
if (extName.isEmpty) {
|
||||
extName = item.mimeType?.split('/').lastOrNull ?? 'bin';
|
||||
}
|
||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
||||
|
||||
await client.download(
|
||||
'/drive/files/${item.id}',
|
||||
filePath,
|
||||
queryParameters: {'original': true},
|
||||
);
|
||||
|
||||
await FileSaver.instance.saveFile(
|
||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||
file: File(filePath),
|
||||
);
|
||||
showSnackBar('File saved to downloads');
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, WidgetRef ref, String serverUrl) {
|
||||
final uri = '$serverUrl/drive/files/${item.id}';
|
||||
|
||||
return switch (item.mimeType?.split('/').firstOrNull) {
|
||||
'image' => _buildImageContent(context, ref, uri),
|
||||
'video' => _buildVideoContent(context, ref, uri),
|
||||
'audio' => _buildAudioContent(context, ref, uri),
|
||||
_ when item.mimeType == 'application/pdf' => _PdfContent(uri: uri),
|
||||
_ when item.mimeType?.startsWith('text/') == true => _TextContent(
|
||||
uri: uri,
|
||||
),
|
||||
_ => _buildGenericContent(context, ref),
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildImageContent(BuildContext context, WidgetRef ref, String uri) {
|
||||
final photoViewController = useMemoized(() => PhotoViewController(), []);
|
||||
final rotation = useState(0);
|
||||
final showOriginal = useState(false);
|
||||
|
||||
final shadow = [
|
||||
Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)),
|
||||
];
|
||||
|
||||
return DismissiblePage(
|
||||
isFullScreen: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
direction: DismissiblePageDismissDirection.down,
|
||||
onDismissed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: PhotoView(
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.9),
|
||||
),
|
||||
controller: photoViewController,
|
||||
imageProvider: CloudImageWidget.provider(
|
||||
fileId: item.id,
|
||||
serverUrl: ref.watch(serverUrlProvider),
|
||||
original: showOriginal.value,
|
||||
),
|
||||
customSize: MediaQuery.of(context).size,
|
||||
basePosition: Alignment.center,
|
||||
filterQuality: FilterQuality.high,
|
||||
),
|
||||
),
|
||||
// Controls overlay
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.remove,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
onPressed: () {
|
||||
photoViewController.scale =
|
||||
(photoViewController.scale ?? 1) - 0.05;
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.add, color: Colors.white, shadows: shadow),
|
||||
onPressed: () {
|
||||
photoViewController.scale =
|
||||
(photoViewController.scale ?? 1) + 0.05;
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.rotate_left,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
onPressed: () {
|
||||
rotation.value = (rotation.value - 1) % 4;
|
||||
photoViewController.rotation =
|
||||
rotation.value * -math.pi / 2;
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.rotate_right,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
onPressed: () {
|
||||
rotation.value = (rotation.value + 1) % 4;
|
||||
photoViewController.rotation =
|
||||
rotation.value * -math.pi / 2;
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showOriginal.value = !showOriginal.value;
|
||||
},
|
||||
icon: Icon(
|
||||
showOriginal.value ? Symbols.hd : Symbols.sd,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoContent(BuildContext context, WidgetRef ref, String uri) {
|
||||
var ratio =
|
||||
item.fileMeta?['ratio'] is num
|
||||
? item.fileMeta!['ratio'].toDouble()
|
||||
: 1.0;
|
||||
if (ratio == 0) ratio = 1.0;
|
||||
|
||||
return DismissiblePage(
|
||||
isFullScreen: true,
|
||||
backgroundColor: Colors.black,
|
||||
direction: DismissiblePageDismissDirection.down,
|
||||
onDismissed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: UniversalVideo(uri: uri, autoplay: true),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAudioContent(BuildContext context, WidgetRef ref, String uri) {
|
||||
return DismissiblePage(
|
||||
isFullScreen: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
direction: DismissiblePageDismissDirection.down,
|
||||
onDismissed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: math.min(360, MediaQuery.of(context).size.width * 0.8),
|
||||
),
|
||||
child: UniversalAudio(uri: uri, filename: item.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGenericContent(BuildContext context, WidgetRef ref) {
|
||||
Future<void> downloadFile() async {
|
||||
try {
|
||||
showSnackBar('Downloading file...');
|
||||
|
||||
final client = ref.read(apiClientProvider);
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
var extName = extension(item.name).trim();
|
||||
if (extName.isEmpty) {
|
||||
extName = item.mimeType?.split('/').lastOrNull ?? 'bin';
|
||||
}
|
||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
||||
|
||||
await client.download(
|
||||
'/drive/files/${item.id}',
|
||||
filePath,
|
||||
queryParameters: {'original': true},
|
||||
);
|
||||
|
||||
await FileSaver.instance.saveFile(
|
||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||
file: File(filePath),
|
||||
);
|
||||
showSnackBar('File saved to downloads');
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
return DismissiblePage(
|
||||
isFullScreen: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
direction: DismissiblePageDismissDirection.down,
|
||||
onDismissed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(32),
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.insert_drive_file,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const Gap(16),
|
||||
Text(
|
||||
item.name,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
formatFileSize(item.size),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
onPressed: downloadFile,
|
||||
icon: const Icon(Symbols.download),
|
||||
label: Text('download').tr(),
|
||||
),
|
||||
const Gap(16),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.info),
|
||||
label: Text('info').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PdfContent extends HookConsumerWidget {
|
||||
final String uri;
|
||||
|
||||
const _PdfContent({required this.uri});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final pdfViewer = useMemoized(() => SfPdfViewer.network(uri), [uri]);
|
||||
return pdfViewer;
|
||||
}
|
||||
}
|
||||
|
||||
class _TextContent extends HookConsumerWidget {
|
||||
final String uri;
|
||||
|
||||
const _TextContent({required this.uri});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final textFuture = useMemoized(
|
||||
() => ref
|
||||
.read(apiClientProvider)
|
||||
.get(uri)
|
||||
.then((response) => response.data as String),
|
||||
[uri],
|
||||
);
|
||||
|
||||
return FutureBuilder<String>(
|
||||
future: textFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.hasError) {
|
||||
return Center(child: Text('Error loading text: ${snapshot.error}'));
|
||||
} else if (snapshot.hasData) {
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: SelectableText(
|
||||
snapshot.data!,
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 14),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Center(child: Text('No content'));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +1,61 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/file_pool.dart';
|
||||
import 'package:island/utils/format.dart';
|
||||
import 'package:island/pods/file_list.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/file_list_view.dart';
|
||||
import 'package:island/widgets/usage_overview.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'file_list.g.dart';
|
||||
|
||||
@riverpod
|
||||
class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||
with CursorPagingNotifierMixin<SnCloudFile> {
|
||||
String? _poolId;
|
||||
bool _includeRecycled = false;
|
||||
|
||||
void setFilters(String? poolId, bool includeRecycled) {
|
||||
_poolId = poolId;
|
||||
_includeRecycled = includeRecycled;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<SnCloudFile>> build() => fetch(cursor: null);
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<SnCloudFile>> fetch({required String? cursor}) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||
final take = 20;
|
||||
|
||||
final queryParameters = <String, dynamic>{'offset': offset, 'take': take};
|
||||
|
||||
// Add filter parameters
|
||||
if (_poolId != null) {
|
||||
queryParameters['pool'] = _poolId!;
|
||||
}
|
||||
if (_includeRecycled) {
|
||||
queryParameters['recycled'] = 'true';
|
||||
}
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/files/me',
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
final List<SnCloudFile> items =
|
||||
(response.data as List)
|
||||
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
|
||||
final hasMore = offset + items.length < total;
|
||||
final nextCursor = hasMore ? (offset + items.length).toString() : null;
|
||||
|
||||
return CursorPagingData(
|
||||
items: items,
|
||||
hasMore: hasMore,
|
||||
nextCursor: nextCursor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/drive/billing/usage');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<Map<String, dynamic>?> billingQuota(Ref ref) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/drive/billing/quota');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
class FileListScreen extends HookConsumerWidget {
|
||||
const FileListScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Filter state
|
||||
final selectedPool = useState<String?>(null);
|
||||
final includeRecycled = useState(false);
|
||||
// Path navigation state
|
||||
final currentPath = useState<String>('/');
|
||||
final mode = useState<FileListMode>(FileListMode.normal);
|
||||
|
||||
final usageAsync = ref.watch(billingUsageProvider);
|
||||
final quotaAsync = ref.watch(billingQuotaProvider);
|
||||
|
||||
// Update notifier filters when state changes
|
||||
useEffect(() {
|
||||
final notifier = ref.read(cloudFileListNotifierProvider.notifier);
|
||||
notifier.setFilters(selectedPool.value, includeRecycled.value);
|
||||
return null;
|
||||
}, [selectedPool.value, includeRecycled.value]);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: Text('Files'), leading: const PageBackButton()),
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text('Files'),
|
||||
leading: const PageBackButton(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.bar_chart),
|
||||
onPressed:
|
||||
() => _showUsageSheet(
|
||||
context,
|
||||
usageAsync.value,
|
||||
quotaAsync.value,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: usageAsync.when(
|
||||
data:
|
||||
(usage) => quotaAsync.when(
|
||||
data:
|
||||
(quota) => _buildQuotaUI(
|
||||
usage,
|
||||
quota,
|
||||
ref,
|
||||
selectedPool,
|
||||
includeRecycled,
|
||||
(quota) => FileListView(
|
||||
usage: usage,
|
||||
quota: quota,
|
||||
currentPath: currentPath,
|
||||
onPickAndUpload:
|
||||
() => _pickAndUploadFile(ref, currentPath.value),
|
||||
onShowCreateDirectory: _showCreateDirectoryDialog,
|
||||
mode: mode,
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(child: Text('Error loading quota')),
|
||||
@@ -127,430 +66,135 @@ class FileListScreen extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuotaUI(
|
||||
Map<String, dynamic>? usage,
|
||||
Map<String, dynamic>? quota,
|
||||
WidgetRef ref,
|
||||
ValueNotifier<String?> selectedPool,
|
||||
ValueNotifier<bool> includeRecycled,
|
||||
) {
|
||||
if (usage == null) return const SizedBox.shrink();
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
const SliverGap(8),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'All Uploads',
|
||||
'${((usage['total_usage_bytes'] as num) / (1024 * 1024 * 1024)).toStringAsFixed(3)} GiB',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'All Files',
|
||||
'${usage['total_file_count']}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Quota',
|
||||
'${usage['total_quota']} MiB',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Used Quota',
|
||||
'${((usage['used_quota'] as num) / (usage['total_quota'] as num) * 100).toStringAsFixed(2)}%',
|
||||
progress:
|
||||
(usage['used_quota'] as num) /
|
||||
(usage['total_quota'] as num),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('Pool Usage'),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(_buildPoolChartData(usage)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('Verbose Quota'),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(_buildQuotaChartData(quota)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
),
|
||||
const SliverGap(8),
|
||||
SliverToBoxAdapter(
|
||||
child: _buildFilters(ref, selectedPool, includeRecycled),
|
||||
),
|
||||
const SliverGap(8),
|
||||
PagingHelperSliverView(
|
||||
provider: cloudFileListNotifierProvider,
|
||||
futureRefreshable: cloudFileListNotifierProvider.future,
|
||||
notifierRefreshable: cloudFileListNotifierProvider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => SliverList.builder(
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
|
||||
final item = data.items[index];
|
||||
final itemType = item.mimeType?.split('/').firstOrNull;
|
||||
return ListTile(
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: switch (itemType) {
|
||||
'image' => CloudImageWidget(file: item),
|
||||
'audio' =>
|
||||
const Icon(Symbols.audio_file, fill: 1).center(),
|
||||
'video' =>
|
||||
const Icon(Symbols.video_file, fill: 1).center(),
|
||||
_ =>
|
||||
const Icon(Symbols.body_system, fill: 1).center(),
|
||||
},
|
||||
),
|
||||
),
|
||||
title:
|
||||
item.name.isEmpty
|
||||
? Text('untitled').tr().italic()
|
||||
: Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(formatFileSize(item.size)),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (context.mounted) showLoadingModal(context);
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/drive/files/${item.id}');
|
||||
ref.invalidate(cloudFileListNotifierProvider);
|
||||
} catch (e) {
|
||||
showSnackBar('failedToDeleteFile'.tr());
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
PieChartData _buildPoolChartData(Map<String, dynamic> usage) {
|
||||
final pools = usage['pool_usages'] as List<dynamic>;
|
||||
final colors = [
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.orange,
|
||||
Colors.red,
|
||||
Colors.purple,
|
||||
];
|
||||
return PieChartData(
|
||||
sections:
|
||||
pools.asMap().entries.map((entry) {
|
||||
final pool = entry.value as Map<String, dynamic>;
|
||||
final title = pool['pool_name'] as String;
|
||||
final truncatedTitle =
|
||||
title.length > 8 ? '${title.substring(0, 8)}...' : title;
|
||||
return PieChartSectionData(
|
||||
value: (pool['usage_bytes'] as num).toDouble(),
|
||||
title: truncatedTitle,
|
||||
color: colors[entry.key % colors.length],
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
PieChartData _buildQuotaChartData(Map<String, dynamic>? quota) {
|
||||
if (quota == null) return PieChartData(sections: []);
|
||||
return PieChartData(
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
value: (quota['based_quota'] as num).toDouble(),
|
||||
title: 'Base',
|
||||
color: Colors.green,
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
value: (quota['extra_quota'] as num).toDouble(),
|
||||
title: 'Extra',
|
||||
color: Colors.orange,
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilters(
|
||||
WidgetRef ref,
|
||||
ValueNotifier<String?> selectedPool,
|
||||
ValueNotifier<bool> includeRecycled,
|
||||
) {
|
||||
final poolsAsync = ref.watch(poolsProvider);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'filters'.tr(),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Gap(16),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth > 600;
|
||||
return isWide
|
||||
? Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: poolsAsync.when(
|
||||
data:
|
||||
(pools) => DropdownButtonFormField<String?>(
|
||||
value: selectedPool.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Pool',
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('allPools'.tr()),
|
||||
),
|
||||
...pools.map(
|
||||
(pool) => DropdownMenuItem<String?>(
|
||||
value: pool.id,
|
||||
child: Text(pool.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged:
|
||||
(value) => selectedPool.value = value,
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (e, _) => const Text('Error loading pools'),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text('includeRecycled'.tr()),
|
||||
const Gap(8),
|
||||
Switch(
|
||||
value: includeRecycled.value,
|
||||
onChanged:
|
||||
(value) => includeRecycled.value = value,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete_sweep),
|
||||
tooltip: 'deleteRecycledFiles'.tr(),
|
||||
onPressed:
|
||||
includeRecycled.value
|
||||
? () => _deleteRecycledFiles(ref)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
poolsAsync.when(
|
||||
data:
|
||||
(pools) => DropdownButtonFormField<String?>(
|
||||
value: selectedPool.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pool',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('allPools'.tr()),
|
||||
),
|
||||
...pools.map(
|
||||
(pool) => DropdownMenuItem<String?>(
|
||||
value: pool.id,
|
||||
child: Text(pool.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged:
|
||||
(value) => selectedPool.value = value,
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (e, _) => const Text('Error loading pools'),
|
||||
),
|
||||
const Gap(16),
|
||||
Row(
|
||||
children: [
|
||||
Text('includeRecycled'.tr()),
|
||||
const Gap(8),
|
||||
Switch(
|
||||
value: includeRecycled.value,
|
||||
onChanged:
|
||||
(value) => includeRecycled.value = value,
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete_sweep),
|
||||
tooltip: 'deleteRecycledFiles'.tr(),
|
||||
onPressed:
|
||||
includeRecycled.value
|
||||
? () => _deleteRecycledFiles(ref)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
).padding(horizontal: 8);
|
||||
}
|
||||
|
||||
Future<void> _deleteRecycledFiles(WidgetRef ref) async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteRecycledFiles'.tr(),
|
||||
'deleteRecycledFiles'.tr(),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (ref.context.mounted) showLoadingModal(ref.context);
|
||||
Future<void> _pickAndUploadFile(WidgetRef ref, String currentPath) async {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/drive/files/recycled');
|
||||
ref.invalidate(cloudFileListNotifierProvider);
|
||||
showSnackBar('recycledFilesDeleted'.tr());
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
allowMultiple: true,
|
||||
withData: false,
|
||||
);
|
||||
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
for (final file in result.files) {
|
||||
if (file.path != null) {
|
||||
// Create UniversalFile from the picked file
|
||||
final universalFile = UniversalFile(
|
||||
data: XFile(file.path!),
|
||||
type: UniversalFileType.file,
|
||||
displayName: file.name,
|
||||
);
|
||||
|
||||
// Upload the file with the current path
|
||||
final completer = FileUploader.createCloudFile(
|
||||
fileData: universalFile,
|
||||
ref: ref,
|
||||
path: currentPath,
|
||||
onProgress: (progress, _) {
|
||||
// Progress is handled by the upload tasks system
|
||||
if (progress != null) {
|
||||
debugPrint('Upload progress: ${(progress * 100).toInt()}%');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
completer.future
|
||||
.then((uploadedFile) {
|
||||
if (uploadedFile != null) {
|
||||
ref.invalidate(cloudFileListNotifierProvider);
|
||||
}
|
||||
})
|
||||
.catchError((error) {
|
||||
showSnackBar('Failed to upload file: $error');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
showSnackBar('failedToDeleteRecycledFiles'.tr());
|
||||
} finally {
|
||||
if (ref.context.mounted) hideLoadingModal(ref.context);
|
||||
showSnackBar('Error picking file: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildStatCard(String label, String value, {double? progress}) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 14)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
Future<void> _showCreateDirectoryDialog(
|
||||
BuildContext context,
|
||||
ValueNotifier<String> currentPath,
|
||||
) async {
|
||||
final controller = TextEditingController(text: currentPath.value);
|
||||
String? newPath;
|
||||
|
||||
void handleChangeDirectory(BuildContext context) {
|
||||
newPath = controller.text.trim();
|
||||
if (newPath!.isNotEmpty) {
|
||||
// Normalize the path
|
||||
String fullPath = newPath!;
|
||||
|
||||
// Ensure it starts with /
|
||||
if (!fullPath.startsWith('/')) {
|
||||
fullPath = '/$fullPath';
|
||||
}
|
||||
|
||||
// Remove double slashes and normalize
|
||||
fullPath = fullPath.replaceAll(RegExp(r'/+'), '/');
|
||||
|
||||
currentPath.value = fullPath;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Navigate to Directory'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Directory path',
|
||||
hintText: 'e.g., documents, projects/my-app',
|
||||
helperText:
|
||||
'Enter a directory path. The directory will be created when you upload files to it.',
|
||||
helperMaxLines: 3,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) {
|
||||
handleChangeDirectory(context);
|
||||
},
|
||||
),
|
||||
if (progress != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(value: progress),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => handleChangeDirectory(context),
|
||||
label: const Text('Go to Directory'),
|
||||
icon: const Icon(Symbols.arrow_right_alt),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showUsageSheet(
|
||||
BuildContext context,
|
||||
Map<String, dynamic>? usage,
|
||||
Map<String, dynamic>? quota,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'Usage Overview',
|
||||
child: UsageOverviewWidget(usage: usage, quota: quota),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
children: [
|
||||
ValueListenableBuilder<Map<int, double>>(
|
||||
ValueListenableBuilder<Map<int, double?>>(
|
||||
valueListenable: state.attachmentProgress,
|
||||
builder: (context, progressMap, _) {
|
||||
return Wrap(
|
||||
|
||||
@@ -92,7 +92,7 @@ class EditRealmScreen extends HookConsumerWidget {
|
||||
try {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
client: ref.read(apiClientProvider),
|
||||
ref: ref,
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
||||
@@ -5,8 +5,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/screens/notification.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/navigation/conditional_bottom_nav.dart';
|
||||
import 'package:island/widgets/navigation/fab_menu.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@@ -21,6 +23,7 @@ const kTabRoutes = [
|
||||
'/chat',
|
||||
'/realms',
|
||||
'/account',
|
||||
'/files',
|
||||
'/creators',
|
||||
'/developers',
|
||||
];
|
||||
@@ -66,19 +69,36 @@ class TabsScreen extends HookConsumerWidget {
|
||||
icon: Badge.count(
|
||||
count: notificationUnreadCount.value ?? 0,
|
||||
isLabelVisible: (notificationUnreadCount.value ?? 0) > 0,
|
||||
child: const Icon(Symbols.person_rounded),
|
||||
child: Consumer(
|
||||
child: const Icon(Symbols.account_circle_rounded),
|
||||
builder: (context, ref, fallbackChild) {
|
||||
final userInfo = ref.watch(userInfoProvider);
|
||||
if (userInfo.value?.profile.picture != null) {
|
||||
return ProfilePictureWidget(
|
||||
file: userInfo.value!.profile.picture,
|
||||
radius: 12,
|
||||
);
|
||||
}
|
||||
return fallbackChild!;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (wideScreen)
|
||||
NavigationDestination(
|
||||
label: 'creatorHub'.tr(),
|
||||
icon: const Icon(Symbols.design_services_rounded),
|
||||
),
|
||||
if (wideScreen)
|
||||
NavigationDestination(
|
||||
label: 'developerHub'.tr(),
|
||||
icon: const Icon(Symbols.data_object_rounded),
|
||||
),
|
||||
...([
|
||||
NavigationDestination(
|
||||
label: 'files'.tr(),
|
||||
icon: const Icon(Symbols.folder_rounded),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'creatorHub'.tr(),
|
||||
icon: const Icon(Symbols.design_services_rounded),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'developerHub'.tr(),
|
||||
icon: const Icon(Symbols.data_object_rounded),
|
||||
),
|
||||
]),
|
||||
];
|
||||
|
||||
int getCurrentIndex() {
|
||||
|
||||
@@ -22,7 +22,8 @@ class TrayService {
|
||||
await trayManager.setIcon(
|
||||
Platform.isWindows
|
||||
? 'assets/icons/icon.ico'
|
||||
: 'assets/icons/icon-outline.svg',
|
||||
: 'assets/icons/icon-tray.png',
|
||||
isTemplate: Platform.isMacOS,
|
||||
);
|
||||
|
||||
final menu = Menu(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/upload_tasks.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:native_exif/native_exif.dart';
|
||||
import 'package:path/path.dart' show extension;
|
||||
@@ -16,16 +17,57 @@ class FileUploader {
|
||||
|
||||
FileUploader(this._client);
|
||||
|
||||
/// Calculates the MD5 hash of a file.
|
||||
Future<String> _calculateFileHash(XFile file) async {
|
||||
final bytes = await file.readAsBytes();
|
||||
/// Calculates the MD5 hash of file bytes.
|
||||
String _calculateFileHash(Uint8List bytes) {
|
||||
final digest = md5.convert(bytes);
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
/// Calculates the MD5 hash from a stream.
|
||||
Future<String> _calculateFileHashFromStream(Stream<List<int>> stream) async {
|
||||
final accumulator = AccumulatorSink<Digest>();
|
||||
final converter = md5.startChunkedConversion(accumulator);
|
||||
await for (final chunk in stream) {
|
||||
converter.add(chunk);
|
||||
}
|
||||
converter.close();
|
||||
final digest = accumulator.events.single;
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
/// Reads the next chunk from a stream subscription.
|
||||
Future<Uint8List> _readNextChunk(
|
||||
StreamSubscription<List<int>> subscription,
|
||||
int size,
|
||||
) async {
|
||||
final completer = Completer<Uint8List>();
|
||||
final buffer = <int>[];
|
||||
int remaining = size;
|
||||
|
||||
void onData(List<int> data) {
|
||||
buffer.addAll(data);
|
||||
remaining -= data.length;
|
||||
if (remaining <= 0) {
|
||||
subscription.pause();
|
||||
completer.complete(Uint8List.fromList(buffer.sublist(0, size)));
|
||||
}
|
||||
}
|
||||
|
||||
void onDone() {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(Uint8List.fromList(buffer));
|
||||
}
|
||||
}
|
||||
|
||||
subscription.onData(onData);
|
||||
subscription.onDone(onDone);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Creates an upload task for the given file.
|
||||
Future<Map<String, dynamic>> createUploadTask({
|
||||
required XFile file,
|
||||
required dynamic fileData,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
String? poolId,
|
||||
@@ -33,9 +75,19 @@ class FileUploader {
|
||||
String? encryptPassword,
|
||||
String? expiredAt,
|
||||
int? chunkSize,
|
||||
String? path,
|
||||
}) async {
|
||||
final hash = await _calculateFileHash(file);
|
||||
final fileSize = await file.length();
|
||||
String hash;
|
||||
int fileSize;
|
||||
if (fileData is XFile) {
|
||||
fileSize = await fileData.length();
|
||||
hash = await _calculateFileHashFromStream(fileData.openRead());
|
||||
} else if (fileData is Uint8List) {
|
||||
hash = _calculateFileHash(fileData);
|
||||
fileSize = fileData.length;
|
||||
} else {
|
||||
throw ArgumentError('Invalid fileData type');
|
||||
}
|
||||
|
||||
final response = await _client.post(
|
||||
'/drive/files/upload/create',
|
||||
@@ -49,6 +101,7 @@ class FileUploader {
|
||||
'encrypt_password': encryptPassword,
|
||||
'expired_at': expiredAt,
|
||||
'chunk_size': chunkSize,
|
||||
'path': path,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -60,6 +113,7 @@ class FileUploader {
|
||||
required String taskId,
|
||||
required int chunkIndex,
|
||||
required Uint8List chunkData,
|
||||
ProgressCallback? onSendProgress,
|
||||
}) async {
|
||||
final formData = FormData.fromMap({
|
||||
'chunk': MultipartFile.fromBytes(
|
||||
@@ -71,19 +125,26 @@ class FileUploader {
|
||||
await _client.post(
|
||||
'/drive/files/upload/chunk/$taskId/$chunkIndex',
|
||||
data: formData,
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
}
|
||||
|
||||
/// Completes the upload and returns the CloudFile object.
|
||||
Future<SnCloudFile> completeUpload(String taskId) async {
|
||||
final response = await _client.post('/drive/files/upload/complete/$taskId');
|
||||
final response = await _client.post(
|
||||
'/drive/files/upload/complete/$taskId',
|
||||
options: Options(
|
||||
sendTimeout: Duration(minutes: 1),
|
||||
receiveTimeout: Duration(minutes: 1),
|
||||
),
|
||||
);
|
||||
|
||||
return SnCloudFile.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Uploads a file in chunks using the multi-part API.
|
||||
Future<SnCloudFile> uploadFile({
|
||||
required XFile file,
|
||||
required dynamic fileData,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
String? poolId,
|
||||
@@ -91,10 +152,13 @@ class FileUploader {
|
||||
String? encryptPassword,
|
||||
String? expiredAt,
|
||||
int? customChunkSize,
|
||||
String? path,
|
||||
Function(double? progress, Duration estimate)? onProgress,
|
||||
}) async {
|
||||
// Step 1: Create upload task
|
||||
onProgress?.call(null, Duration.zero);
|
||||
final createResponse = await createUploadTask(
|
||||
file: file,
|
||||
fileData: fileData,
|
||||
fileName: fileName,
|
||||
contentType: contentType,
|
||||
poolId: poolId,
|
||||
@@ -102,6 +166,7 @@ class FileUploader {
|
||||
encryptPassword: encryptPassword,
|
||||
expiredAt: expiredAt,
|
||||
chunkSize: customChunkSize,
|
||||
path: path,
|
||||
);
|
||||
|
||||
if (createResponse['file_exists'] == true) {
|
||||
@@ -112,50 +177,74 @@ class FileUploader {
|
||||
final taskId = createResponse['task_id'] as String;
|
||||
final chunkSize = createResponse['chunk_size'] as int;
|
||||
final chunksCount = createResponse['chunks_count'] as int;
|
||||
int totalSize;
|
||||
if (fileData is XFile) {
|
||||
totalSize = await fileData.length();
|
||||
} else if (fileData is Uint8List) {
|
||||
totalSize = fileData.length;
|
||||
} else {
|
||||
throw ArgumentError('Invalid fileData type');
|
||||
}
|
||||
|
||||
// Step 2: Upload chunks
|
||||
final stream = file.openRead();
|
||||
final chunks = <Uint8List>[];
|
||||
int bytesRead = 0;
|
||||
final buffer = BytesBuilder();
|
||||
|
||||
await for (final chunk in stream) {
|
||||
buffer.add(chunk);
|
||||
bytesRead += chunk.length;
|
||||
|
||||
if (bytesRead >= chunkSize) {
|
||||
chunks.add(buffer.takeBytes());
|
||||
bytesRead = 0;
|
||||
int bytesUploaded = 0;
|
||||
if (fileData is XFile) {
|
||||
// Use stream for XFile
|
||||
final subscription = fileData.openRead().listen(null);
|
||||
subscription.pause();
|
||||
for (int i = 0; i < chunksCount; i++) {
|
||||
subscription.resume();
|
||||
final chunkData = await _readNextChunk(subscription, chunkSize);
|
||||
await uploadChunk(
|
||||
taskId: taskId,
|
||||
chunkIndex: i,
|
||||
chunkData: chunkData,
|
||||
onSendProgress: (sent, total) {
|
||||
final overallProgress = (bytesUploaded + sent) / totalSize;
|
||||
onProgress?.call(overallProgress, Duration.zero);
|
||||
},
|
||||
);
|
||||
bytesUploaded += chunkData.length;
|
||||
}
|
||||
subscription.cancel();
|
||||
} else if (fileData is Uint8List) {
|
||||
// Use old way for Uint8List
|
||||
final chunks = <Uint8List>[];
|
||||
for (int i = 0; i < fileData.length; i += chunkSize) {
|
||||
final end =
|
||||
i + chunkSize > fileData.length ? fileData.length : i + chunkSize;
|
||||
chunks.add(Uint8List.fromList(fileData.sublist(i, end)));
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining bytes as last chunk
|
||||
if (buffer.length > 0) {
|
||||
chunks.add(buffer.takeBytes());
|
||||
}
|
||||
|
||||
// Ensure we have the correct number of chunks
|
||||
if (chunks.length != chunksCount) {
|
||||
throw Exception(
|
||||
'Chunk count mismatch: expected $chunksCount, got ${chunks.length}',
|
||||
);
|
||||
}
|
||||
|
||||
// Upload each chunk
|
||||
for (int i = 0; i < chunks.length; i++) {
|
||||
await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunks[i]);
|
||||
// Upload each chunk
|
||||
for (int i = 0; i < chunks.length; i++) {
|
||||
await uploadChunk(
|
||||
taskId: taskId,
|
||||
chunkIndex: i,
|
||||
chunkData: chunks[i],
|
||||
onSendProgress: (sent, total) {
|
||||
final overallProgress = (bytesUploaded + sent) / totalSize;
|
||||
onProgress?.call(overallProgress, Duration.zero);
|
||||
},
|
||||
);
|
||||
bytesUploaded += chunks[i].length;
|
||||
}
|
||||
} else {
|
||||
throw ArgumentError('Invalid fileData type');
|
||||
}
|
||||
|
||||
// Step 3: Complete upload
|
||||
onProgress?.call(null, Duration.zero);
|
||||
return await completeUpload(taskId);
|
||||
}
|
||||
|
||||
static Completer<SnCloudFile?> createCloudFile({
|
||||
required UniversalFile fileData,
|
||||
required Dio client,
|
||||
required WidgetRef ref,
|
||||
String? poolId,
|
||||
String? path,
|
||||
FileUploadMode? mode,
|
||||
Function(double progress, Duration estimate)? onProgress,
|
||||
Function(double? progress, Duration estimate)? onProgress,
|
||||
}) {
|
||||
final completer = Completer<SnCloudFile?>();
|
||||
|
||||
@@ -191,8 +280,9 @@ class FileUploader {
|
||||
.then(
|
||||
(_) => _processUpload(
|
||||
fileData,
|
||||
client,
|
||||
ref,
|
||||
poolId,
|
||||
path,
|
||||
onProgress,
|
||||
completer,
|
||||
),
|
||||
@@ -201,8 +291,9 @@ class FileUploader {
|
||||
debugPrint('Error removing GPS EXIF data: $e');
|
||||
return _processUpload(
|
||||
fileData,
|
||||
client,
|
||||
ref,
|
||||
poolId,
|
||||
path,
|
||||
onProgress,
|
||||
completer,
|
||||
);
|
||||
@@ -212,33 +303,41 @@ class FileUploader {
|
||||
}
|
||||
}
|
||||
|
||||
_processUpload(fileData, client, poolId, onProgress, completer);
|
||||
_processUpload(fileData, ref, poolId, path, onProgress, completer);
|
||||
return completer;
|
||||
}
|
||||
|
||||
// Helper method to process the upload
|
||||
// Helper method to process the upload with enhanced uploader
|
||||
static Completer<SnCloudFile?> _processUpload(
|
||||
UniversalFile fileData,
|
||||
Dio client,
|
||||
WidgetRef ref,
|
||||
String? poolId,
|
||||
Function(double progress, Duration estimate)? onProgress,
|
||||
String? path,
|
||||
Function(double? progress, Duration estimate)? onProgress,
|
||||
Completer<SnCloudFile?> completer,
|
||||
) {
|
||||
String actualMimetype = getMimeType(fileData);
|
||||
late XFile file;
|
||||
String actualFilename = fileData.displayName ?? 'randomly_file';
|
||||
Uint8List? byteData;
|
||||
Uint8List? bytes;
|
||||
|
||||
// Handle the data based on what's in the UniversalFile
|
||||
final data = fileData.data;
|
||||
|
||||
if (data is XFile) {
|
||||
file = data;
|
||||
actualFilename = fileData.displayName ?? data.name;
|
||||
_performUpload(
|
||||
fileData: data,
|
||||
fileName: fileData.displayName ?? data.name,
|
||||
path: path,
|
||||
contentType: actualMimetype,
|
||||
ref: ref,
|
||||
poolId: poolId,
|
||||
onProgress: onProgress,
|
||||
completer: completer,
|
||||
);
|
||||
return completer;
|
||||
} else if (data is List<int> || data is Uint8List) {
|
||||
byteData = data is List<int> ? Uint8List.fromList(data) : data;
|
||||
bytes = data is List<int> ? Uint8List.fromList(data) : data;
|
||||
actualFilename = fileData.displayName ?? 'uploaded_file';
|
||||
file = XFile.fromData(byteData!, mimeType: actualMimetype);
|
||||
} else if (data is SnCloudFile) {
|
||||
// If the file is already on the cloud, just return it
|
||||
completer.complete(data);
|
||||
@@ -252,28 +351,56 @@ class FileUploader {
|
||||
return completer;
|
||||
}
|
||||
|
||||
final uploader = FileUploader(client);
|
||||
if (bytes != null) {
|
||||
_performUpload(
|
||||
fileData: bytes,
|
||||
fileName: actualFilename,
|
||||
contentType: actualMimetype,
|
||||
path: path,
|
||||
ref: ref,
|
||||
poolId: poolId,
|
||||
onProgress: onProgress,
|
||||
completer: completer,
|
||||
);
|
||||
}
|
||||
|
||||
return completer;
|
||||
}
|
||||
|
||||
// Helper method to perform the actual upload with enhanced uploader
|
||||
static void _performUpload({
|
||||
required dynamic fileData,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
required WidgetRef ref,
|
||||
String? poolId,
|
||||
String? path,
|
||||
Function(double? progress, Duration estimate)? onProgress,
|
||||
required Completer<SnCloudFile?> completer,
|
||||
}) {
|
||||
// Use the enhanced uploader with task tracking
|
||||
final uploader = ref.read(enhancedFileUploaderProvider);
|
||||
|
||||
// Call progress start
|
||||
onProgress?.call(0.0, Duration.zero);
|
||||
onProgress?.call(null, Duration.zero);
|
||||
uploader
|
||||
.uploadFile(
|
||||
file: file,
|
||||
fileName: actualFilename,
|
||||
contentType: actualMimetype,
|
||||
fileData: fileData,
|
||||
fileName: fileName,
|
||||
contentType: contentType,
|
||||
poolId: poolId,
|
||||
path: path,
|
||||
onProgress: onProgress,
|
||||
)
|
||||
.then((result) {
|
||||
// Call progress end
|
||||
onProgress?.call(1.0, Duration.zero);
|
||||
onProgress?.call(null, Duration.zero);
|
||||
completer.complete(result);
|
||||
})
|
||||
.catchError((e) {
|
||||
completer.completeError(e);
|
||||
throw e;
|
||||
});
|
||||
|
||||
return completer;
|
||||
}
|
||||
|
||||
/// Gets the MIME type of a UniversalFile.
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:island/route.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/upload_overlay.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
@@ -198,6 +199,7 @@ class WindowScaffold extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
_WebSocketIndicator(),
|
||||
const UploadOverlay(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -213,7 +215,11 @@ class WindowScaffold extends HookConsumerWidget {
|
||||
actions: <Type, Action<Intent>>{PopIntent: PopAction(ref)},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [Positioned.fill(child: child), _WebSocketIndicator()],
|
||||
children: [
|
||||
Positioned.fill(child: child),
|
||||
_WebSocketIndicator(),
|
||||
const UploadOverlay(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:protocol_handler/protocol_handler.dart';
|
||||
import 'package:island/pods/activity/activity_rpc.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/route.dart';
|
||||
@@ -15,57 +16,61 @@ import 'package:island/widgets/tour/tour.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class AppWrapper extends HookConsumerWidget with TrayListener {
|
||||
class AppWrapper extends ConsumerStatefulWidget {
|
||||
final Widget child;
|
||||
const AppWrapper({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
useEffect(() {
|
||||
StreamSubscription? ntySubs;
|
||||
StreamSubscription? appLinksSubs;
|
||||
Future(() async {
|
||||
final appLinks = AppLinks();
|
||||
ConsumerState<AppWrapper> createState() => _AppWrapperState();
|
||||
}
|
||||
|
||||
if (context.mounted) ntySubs = setupNotificationListener(context, ref);
|
||||
class _AppWrapperState extends ConsumerState<AppWrapper>
|
||||
with ProtocolListener, TrayListener {
|
||||
StreamSubscription? ntySubs;
|
||||
bool networkStateShowing = false;
|
||||
|
||||
final sharingService = SharingIntentService();
|
||||
if (context.mounted) sharingService.initialize(context);
|
||||
if (context.mounted) UpdateService().checkForUpdates(context);
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
protocolHandler.addListener(this);
|
||||
Future(() async {
|
||||
if (mounted) ntySubs = setupNotificationListener(context, ref);
|
||||
|
||||
TrayService.instance.initialize(this);
|
||||
final sharingService = SharingIntentService();
|
||||
if (mounted) sharingService.initialize(context);
|
||||
if (mounted) UpdateService().checkForUpdates(context);
|
||||
|
||||
ref.read(rpcServerStateProvider.notifier).start();
|
||||
TrayService.instance.initialize(this);
|
||||
|
||||
final initialUri = await appLinks.getLatestLink();
|
||||
if (initialUri != null && context.mounted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_handleDeepLink(initialUri, ref);
|
||||
});
|
||||
}
|
||||
ref.read(rpcServerStateProvider.notifier).start();
|
||||
|
||||
appLinksSubs = appLinks.uriLinkStream.listen((uri) {
|
||||
_handleDeepLink(uri, ref);
|
||||
final initialUrl = await protocolHandler.getInitialUrl();
|
||||
if (initialUrl != null && mounted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_handleDeepLink(Uri.parse(initialUrl), ref);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () {
|
||||
ref.read(rpcServerProvider).stop();
|
||||
TrayService.instance.dispose(this);
|
||||
ntySubs?.cancel();
|
||||
appLinksSubs?.cancel();
|
||||
};
|
||||
}, const []);
|
||||
@override
|
||||
void dispose() {
|
||||
protocolHandler.removeListener(this);
|
||||
ref.read(rpcServerProvider).stop();
|
||||
TrayService.instance.dispose(this);
|
||||
ntySubs?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final wsNotifier = ref.watch(websocketStateProvider.notifier);
|
||||
final websocketState = ref.watch(websocketStateProvider);
|
||||
|
||||
final networkStateShowing = useState(false);
|
||||
|
||||
if (websocketState == WebSocketState.duplicateDevice()) {
|
||||
if (!networkStateShowing.value) {
|
||||
if (!networkStateShowing) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
networkStateShowing.value = true;
|
||||
setState(() => networkStateShowing = true);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@@ -73,12 +78,17 @@ class AppWrapper extends HookConsumerWidget with TrayListener {
|
||||
builder:
|
||||
(context) =>
|
||||
NetworkStatusSheet(onReconnect: () => wsNotifier.connect()),
|
||||
).then((_) => networkStateShowing.value = false);
|
||||
).then((_) => setState(() => networkStateShowing = false));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return TourTriggerWidget(key: UniqueKey(), child: child);
|
||||
return TourTriggerWidget(key: UniqueKey(), child: widget.child);
|
||||
}
|
||||
|
||||
@override
|
||||
void onProtocolUrlReceived(String url) {
|
||||
_handleDeepLink(Uri.parse(url), ref);
|
||||
}
|
||||
|
||||
void _trayIconPrimaryAction() {
|
||||
@@ -106,13 +116,17 @@ class AppWrapper extends HookConsumerWidget with TrayListener {
|
||||
|
||||
void _handleDeepLink(Uri uri, WidgetRef ref) {
|
||||
final router = ref.read(routerProvider);
|
||||
String path = '/${uri.path}';
|
||||
String path = '/${uri.host}${uri.path}';
|
||||
if (uri.queryParameters.isNotEmpty) {
|
||||
path =
|
||||
Uri.parse(
|
||||
path,
|
||||
).replace(queryParameters: uri.queryParameters).toString();
|
||||
}
|
||||
router.go(path);
|
||||
router.push(path);
|
||||
if (!kIsWeb &&
|
||||
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||
windowManager.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class ChatInput extends HookConsumerWidget {
|
||||
final Function(int) onDeleteAttachment;
|
||||
final Function(int, int) onMoveAttachment;
|
||||
final Function(List<UniversalFile>) onAttachmentsChanged;
|
||||
final Map<String, Map<int, double>> attachmentProgress;
|
||||
final Map<String, Map<int, double?>> attachmentProgress;
|
||||
|
||||
const ChatInput({
|
||||
super.key,
|
||||
|
||||
@@ -40,7 +40,7 @@ class MessageItem extends HookConsumerWidget {
|
||||
final LocalChatMessage message;
|
||||
final bool isCurrentUser;
|
||||
final Function(String action)? onAction;
|
||||
final Map<int, double>? progress;
|
||||
final Map<int, double?>? progress;
|
||||
final bool showAvatar;
|
||||
final Function(String messageId) onJump;
|
||||
final bool isSelectionMode;
|
||||
@@ -689,7 +689,7 @@ class MessageHoverActionMenu extends StatelessWidget {
|
||||
class MessageItemDisplayBubble extends HookConsumerWidget {
|
||||
final LocalChatMessage message;
|
||||
final bool isCurrentUser;
|
||||
final Map<int, double>? progress;
|
||||
final Map<int, double?>? progress;
|
||||
final bool showAvatar;
|
||||
final Function(String messageId) onJump;
|
||||
final String? translatedText;
|
||||
@@ -821,7 +821,7 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
|
||||
class MessageItemDisplayIRC extends HookConsumerWidget {
|
||||
final LocalChatMessage message;
|
||||
final bool isCurrentUser;
|
||||
final Map<int, double>? progress;
|
||||
final Map<int, double?>? progress;
|
||||
final bool showAvatar;
|
||||
final Function(String messageId) onJump;
|
||||
final String? translatedText;
|
||||
@@ -949,7 +949,7 @@ class MessageItemDisplayIRC extends HookConsumerWidget {
|
||||
class MessageItemDisplayDiscord extends HookConsumerWidget {
|
||||
final LocalChatMessage message;
|
||||
final bool isCurrentUser;
|
||||
final Map<int, double>? progress;
|
||||
final Map<int, double?>? progress;
|
||||
final bool showAvatar;
|
||||
final Function(String messageId) onJump;
|
||||
final String? translatedText;
|
||||
@@ -1238,7 +1238,7 @@ class MessageQuoteWidget extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
class FileUploadProgressWidget extends StatelessWidget {
|
||||
final Map<int, double>? progress;
|
||||
final Map<int, double?>? progress;
|
||||
final Color textColor;
|
||||
final bool hasContent;
|
||||
|
||||
@@ -1266,7 +1266,9 @@ class FileUploadProgressWidget extends StatelessWidget {
|
||||
'fileUploadingProgress'.tr(
|
||||
args: [
|
||||
(entry.key + 1).toString(),
|
||||
(entry.value * 100).toStringAsFixed(1),
|
||||
entry.value != null
|
||||
? (entry.value! * 100).toStringAsFixed(1)
|
||||
: '0.0',
|
||||
],
|
||||
),
|
||||
style: TextStyle(
|
||||
|
||||
@@ -104,9 +104,7 @@ class CheckInWidget extends HookConsumerWidget {
|
||||
} catch (err) {
|
||||
if (err is DioException) {
|
||||
if (err.response?.statusCode == 423 && context.mounted) {
|
||||
final captchaTk = await Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
|
||||
final captchaTk = await CaptchaScreen.show(context);
|
||||
if (captchaTk == null) return;
|
||||
return await checkIn(captchatTk: captchaTk);
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ class AttachmentPreview extends HookConsumerWidget {
|
||||
children: [
|
||||
if (progress != null)
|
||||
Text(
|
||||
'${progress!.toStringAsFixed(2)}%',
|
||||
'${(progress! * 100).toStringAsFixed(2)}%',
|
||||
style: TextStyle(color: Colors.white),
|
||||
)
|
||||
else
|
||||
@@ -411,10 +411,7 @@ class AttachmentPreview extends HookConsumerWidget {
|
||||
),
|
||||
Gap(6),
|
||||
Center(
|
||||
child: LinearProgressIndicator(
|
||||
value:
|
||||
progress != null ? progress! / 100.0 : null,
|
||||
),
|
||||
child: LinearProgressIndicator(value: progress),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/attachment_preview.dart';
|
||||
@@ -61,7 +60,7 @@ class CloudFilePicker extends HookConsumerWidget {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
fileData: file,
|
||||
client: ref.read(apiClientProvider),
|
||||
ref: ref,
|
||||
onProgress: (progress, _) {
|
||||
uploadProgress.value = progress;
|
||||
},
|
||||
@@ -112,23 +111,28 @@ class CloudFilePicker extends HookConsumerWidget {
|
||||
|
||||
void pickImage() async {
|
||||
showLoadingModal(context);
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
allowMultiple: allowMultiple,
|
||||
type: FileType.image,
|
||||
);
|
||||
if (result == null || result.files.isEmpty) {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
List<XFile> results;
|
||||
if (allowMultiple) {
|
||||
results = await picker.pickMultiImage();
|
||||
} else {
|
||||
final XFile? result = await picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
);
|
||||
results = result != null ? [result] : [];
|
||||
}
|
||||
if (results.isEmpty) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final newFiles =
|
||||
result.files.map((e) {
|
||||
final xfile =
|
||||
e.bytes != null
|
||||
? XFile.fromData(e.bytes!, name: e.name)
|
||||
: XFile(e.path!);
|
||||
return UniversalFile(data: xfile, type: UniversalFileType.image);
|
||||
}).toList();
|
||||
results
|
||||
.map(
|
||||
(xfile) =>
|
||||
UniversalFile(data: xfile, type: UniversalFileType.image),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (!allowMultiple) {
|
||||
files.value = newFiles;
|
||||
|
||||
@@ -13,8 +13,8 @@ import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class FileInfoSheet extends StatelessWidget {
|
||||
final SnCloudFile item;
|
||||
|
||||
const FileInfoSheet({super.key, required this.item});
|
||||
final VoidCallback? onClose;
|
||||
const FileInfoSheet({super.key, required this.item, this.onClose});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -22,6 +22,7 @@ class FileInfoSheet extends StatelessWidget {
|
||||
final exifData = item.fileMeta?['exif'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return SheetScaffold(
|
||||
onClose: onClose,
|
||||
titleText: 'fileInfoTitle'.tr(),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
|
||||
@@ -8,6 +8,7 @@ class SheetScaffold extends StatelessWidget {
|
||||
final Widget child;
|
||||
final double heightFactor;
|
||||
final double? height;
|
||||
final VoidCallback? onClose;
|
||||
const SheetScaffold({
|
||||
super.key,
|
||||
this.title,
|
||||
@@ -16,6 +17,7 @@ class SheetScaffold extends StatelessWidget {
|
||||
this.actions = const [],
|
||||
this.heightFactor = 0.8,
|
||||
this.height,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -50,7 +52,11 @@ class SheetScaffold extends StatelessWidget {
|
||||
...actions,
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed:
|
||||
() =>
|
||||
onClose != null
|
||||
? onClose?.call()
|
||||
: Navigator.pop(context),
|
||||
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
|
||||
),
|
||||
],
|
||||
|
||||
600
lib/widgets/file_list_view.dart
Normal file
600
lib/widgets/file_list_view.dart
Normal file
@@ -0,0 +1,600 @@
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file_list_item.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/file_list.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/utils/format.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
enum FileListMode { normal, unindexed }
|
||||
|
||||
class FileListView extends HookConsumerWidget {
|
||||
final Map<String, dynamic>? usage;
|
||||
final Map<String, dynamic>? quota;
|
||||
final ValueNotifier<String> currentPath;
|
||||
final VoidCallback onPickAndUpload;
|
||||
final Function(BuildContext, ValueNotifier<String>) onShowCreateDirectory;
|
||||
final ValueNotifier<FileListMode> mode;
|
||||
|
||||
const FileListView({
|
||||
required this.usage,
|
||||
required this.quota,
|
||||
required this.currentPath,
|
||||
required this.onPickAndUpload,
|
||||
required this.onShowCreateDirectory,
|
||||
required this.mode,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final dragging = useState(false);
|
||||
|
||||
useEffect(() {
|
||||
if (mode.value == FileListMode.normal) {
|
||||
final notifier = ref.read(cloudFileListNotifierProvider.notifier);
|
||||
notifier.setPath(currentPath.value);
|
||||
}
|
||||
return null;
|
||||
}, [currentPath.value, mode.value]);
|
||||
|
||||
if (usage == null) return const SizedBox.shrink();
|
||||
|
||||
final bodyWidget = switch (mode.value) {
|
||||
FileListMode.unindexed => PagingHelperSliverView(
|
||||
provider: unindexedFileListNotifierProvider,
|
||||
futureRefreshable: unindexedFileListNotifierProvider.future,
|
||||
notifierRefreshable: unindexedFileListNotifierProvider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) =>
|
||||
data.items.isEmpty
|
||||
? SliverToBoxAdapter(
|
||||
child: _buildEmptyUnindexedFilesHint(ref),
|
||||
)
|
||||
: SliverList.builder(
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
|
||||
final item = data.items[index];
|
||||
return item.map(
|
||||
file: (fileItem) {
|
||||
// This should not happen in unindexed mode
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
folder: (folderItem) {
|
||||
// This should not happen in unindexed mode
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
unindexedFile: (unindexedFileItem) {
|
||||
final file = unindexedFileItem.file;
|
||||
final itemType =
|
||||
file.mimeType?.split('/').firstOrNull;
|
||||
return ListTile(
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: switch (itemType) {
|
||||
'image' => CloudImageWidget(file: file),
|
||||
'audio' =>
|
||||
const Icon(
|
||||
Symbols.audio_file,
|
||||
fill: 1,
|
||||
).center(),
|
||||
'video' =>
|
||||
const Icon(
|
||||
Symbols.video_file,
|
||||
fill: 1,
|
||||
).center(),
|
||||
_ =>
|
||||
const Icon(
|
||||
Symbols.body_system,
|
||||
fill: 1,
|
||||
).center(),
|
||||
},
|
||||
),
|
||||
),
|
||||
title:
|
||||
file.name.isEmpty
|
||||
? Text('untitled').tr().italic()
|
||||
: Text(
|
||||
file.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(formatFileSize(file.size)),
|
||||
onTap: () {
|
||||
context.push('/files/${file.id}', extra: file);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (context.mounted) {
|
||||
showLoadingModal(context);
|
||||
}
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete(
|
||||
'/drive/files/${file.id}',
|
||||
);
|
||||
ref.invalidate(
|
||||
unindexedFileListNotifierProvider,
|
||||
);
|
||||
} catch (e) {
|
||||
showSnackBar('failedToDeleteFile'.tr());
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
_ => PagingHelperSliverView(
|
||||
provider: cloudFileListNotifierProvider,
|
||||
futureRefreshable: cloudFileListNotifierProvider.future,
|
||||
notifierRefreshable: cloudFileListNotifierProvider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) =>
|
||||
data.items.isEmpty
|
||||
? SliverToBoxAdapter(
|
||||
child: _buildEmptyDirectoryHint(ref, currentPath),
|
||||
)
|
||||
: SliverList.builder(
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
|
||||
final item = data.items[index];
|
||||
return item.map(
|
||||
file: (fileItem) {
|
||||
final file = fileItem.fileIndex.file;
|
||||
final itemType =
|
||||
file.mimeType?.split('/').firstOrNull;
|
||||
return ListTile(
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: switch (itemType) {
|
||||
'image' => CloudImageWidget(file: file),
|
||||
'audio' =>
|
||||
const Icon(
|
||||
Symbols.audio_file,
|
||||
fill: 1,
|
||||
).center(),
|
||||
'video' =>
|
||||
const Icon(
|
||||
Symbols.video_file,
|
||||
fill: 1,
|
||||
).center(),
|
||||
_ =>
|
||||
const Icon(
|
||||
Symbols.body_system,
|
||||
fill: 1,
|
||||
).center(),
|
||||
},
|
||||
),
|
||||
),
|
||||
title:
|
||||
file.name.isEmpty
|
||||
? Text('untitled').tr().italic()
|
||||
: Text(
|
||||
file.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(formatFileSize(file.size)),
|
||||
onTap: () {
|
||||
context.push(
|
||||
'/files/${fileItem.fileIndex.id}',
|
||||
extra: file,
|
||||
);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (context.mounted) {
|
||||
showLoadingModal(context);
|
||||
}
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete(
|
||||
'/drive/index/remove/${fileItem.fileIndex.id}',
|
||||
);
|
||||
ref.invalidate(
|
||||
cloudFileListNotifierProvider,
|
||||
);
|
||||
} catch (e) {
|
||||
showSnackBar('failedToDeleteFile'.tr());
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
folder:
|
||||
(folderItem) => ListTile(
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child:
|
||||
const Icon(
|
||||
Symbols.folder,
|
||||
fill: 1,
|
||||
).center(),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
folderItem.folderName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: const Text('Folder'),
|
||||
onTap: () {
|
||||
// Navigate to folder
|
||||
final newPath =
|
||||
currentPath.value == '/'
|
||||
? '/${folderItem.folderName}'
|
||||
: '${currentPath.value}/${folderItem.folderName}';
|
||||
currentPath.value = newPath;
|
||||
},
|
||||
),
|
||||
unindexedFile: (unindexedFileItem) {
|
||||
// This should not happen in normal mode
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
return DropTarget(
|
||||
onDragDone: (details) async {
|
||||
dragging.value = false;
|
||||
// Handle file upload
|
||||
for (final file in details.files) {
|
||||
final universalFile = UniversalFile(
|
||||
data: file,
|
||||
type: UniversalFileType.file,
|
||||
displayName: file.name,
|
||||
);
|
||||
|
||||
final completer = FileUploader.createCloudFile(
|
||||
fileData: universalFile,
|
||||
ref: ref,
|
||||
path: currentPath.value,
|
||||
onProgress: (progress, _) {
|
||||
// Progress is handled by the upload tasks system
|
||||
if (progress != null) {
|
||||
debugPrint('Upload progress: ${(progress * 100).toInt()}%');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
completer.future
|
||||
.then((uploadedFile) {
|
||||
if (uploadedFile != null) {
|
||||
ref.invalidate(cloudFileListNotifierProvider);
|
||||
}
|
||||
})
|
||||
.catchError((error) {
|
||||
showSnackBar('Failed to upload file: $error');
|
||||
});
|
||||
}
|
||||
},
|
||||
onDragEntered: (details) {
|
||||
dragging.value = true;
|
||||
},
|
||||
onDragExited: (details) {
|
||||
dragging.value = false;
|
||||
},
|
||||
child: Container(
|
||||
color:
|
||||
dragging.value
|
||||
? Theme.of(context).primaryColor.withOpacity(0.1)
|
||||
: null,
|
||||
child: Column(
|
||||
children: [
|
||||
const Gap(8),
|
||||
_buildPathNavigation(ref, currentPath),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
bodyWidget,
|
||||
const SliverGap(12),
|
||||
if (mode.value == FileListMode.normal &&
|
||||
currentPath.value == '/')
|
||||
SliverToBoxAdapter(child: _buildUnindexedFilesEntry(ref)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPathNavigation(
|
||||
WidgetRef ref,
|
||||
ValueNotifier<String> currentPath,
|
||||
) {
|
||||
Widget pathContent;
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
pathContent = Row(
|
||||
children: [
|
||||
Text(
|
||||
'Unindexed Files',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (currentPath.value == '/') {
|
||||
pathContent = Text(
|
||||
'Root Directory',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
);
|
||||
} else {
|
||||
final pathParts =
|
||||
currentPath.value
|
||||
.split('/')
|
||||
.where((part) => part.isNotEmpty)
|
||||
.toList();
|
||||
final breadcrumbs = <Widget>[];
|
||||
|
||||
// Add root
|
||||
breadcrumbs.add(
|
||||
InkWell(onTap: () => currentPath.value = '/', child: Text('Root')),
|
||||
);
|
||||
|
||||
// Add path parts
|
||||
String currentPathBuilder = '';
|
||||
for (int i = 0; i < pathParts.length; i++) {
|
||||
currentPathBuilder += '/${pathParts[i]}';
|
||||
final path = currentPathBuilder;
|
||||
|
||||
breadcrumbs.add(const Text(' / '));
|
||||
if (i == pathParts.length - 1) {
|
||||
// Current directory
|
||||
breadcrumbs.add(
|
||||
Text(pathParts[i], style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
);
|
||||
} else {
|
||||
// Clickable parent directory
|
||||
breadcrumbs.add(
|
||||
InkWell(
|
||||
onTap: () => currentPath.value = path,
|
||||
child: Text(pathParts[i]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pathContent = Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: breadcrumbs,
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
mode.value == FileListMode.unindexed
|
||||
? Symbols.inventory_2
|
||||
: Symbols.folder,
|
||||
),
|
||||
onPressed: () {
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
mode.value = FileListMode.normal;
|
||||
}
|
||||
currentPath.value = '/';
|
||||
},
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(child: pathContent),
|
||||
if (mode.value == FileListMode.normal) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.create_new_folder),
|
||||
onPressed:
|
||||
() => onShowCreateDirectory(ref.context, currentPath),
|
||||
tooltip: 'Create Directory',
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.upload_file),
|
||||
onPressed: onPickAndUpload,
|
||||
tooltip: 'Upload File',
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
).padding(horizontal: 8),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUnindexedFilesEntry(WidgetRef ref) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(ref.context).colorScheme.outline),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.inventory_2).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
const Text('Unindexed Files').bold(),
|
||||
const Spacer(),
|
||||
const Icon(Symbols.chevron_right).padding(horizontal: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
mode.value = FileListMode.unindexed;
|
||||
currentPath.value = '/';
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyDirectoryHint(
|
||||
WidgetRef ref,
|
||||
ValueNotifier<String> currentPath,
|
||||
) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.fromLTRB(12, 0, 12, 16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Symbols.folder_off, size: 64, color: Colors.grey),
|
||||
const Gap(16),
|
||||
Text(
|
||||
'This directory is empty',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(ref.context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Upload files or create subdirectories to populate this path.\n'
|
||||
'Directories are created implicitly when you upload files to them.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
ref.context,
|
||||
).textTheme.bodyMedium?.color?.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: onPickAndUpload,
|
||||
icon: const Icon(Symbols.upload_file),
|
||||
label: const Text('Upload Files'),
|
||||
),
|
||||
const Gap(12),
|
||||
OutlinedButton.icon(
|
||||
onPressed:
|
||||
() => onShowCreateDirectory(ref.context, currentPath),
|
||||
icon: const Icon(Symbols.create_new_folder),
|
||||
label: const Text('Create Directory'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyUnindexedFilesHint(WidgetRef ref) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Symbols.inventory_2, size: 64, color: Colors.grey),
|
||||
const Gap(16),
|
||||
Text(
|
||||
'No unindexed files',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(ref.context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'All files have been assigned to paths.\n'
|
||||
'Files without paths will appear here.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
ref.context,
|
||||
).textTheme.bodyMedium?.color?.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ class ArticleComposeAttachments extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
children: [
|
||||
ValueListenableBuilder<Map<int, double>>(
|
||||
ValueListenableBuilder<Map<int, double?>>(
|
||||
valueListenable: state.attachmentProgress,
|
||||
builder: (context, progressMap, _) {
|
||||
return Wrap(
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
@@ -330,7 +329,13 @@ class PostComposeCard extends HookConsumerWidget {
|
||||
if (isContained) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
context.pushNamed('creatorNew').then((value) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder:
|
||||
(context) => const NewPublisherScreen(),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
composeState.currentPublisher.value =
|
||||
value as SnPublisher;
|
||||
@@ -368,9 +373,14 @@ class PostComposeCard extends HookConsumerWidget {
|
||||
if (isContained) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
context.pushNamed('creatorNew').then((
|
||||
value,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder:
|
||||
(context) =>
|
||||
const NewPublisherScreen(),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
composeState.currentPublisher.value =
|
||||
value as SnPublisher;
|
||||
|
||||
@@ -33,7 +33,7 @@ class ComposeState {
|
||||
final TextEditingController slugController;
|
||||
final ValueNotifier<int> visibility;
|
||||
final ValueNotifier<List<UniversalFile>> attachments;
|
||||
final ValueNotifier<Map<int, double>> attachmentProgress;
|
||||
final ValueNotifier<Map<int, double?>> attachmentProgress;
|
||||
final ValueNotifier<SnPublisher?> currentPublisher;
|
||||
final ValueNotifier<bool> submitting;
|
||||
final ValueNotifier<List<SnPostCategory>> categories;
|
||||
@@ -123,7 +123,7 @@ class ComposeLogic {
|
||||
slugController: TextEditingController(text: originalPost?.slug),
|
||||
visibility: ValueNotifier<int>(originalPost?.visibility ?? 0),
|
||||
submitting: ValueNotifier<bool>(false),
|
||||
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
||||
attachmentProgress: ValueNotifier<Map<int, double?>>({}),
|
||||
currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
|
||||
tags: ValueNotifier<List<String>>(tags),
|
||||
categories: ValueNotifier<List<SnPostCategory>>(categories),
|
||||
@@ -149,7 +149,7 @@ class ComposeLogic {
|
||||
slugController: TextEditingController(text: draft.slug),
|
||||
visibility: ValueNotifier<int>(draft.visibility),
|
||||
submitting: ValueNotifier<bool>(false),
|
||||
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
||||
attachmentProgress: ValueNotifier<Map<int, double?>>({}),
|
||||
currentPublisher: ValueNotifier<SnPublisher?>(null),
|
||||
tags: ValueNotifier<List<String>>(tags),
|
||||
categories: ValueNotifier<List<SnPostCategory>>(draft.categories),
|
||||
@@ -180,7 +180,7 @@ class ComposeLogic {
|
||||
try {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
client: ref.read(apiClientProvider),
|
||||
ref: ref,
|
||||
fileData: attachment,
|
||||
).future;
|
||||
if (cloudFile != null) {
|
||||
@@ -402,16 +402,13 @@ class ComposeLogic {
|
||||
}
|
||||
|
||||
static Future<void> pickPhotoMedia(WidgetRef ref, ComposeState state) async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.image,
|
||||
allowMultiple: true,
|
||||
allowCompression: false,
|
||||
);
|
||||
if (result == null || result.count == 0) return;
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final List<XFile> results = await picker.pickMultiImage();
|
||||
if (results.isEmpty) return;
|
||||
state.attachments.value = [
|
||||
...state.attachments.value,
|
||||
...result.files.map(
|
||||
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image),
|
||||
...results.map(
|
||||
(xfile) => UniversalFile(data: xfile, type: UniversalFileType.image),
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -503,7 +500,7 @@ class ComposeLogic {
|
||||
try {
|
||||
state.attachmentProgress.value = {
|
||||
...state.attachmentProgress.value,
|
||||
index: 0,
|
||||
index: 0.0,
|
||||
};
|
||||
|
||||
SnCloudFile? cloudFile;
|
||||
@@ -513,7 +510,7 @@ class ComposeLogic {
|
||||
|
||||
cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
client: ref.read(apiClientProvider),
|
||||
ref: ref,
|
||||
fileData: attachment,
|
||||
poolId: poolId ?? selectedPoolId,
|
||||
mode:
|
||||
@@ -523,7 +520,7 @@ class ComposeLogic {
|
||||
onProgress: (progress, _) {
|
||||
state.attachmentProgress.value = {
|
||||
...state.attachmentProgress.value,
|
||||
index: progress,
|
||||
index: progress ?? 0.0,
|
||||
};
|
||||
},
|
||||
).future;
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'post_list.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$postListNotifierHash() => r'8241120dc3c2004387c6cf881e5cb9224cbd3a97';
|
||||
String _$postListNotifierHash() => r'bfc3d652dffc5ff3a94a6c3d04aac65354fe63b5';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'dart:math' as math;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/screens/creators/publishers_form.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
@@ -43,9 +42,13 @@ class PublisherModal extends HookConsumerWidget {
|
||||
const Gap(12),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.pushNamed('creatorNew').then((
|
||||
value,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) =>
|
||||
const NewPublisherScreen(),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(
|
||||
publishersManagedProvider,
|
||||
|
||||
@@ -241,12 +241,13 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
|
||||
final file = universalFiles[idx];
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
client: apiClient,
|
||||
ref: ref,
|
||||
fileData: file,
|
||||
onProgress: (progress, _) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_fileUploadProgress[messageId]?[idx] = progress;
|
||||
_fileUploadProgress[messageId]?[idx] =
|
||||
progress ?? 0.0;
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -306,7 +307,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
|
||||
|
||||
// Navigate to chat if requested
|
||||
if (shouldNavigate == true && mounted) {
|
||||
context.push('/sphere/chat/${chatRoom.id}');
|
||||
context.push('/chat/${chatRoom.id}');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
756
lib/widgets/upload_overlay.dart
Normal file
756
lib/widgets/upload_overlay.dart
Normal file
@@ -0,0 +1,756 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/drive_task.dart';
|
||||
import 'package:island/pods/upload_tasks.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class UploadOverlay extends HookConsumerWidget {
|
||||
const UploadOverlay({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final uploadTasks = ref.watch(uploadTasksProvider);
|
||||
final activeTasks =
|
||||
uploadTasks
|
||||
.where(
|
||||
(task) =>
|
||||
task.status == DriveTaskStatus.pending ||
|
||||
task.status == DriveTaskStatus.inProgress ||
|
||||
task.status == DriveTaskStatus.paused ||
|
||||
task.status == DriveTaskStatus.completed,
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Newest first
|
||||
|
||||
final isVisible = activeTasks.isNotEmpty;
|
||||
final slideController = useAnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
final slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, 1), // Start from below the screen
|
||||
end: Offset.zero, // End at normal position
|
||||
).animate(CurvedAnimation(parent: slideController, curve: Curves.easeOut));
|
||||
|
||||
// Animate when visibility changes
|
||||
useEffect(() {
|
||||
if (isVisible) {
|
||||
slideController.forward();
|
||||
} else {
|
||||
slideController.reverse();
|
||||
}
|
||||
return null;
|
||||
}, [isVisible]);
|
||||
|
||||
if (!isVisible && slideController.status == AnimationStatus.dismissed) {
|
||||
// If not visible and animation is complete (back to start), don't show anything
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final isDesktop = isWideScreen(context);
|
||||
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
left: isDesktop ? null : 0,
|
||||
right: isDesktop ? 24 : 0,
|
||||
child: SlideTransition(
|
||||
position: slideAnimation,
|
||||
child: _UploadOverlayContent(
|
||||
activeTasks: activeTasks,
|
||||
).padding(bottom: 16 + MediaQuery.of(context).padding.bottom),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UploadOverlayContent extends HookConsumerWidget {
|
||||
final List<DriveTask> activeTasks;
|
||||
|
||||
const _UploadOverlayContent({required this.activeTasks});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isExpanded = useState(false);
|
||||
final animationController = useAnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
initialValue: 0.0,
|
||||
);
|
||||
final heightAnimation = useAnimation(
|
||||
Tween<double>(begin: 60, end: 400).animate(
|
||||
CurvedAnimation(parent: animationController, curve: Curves.easeInOut),
|
||||
),
|
||||
);
|
||||
final opacityAnimation = useAnimation(
|
||||
CurvedAnimation(parent: animationController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
if (isExpanded.value) {
|
||||
animationController.forward();
|
||||
} else {
|
||||
animationController.reverse();
|
||||
}
|
||||
return null;
|
||||
}, [isExpanded.value]);
|
||||
|
||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: isMobile ? 16 : 24,
|
||||
left: isMobile ? 16 : 0,
|
||||
right: isMobile ? 16 : 24,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () => isExpanded.value = !isExpanded.value,
|
||||
child: AnimatedBuilder(
|
||||
animation: animationController,
|
||||
builder: (context, child) {
|
||||
return Material(
|
||||
elevation: 8 + (opacityAnimation * 4),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
width: isMobile ? MediaQuery.of(context).size.width - 32 : 320,
|
||||
height: heightAnimation,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Collapsed Header
|
||||
Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Upload icon with animation
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
key: ValueKey(isExpanded.value),
|
||||
isExpanded.value
|
||||
? Symbols.list_rounded
|
||||
: _getOverallStatusIcon(activeTasks),
|
||||
size: 24,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Title and count
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isExpanded.value
|
||||
? 'uploadTasks'.tr()
|
||||
: _getOverallStatusText(activeTasks),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (!isExpanded.value &&
|
||||
activeTasks.isNotEmpty)
|
||||
Text(
|
||||
_getOverallProgressText(activeTasks),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Progress indicator (collapsed)
|
||||
if (!isExpanded.value)
|
||||
SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: CircularProgressIndicator(
|
||||
value: _getOverallProgress(activeTasks),
|
||||
strokeWidth: 3,
|
||||
backgroundColor:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
),
|
||||
|
||||
// Expand/collapse button
|
||||
IconButton(
|
||||
icon: AnimatedRotation(
|
||||
turns: opacityAnimation * 0.5,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Icon(
|
||||
isExpanded.value
|
||||
? Symbols.expand_more
|
||||
: Symbols.chevron_right,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
onPressed:
|
||||
() => isExpanded.value = !isExpanded.value,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Expanded content
|
||||
if (isExpanded.value)
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width:
|
||||
1 /
|
||||
MediaQuery.of(context).devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
// Clear completed tasks button
|
||||
if (_hasCompletedTasks(activeTasks))
|
||||
SliverToBoxAdapter(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Clear Completed'),
|
||||
leading: Icon(
|
||||
Symbols.clear_all,
|
||||
size: 18,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.clearCompletedTasks();
|
||||
},
|
||||
tileColor:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
),
|
||||
|
||||
// Task list
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate((
|
||||
context,
|
||||
index,
|
||||
) {
|
||||
final task = activeTasks[index];
|
||||
return AnimatedOpacity(
|
||||
opacity: opacityAnimation,
|
||||
duration: const Duration(
|
||||
milliseconds: 150,
|
||||
),
|
||||
child: UploadTaskTile(task: task),
|
||||
);
|
||||
}, childCount: activeTasks.length),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _getOverallProgress(List<DriveTask> tasks) {
|
||||
if (tasks.isEmpty) return 0.0;
|
||||
final totalProgress = tasks.fold<double>(
|
||||
0.0,
|
||||
(sum, task) =>
|
||||
sum +
|
||||
(task.status == DriveTaskStatus.inProgress
|
||||
? task.progress
|
||||
: task.status == DriveTaskStatus.completed
|
||||
? 1
|
||||
: 0),
|
||||
);
|
||||
return totalProgress / tasks.length;
|
||||
}
|
||||
|
||||
String _getOverallProgressText(List<DriveTask> tasks) {
|
||||
final overallProgress = _getOverallProgress(tasks);
|
||||
return '${(overallProgress * 100).toStringAsFixed(0)}%';
|
||||
}
|
||||
|
||||
IconData _getOverallStatusIcon(List<DriveTask> tasks) {
|
||||
if (tasks.isEmpty) return Symbols.upload;
|
||||
|
||||
final hasInProgress = tasks.any(
|
||||
(task) => task.status == DriveTaskStatus.inProgress,
|
||||
);
|
||||
final hasPending = tasks.any(
|
||||
(task) => task.status == DriveTaskStatus.pending,
|
||||
);
|
||||
final hasPaused = tasks.any(
|
||||
(task) => task.status == DriveTaskStatus.paused,
|
||||
);
|
||||
final hasFailed = tasks.any(
|
||||
(task) =>
|
||||
task.status == DriveTaskStatus.failed ||
|
||||
task.status == DriveTaskStatus.cancelled ||
|
||||
task.status == DriveTaskStatus.expired,
|
||||
);
|
||||
final hasCompleted = tasks.any(
|
||||
(task) => task.status == DriveTaskStatus.completed,
|
||||
);
|
||||
|
||||
// Priority order: in progress > pending > paused > failed > completed
|
||||
if (hasInProgress) {
|
||||
return Symbols.upload;
|
||||
} else if (hasPending) {
|
||||
return Symbols.schedule;
|
||||
} else if (hasPaused) {
|
||||
return Symbols.pause_circle;
|
||||
} else if (hasFailed) {
|
||||
return Symbols.error;
|
||||
} else if (hasCompleted) {
|
||||
return Symbols.check_circle;
|
||||
} else {
|
||||
return Symbols.upload;
|
||||
}
|
||||
}
|
||||
|
||||
String _getOverallStatusText(List<DriveTask> tasks) {
|
||||
if (tasks.isEmpty) return '0 tasks';
|
||||
|
||||
final hasInProgress = tasks.any(
|
||||
(task) => task.status == DriveTaskStatus.inProgress,
|
||||
);
|
||||
final hasPending = tasks.any(
|
||||
(task) => task.status == DriveTaskStatus.pending,
|
||||
);
|
||||
final hasPaused = tasks.any(
|
||||
(task) => task.status == DriveTaskStatus.paused,
|
||||
);
|
||||
final hasFailed = tasks.any(
|
||||
(task) =>
|
||||
task.status == DriveTaskStatus.failed ||
|
||||
task.status == DriveTaskStatus.cancelled ||
|
||||
task.status == DriveTaskStatus.expired,
|
||||
);
|
||||
final hasCompleted = tasks.any(
|
||||
(task) => task.status == DriveTaskStatus.completed,
|
||||
);
|
||||
|
||||
// Priority order: in progress > pending > paused > failed > completed
|
||||
if (hasInProgress) {
|
||||
return '${tasks.length} ${'uploading'.tr()}';
|
||||
} else if (hasPending) {
|
||||
return '${tasks.length} ${'pending'.tr()}';
|
||||
} else if (hasPaused) {
|
||||
return '${tasks.length} ${'paused'.tr()}';
|
||||
} else if (hasFailed) {
|
||||
return '${tasks.length} ${'failed'.tr()}';
|
||||
} else if (hasCompleted) {
|
||||
return '${tasks.length} ${'completed'.tr()}';
|
||||
} else {
|
||||
return '${tasks.length} ${'tasks'.tr()}';
|
||||
}
|
||||
}
|
||||
|
||||
bool _hasCompletedTasks(List<DriveTask> tasks) {
|
||||
return tasks.any(
|
||||
(task) =>
|
||||
task.status == DriveTaskStatus.completed ||
|
||||
task.status == DriveTaskStatus.failed ||
|
||||
task.status == DriveTaskStatus.cancelled ||
|
||||
task.status == DriveTaskStatus.expired,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UploadTaskTile extends StatefulWidget {
|
||||
final DriveTask task;
|
||||
|
||||
const UploadTaskTile({super.key, required this.task});
|
||||
|
||||
@override
|
||||
State<UploadTaskTile> createState() => _UploadTaskTileState();
|
||||
}
|
||||
|
||||
class _UploadTaskTileState extends State<UploadTaskTile>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _rotationController;
|
||||
late Animation<double> _rotationAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_rotationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
_rotationAnimation = Tween<double>(begin: 0.0, end: 0.5).animate(
|
||||
CurvedAnimation(parent: _rotationController, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_rotationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExpansionTile(
|
||||
leading: _buildStatusIcon(context),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.task.fileName.isEmpty
|
||||
? 'untitled'.tr()
|
||||
: widget.task.fileName,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_formatFileSize(widget.task.fileSize),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: CircularProgressIndicator(
|
||||
value: widget.task.progress,
|
||||
strokeWidth: 2.5,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
AnimatedBuilder(
|
||||
animation: _rotationAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.rotate(
|
||||
angle: _rotationAnimation.value * math.pi,
|
||||
child: Icon(
|
||||
Symbols.expand_more,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
onExpansionChanged: (expanded) {
|
||||
if (expanded) {
|
||||
_rotationController.forward();
|
||||
} else {
|
||||
_rotationController.reverse();
|
||||
}
|
||||
},
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
|
||||
child: _buildExpandedDetails(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusIcon(BuildContext context) {
|
||||
IconData icon;
|
||||
Color color;
|
||||
|
||||
switch (widget.task.status) {
|
||||
case DriveTaskStatus.pending:
|
||||
icon = Symbols.schedule;
|
||||
color = Theme.of(context).colorScheme.secondary;
|
||||
break;
|
||||
case DriveTaskStatus.inProgress:
|
||||
icon = Symbols.upload;
|
||||
color = Theme.of(context).colorScheme.primary;
|
||||
break;
|
||||
case DriveTaskStatus.paused:
|
||||
icon = Symbols.pause_circle;
|
||||
color = Theme.of(context).colorScheme.tertiary;
|
||||
break;
|
||||
case DriveTaskStatus.completed:
|
||||
icon = Symbols.check_circle;
|
||||
color = Colors.green;
|
||||
break;
|
||||
case DriveTaskStatus.failed:
|
||||
icon = Symbols.error;
|
||||
color = Theme.of(context).colorScheme.error;
|
||||
break;
|
||||
case DriveTaskStatus.cancelled:
|
||||
icon = Symbols.cancel;
|
||||
color = Theme.of(context).colorScheme.error;
|
||||
break;
|
||||
case DriveTaskStatus.expired:
|
||||
icon = Symbols.timer_off;
|
||||
color = Theme.of(context).colorScheme.error;
|
||||
break;
|
||||
}
|
||||
|
||||
return Icon(icon, size: 24, color: color);
|
||||
}
|
||||
|
||||
Widget _buildExpandedDetails(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: switch (widget.task.type) {
|
||||
'FileUpload' => _buildFileUploadDetails(context),
|
||||
_ => _buildGenericTaskDetails(context),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFileUploadDetails(BuildContext context) {
|
||||
final transmissionProgress = widget.task.transmissionProgress ?? 0.0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Server Processing Progress
|
||||
Text(
|
||||
widget.task.statusMessage ?? 'Processing',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${(widget.task.progress * 100).toStringAsFixed(1)}%',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(
|
||||
'${widget.task.uploadedChunks}/${widget.task.totalChunks} chunks',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
LinearProgressIndicator(
|
||||
value: widget.task.progress,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// File Transmission Progress
|
||||
Text(
|
||||
'File Transmission',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${(transmissionProgress * 100).toStringAsFixed(1)}%',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(
|
||||
'${_formatFileSize((transmissionProgress * widget.task.fileSize).toInt())} / ${_formatFileSize(widget.task.fileSize)}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
LinearProgressIndicator(
|
||||
value: transmissionProgress,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Speed and ETA
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_formatBytesPerSecond(widget.task),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (widget.task.status == DriveTaskStatus.inProgress)
|
||||
Text(
|
||||
'ETA: ${_formatDuration(widget.task.estimatedTimeRemaining)}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Error message if failed
|
||||
if (widget.task.errorMessage != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.task.errorMessage!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGenericTaskDetails(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Generic task progress
|
||||
Text(
|
||||
'Progress',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${(widget.task.progress * 100).toStringAsFixed(1)}%',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(
|
||||
widget.task.status.name,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
LinearProgressIndicator(
|
||||
value: widget.task.progress,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
||||
// Error message if failed
|
||||
if (widget.task.errorMessage != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.task.errorMessage!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes >= 1073741824) {
|
||||
return '${(bytes / 1073741824).toStringAsFixed(1)} GB';
|
||||
} else if (bytes >= 1048576) {
|
||||
return '${(bytes / 1048576).toStringAsFixed(1)} MB';
|
||||
} else if (bytes >= 1024) {
|
||||
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
} else {
|
||||
return '$bytes bytes';
|
||||
}
|
||||
}
|
||||
|
||||
String _formatBytesPerSecond(DriveTask task) {
|
||||
if (task.uploadedBytes == 0) return '0 B/s';
|
||||
|
||||
final elapsedSeconds = DateTime.now().difference(task.createdAt).inSeconds;
|
||||
if (elapsedSeconds == 0) return '0 B/s';
|
||||
|
||||
final bytesPerSecond = task.uploadedBytes / elapsedSeconds;
|
||||
return '${_formatFileSize(bytesPerSecond.toInt())}/s';
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
if (duration.inHours > 0) {
|
||||
return '${duration.inHours}h ${duration.inMinutes.remainder(60)}m';
|
||||
} else if (duration.inMinutes > 0) {
|
||||
return '${duration.inMinutes}m ${duration.inSeconds.remainder(60)}s';
|
||||
} else {
|
||||
return '${duration.inSeconds}s';
|
||||
}
|
||||
}
|
||||
}
|
||||
197
lib/widgets/usage_overview.dart
Normal file
197
lib/widgets/usage_overview.dart
Normal file
@@ -0,0 +1,197 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class UsageOverviewWidget extends StatelessWidget {
|
||||
final Map<String, dynamic>? usage;
|
||||
final Map<String, dynamic>? quota;
|
||||
|
||||
const UsageOverviewWidget({
|
||||
super.key,
|
||||
required this.usage,
|
||||
required this.quota,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (usage == null) return const SizedBox.shrink();
|
||||
final nonNullUsage = usage!;
|
||||
return Column(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'All Uploads',
|
||||
'${((nonNullUsage['total_usage_bytes'] as num) / (1024 * 1024 * 1024)).toStringAsFixed(3)} GiB',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'All Files',
|
||||
'${nonNullUsage['total_file_count']}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Quota',
|
||||
'${nonNullUsage['total_quota']} MiB',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Used Quota',
|
||||
'${((nonNullUsage['used_quota'] as num) / (nonNullUsage['total_quota'] as num) * 100).toStringAsFixed(2)}%',
|
||||
progress:
|
||||
(nonNullUsage['used_quota'] as num) /
|
||||
(nonNullUsage['total_quota'] as num),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('Pool Usage'),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(_buildPoolChartData(nonNullUsage)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('Verbose Quota'),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(_buildQuotaChartData(quota)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
PieChartData _buildPoolChartData(Map<String, dynamic> usage) {
|
||||
final pools = usage['pool_usages'] as List<dynamic>;
|
||||
final colors = [
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.orange,
|
||||
Colors.red,
|
||||
Colors.purple,
|
||||
];
|
||||
return PieChartData(
|
||||
sections:
|
||||
pools.asMap().entries.map((entry) {
|
||||
final pool = entry.value as Map<String, dynamic>;
|
||||
final title = pool['pool_name'] as String;
|
||||
final truncatedTitle =
|
||||
title.length > 8 ? '${title.substring(0, 8)}...' : title;
|
||||
return PieChartSectionData(
|
||||
value: (pool['usage_bytes'] as num).toDouble(),
|
||||
title: truncatedTitle,
|
||||
color: colors[entry.key % colors.length],
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
PieChartData _buildQuotaChartData(Map<String, dynamic>? quota) {
|
||||
if (quota == null) return PieChartData(sections: []);
|
||||
return PieChartData(
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
value: (quota['based_quota'] as num).toDouble(),
|
||||
title: 'Base',
|
||||
color: Colors.green,
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
value: (quota['extra_quota'] as num).toDouble(),
|
||||
title: 'Extra',
|
||||
color: Colors.orange,
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCard(String label, String value, {double? progress}) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 14)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (progress != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(value: progress),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
|
||||
@@ -13,7 +14,6 @@
|
||||
#include <flutter_timezone/flutter_timezone_plugin.h>
|
||||
#include <flutter_udid/flutter_udid_plugin.h>
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <irondash_engine_context/irondash_engine_context_plugin.h>
|
||||
#include <livekit_client/live_kit_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
@@ -30,6 +30,9 @@
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
|
||||
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
|
||||
g_autoptr(FlPluginRegistrar) file_saver_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
|
||||
file_saver_plugin_register_with_registrar(file_saver_registrar);
|
||||
@@ -51,9 +54,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
||||
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||
g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin");
|
||||
irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_drop
|
||||
file_saver
|
||||
file_selector_linux
|
||||
flutter_platform_alert
|
||||
@@ -10,7 +11,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_timezone
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
gtk
|
||||
irondash_engine_context
|
||||
livekit_client
|
||||
media_kit_libs_linux
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import app_links
|
||||
import connectivity_plus
|
||||
import desktop_drop
|
||||
import device_info_plus
|
||||
import file_picker
|
||||
import file_saver
|
||||
@@ -31,6 +31,7 @@ import media_kit_video
|
||||
import package_info_plus
|
||||
import pasteboard
|
||||
import path_provider_foundation
|
||||
import protocol_handler_macos
|
||||
import record_macos
|
||||
import screen_retriever_macos
|
||||
import share_plus
|
||||
@@ -47,8 +48,8 @@ import wakelock_plus
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
|
||||
@@ -73,6 +74,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
ProtocolHandlerMacosPlugin.register(with: registry.registrar(forPlugin: "ProtocolHandlerMacosPlugin"))
|
||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
PODS:
|
||||
- app_links (6.4.1):
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- croppy (0.0.1):
|
||||
- FlutterMacOS
|
||||
- desktop_drop (0.0.1):
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_picker (0.0.1):
|
||||
@@ -21,19 +21,19 @@ PODS:
|
||||
- Firebase/Messaging (12.4.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 12.4.0)
|
||||
- firebase_analytics (12.0.3):
|
||||
- firebase_analytics (12.0.4):
|
||||
- firebase_core
|
||||
- FirebaseAnalytics (= 12.4.0)
|
||||
- FlutterMacOS
|
||||
- firebase_core (4.2.0):
|
||||
- firebase_core (4.2.1):
|
||||
- Firebase/CoreOnly (~> 12.4.0)
|
||||
- FlutterMacOS
|
||||
- firebase_crashlytics (5.0.3):
|
||||
- firebase_crashlytics (5.0.4):
|
||||
- Firebase/CoreOnly (~> 12.4.0)
|
||||
- Firebase/Crashlytics (~> 12.4.0)
|
||||
- firebase_core
|
||||
- FlutterMacOS
|
||||
- firebase_messaging (16.0.3):
|
||||
- firebase_messaging (16.0.4):
|
||||
- Firebase/CoreOnly (~> 12.4.0)
|
||||
- Firebase/Messaging (~> 12.4.0)
|
||||
- firebase_core
|
||||
@@ -199,6 +199,8 @@ PODS:
|
||||
- PromisesObjC (2.4.0)
|
||||
- PromisesSwift (2.4.0):
|
||||
- PromisesObjC (= 2.4.0)
|
||||
- protocol_handler_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- record_macos (1.1.0):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain (1.5.3)
|
||||
@@ -256,9 +258,9 @@ PODS:
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
||||
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
||||
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
|
||||
@@ -284,6 +286,7 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- protocol_handler_macos (from `Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos`)
|
||||
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
||||
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
|
||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||
@@ -323,12 +326,12 @@ SPEC REPOS:
|
||||
- WebRTC-SDK
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
|
||||
connectivity_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
|
||||
croppy:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
|
||||
desktop_drop:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
||||
device_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||
file_picker:
|
||||
@@ -379,6 +382,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
|
||||
path_provider_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||
protocol_handler_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos
|
||||
record_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
|
||||
screen_retriever_macos:
|
||||
@@ -409,18 +414,18 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
|
||||
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
|
||||
croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43
|
||||
desktop_drop: 10a3e6a7fa9dbe350541f2574092fecfa345a07b
|
||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
||||
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
|
||||
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
|
||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||
firebase_analytics: d876586269c1d8d2b3dcac085bc2d97c62abc9df
|
||||
firebase_core: d81d1a44df95699ce074ae63d8cb43e9df21e142
|
||||
firebase_crashlytics: 723622cc39a9fa7320585424f5864c5699893ce1
|
||||
firebase_messaging: 31f412ae5a54e02d1c46d467969f7ad92c4b81ec
|
||||
firebase_analytics: 09241c4796c1c42a02349ef8bf30025f5b640f0e
|
||||
firebase_core: e054894ab56033ef9bcbe2d9eac9395e5306e2fc
|
||||
firebase_crashlytics: c2438b5f5bdcacf59d0eaee5852c6b0ab09dab77
|
||||
firebase_messaging: 373ac3a56e5aa37bb9aff4127f700aa5973c1168
|
||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
|
||||
@@ -454,6 +459,7 @@ SPEC CHECKSUMS:
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
protocol_handler_macos: f9cd7b13bcaf6b0425f7410cbe52376cb843a936
|
||||
record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||
|
||||
@@ -1,45 +1,58 @@
|
||||
<?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>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Solian</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.social-networking</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticTermination</key>
|
||||
<false/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false />
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Solian</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.social-networking</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticTermination</key>
|
||||
<false />
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true />
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict />
|
||||
</dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
246
pubspec.lock
246
pubspec.lock
@@ -13,10 +13,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: f871a7d1b686bea1f13722aa51ab31554d05c81f47054d6de48cc8c45153508b
|
||||
sha256: "8a1f5f3020ef2a74fb93f7ab3ef127a8feea33a7a2276279113660784ee7516a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.63"
|
||||
version: "1.3.64"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -49,38 +49,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
app_links:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: app_links
|
||||
sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
app_links_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_linux
|
||||
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
app_links_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_platform_interface
|
||||
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
app_links_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_web
|
||||
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
archive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -141,10 +109,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "409002f1adeea601018715d613115cfaf0e31f512cb80ae4534c79867ae2363d"
|
||||
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "4.1.1"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -314,7 +282,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
@@ -341,10 +309,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.0.7"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -425,14 +393,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
desktop_drop:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: desktop_drop
|
||||
sha256: e70b46b2d61f1af7a81a40d1f79b43c28a879e30a4ef31e87e9c27bea4d784e8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
device_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
|
||||
sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.5.0"
|
||||
version: "11.3.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -589,10 +565,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f
|
||||
sha256: f8f4ea435f791ab1f817b4e338ed958cb3d04ba43d6736ffc39958d950754967
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.3"
|
||||
version: "10.3.6"
|
||||
file_saver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -621,10 +597,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_platform_interface
|
||||
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
|
||||
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.2"
|
||||
version: "2.7.0"
|
||||
file_selector_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -637,34 +613,34 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_analytics
|
||||
sha256: "3cfc4089e61e810ffb531af63cfde2c8cfd36f12dc14fdba359e623992311015"
|
||||
sha256: bfb80d92eee10a6585ebd5a7e60de5caf0f2c06329e5676c0578130aea1bfe85
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.3"
|
||||
version: "12.0.4"
|
||||
firebase_analytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_platform_interface
|
||||
sha256: "775fc18d9b00a014362510a33f76f1f34deb30f69a64edcb41a7dfd0ebd9cf98"
|
||||
sha256: "3b803077907def997044774f6c022d8e9204e9c0f5e205e3572d887c93dafd72"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.3"
|
||||
version: "5.0.4"
|
||||
firebase_analytics_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_web
|
||||
sha256: "6eafa8fef5fdca6c922ac3e353c9a093c12344a3ba996e65fd40f8db0a00d26f"
|
||||
sha256: "0dbd96dbe77b51185319000c0078477fdcffb4abb0018c362dd9afb9845c1e06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0+3"
|
||||
version: "0.6.1"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "132e1c311bc41e7d387b575df0aacdf24efbf4930365eb61042be5bde3978f03"
|
||||
sha256: "1f2dfd9f535d81f8b06d7a50ecda6eac1e6922191ed42e09ca2c84bd2288927c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
version: "4.2.1"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -677,50 +653,50 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: ecde2def458292404a4fcd3731ee4992fd631a0ec359d2d67c33baa8da5ec8ae
|
||||
sha256: ff18fabb0ad0ed3595d2f2c85007ecc794aadecdff5b3bb1460b7ee47cded398
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.3.0"
|
||||
firebase_crashlytics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_crashlytics
|
||||
sha256: "2f53d0d3c0875105b166f09bdf026026bb74f26930c6ffcd5d65b311ca5a9f58"
|
||||
sha256: c3ebe3ed9f3b1d36c0864a4a28b041fcc2686f11fb2a4f7891319ea8d1d161cc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.3"
|
||||
version: "5.0.4"
|
||||
firebase_crashlytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_crashlytics_platform_interface
|
||||
sha256: de5c857525fc9576cd3fc30fc72422bc2371179ecae110246c0135ae896c6de3
|
||||
sha256: a8ca502fe3aa48b4f0b9e6e3bc0019085a247b5d1214cd342a189457975662db
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.8.14"
|
||||
version: "3.8.15"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "5021279acd1cb5ccaceaa388e616e82cc4a2e4d862f02637df0e8ab766e6900a"
|
||||
sha256: "22086f857d2340f5d973776cfd542d3fb30cf98e1c643c3aa4a7520bb12745bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.0.3"
|
||||
version: "16.0.4"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: f3a16c51f02055ace2a7c16ccb341c1f1b36b67c13270a48bcef68c1d970bbe8
|
||||
sha256: a59920cbf2eb7c83d34a5f354331210ffec116b216dc72d864d8b8eb983ca398
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.3"
|
||||
version: "4.7.4"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: "3eb9a1382caeb95b370f21e36d4a460496af777c9c2ef5df9b90d4803982c069"
|
||||
sha256: "1183e40e6fd2a279a628951cc3b639fcf5ffe7589902632db645011eb70ebefb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.3"
|
||||
version: "4.1.0"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -778,10 +754,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_card_swiper
|
||||
sha256: "9fbe75c913c0a01f34f9f98068ad198e396695fcf8abfa433cc53652fceb5617"
|
||||
sha256: "895c6974729b51cf73a35f1b58ab57a0af3293131319e2cbccac3bc57ffcd69f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.1.0"
|
||||
version: "7.2.0"
|
||||
flutter_colorpicker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1103,10 +1079,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678
|
||||
sha256: "055de8921be7b8e8b98a233c7a5ef84b3a6fcc32f46f1ebf5b9bb3576d108355"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -1201,10 +1177,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: get_it
|
||||
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
|
||||
sha256: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.0"
|
||||
version: "8.3.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1217,10 +1193,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c
|
||||
sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.3.0"
|
||||
version: "17.0.0"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1245,14 +1221,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.4"
|
||||
gtk:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gtk
|
||||
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
highlight:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1297,10 +1265,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
version: "1.6.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1329,10 +1297,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041"
|
||||
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
image_picker_android:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1673,10 +1641,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1957,6 +1925,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
protocol_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: protocol_handler
|
||||
sha256: dc2e2dcb1e0e313c3f43827ec3fa6d98adee6e17edc0c3923ac67efee87479a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
protocol_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protocol_handler_android
|
||||
sha256: "82eb860ca42149e400328f54b85140329a1766d982e94705b68271f6ca73895c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
protocol_handler_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protocol_handler_ios
|
||||
sha256: "0d3a56b8c1926002cb1e32b46b56874759f4dcc8183d389b670864ac041b6ec2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
protocol_handler_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protocol_handler_macos
|
||||
sha256: "6eb8687a84e7da3afbc5660ce046f29d7ecf7976db45a9dadeae6c87147dd710"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
protocol_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protocol_handler_platform_interface
|
||||
sha256: "53776b10526fdc25efdf1abcf68baf57fdfdb75342f4101051db521c9e3f3e5b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
protocol_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protocol_handler_windows
|
||||
sha256: d8f3a58938386aca2c76292757392f4d059d09f11439d6d896d876ebe997f2c4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2395,14 +2411,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2551,18 +2559,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_core
|
||||
sha256: a24e9ec04e03c2c14b7b41b1afe60e455adef09b244ab4c425ce6c5b8f58c9ce
|
||||
sha256: "8118f13264d1401a7085d12a0aaeac1ebd5cd939046b8c565d195879646daad6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.10"
|
||||
syncfusion_flutter_pdf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_pdf
|
||||
sha256: "8d98edae5c5d3aba2125de49bd37882da124409021d4f3de5730eb93d8247a81"
|
||||
sha256: "34d8b658e9fa7b18c4c16b4a775dc3f634933c4367e5f1ef4854f80cdd22c3ff"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.10"
|
||||
syncfusion_flutter_pdfviewer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2575,50 +2583,50 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_signaturepad
|
||||
sha256: d2f87273133283efd550370403462739329ad0ad1bdae6a73998be1fb30e9ee1
|
||||
sha256: ef891418bee7c79470ff1b6290f7745df8c1b2adf4df6b81ab9cd69ef900c4e8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.10"
|
||||
syncfusion_pdfviewer_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_linux
|
||||
sha256: "1edc9c3408526ad25c7a0d67b0f12a3e427225fd7e87d67319cd6e19bbfaeb45"
|
||||
sha256: ec4efb4cdd34462f40b7dafcb5094780a15c988691f28bdec141ea2a01f145bb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.10"
|
||||
syncfusion_pdfviewer_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_macos
|
||||
sha256: "962911d8cba4d3f5f0bf5dee5ef87cc0b31651431adfad56a51c47057859fb50"
|
||||
sha256: "15d4e7d5a5a705b7861bdf7e5758d371973a03fda33a21068dc934569b8fc363"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.10"
|
||||
syncfusion_pdfviewer_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_platform_interface
|
||||
sha256: a701825a971f1bb8540ad39611872ebc08ed0955a0a9600f263cb6cb85826ce2
|
||||
sha256: d2b9d4631693503340d5eaef6f42446d6d74f290dca9764e65f5b55b0b4043cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.10"
|
||||
syncfusion_pdfviewer_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_web
|
||||
sha256: e3eda11636a013a7ebab01a573b079d3a52c695474ac7c5239f65d5952d8da82
|
||||
sha256: "073384338eb9a6370e3ec7b9fbad973b6d0312c027698392c09409e156644807"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.10"
|
||||
syncfusion_pdfviewer_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_windows
|
||||
sha256: "9f8def51da7277bda5796ba27fff357a697689e226be397d7c52e353824cf961"
|
||||
sha256: de6254b5b939c17b32498d895aaf272748035fd20a9790b4ee7e8afe915ef233
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.10"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2687,10 +2695,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6"
|
||||
version: "0.7.7"
|
||||
timezone:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2719,10 +2727,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tray_manager
|
||||
sha256: "537e539f48cd82d8ee2240d4330158c7b44c7e043e8e18b5811f2f8f6b7df25a"
|
||||
sha256: c5fd83b0ae4d80be6eaedfad87aaefab8787b333b8ebd064b0e442a81006035b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
version: "0.5.2"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2840,10 +2848,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
version: "4.5.2"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2984,10 +2992,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
|
||||
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "1.1.5"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
32
pubspec.yaml
32
pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# 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: 3.3.0+144
|
||||
version: 3.3.0+145
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
@@ -38,7 +38,7 @@ dependencies:
|
||||
cupertino_icons: ^1.0.8
|
||||
flutter_hooks: ^0.21.3+1
|
||||
hooks_riverpod: ^2.6.1
|
||||
go_router: ^16.3.0
|
||||
go_router: ^17.0.0
|
||||
styled_widget: ^0.4.1
|
||||
shared_preferences: ^2.5.3
|
||||
flutter_riverpod: ^2.6.1
|
||||
@@ -50,7 +50,7 @@ dependencies:
|
||||
flutter_markdown_latex: ^0.3.4
|
||||
markdown: ^7.3.0
|
||||
flutter_highlight: ^0.7.0
|
||||
uuid: ^4.5.1
|
||||
uuid: ^4.5.2
|
||||
url_launcher: ^6.3.2
|
||||
google_fonts: ^6.3.2
|
||||
gap: ^3.0.1
|
||||
@@ -67,20 +67,21 @@ dependencies:
|
||||
flutter_inappwebview: ^6.1.5
|
||||
animations: ^2.1.0
|
||||
package_info_plus: ^9.0.0
|
||||
device_info_plus: ^11.5.0
|
||||
device_info_plus: ^11.3.0
|
||||
protocol_handler: ^0.2.0
|
||||
tus_client_dart:
|
||||
git: https://github.com/LittleSheep2Code/tus_client.git
|
||||
cross_file: ^0.3.5
|
||||
image_picker: ^1.2.0
|
||||
file_picker: ^10.3.3
|
||||
image_picker: ^1.2.1
|
||||
file_picker: ^10.3.6
|
||||
riverpod_annotation: ^2.6.1
|
||||
image_picker_platform_interface: ^2.11.1
|
||||
image_picker_android: ^0.8.13+7
|
||||
super_context_menu: ^0.9.1
|
||||
modal_bottom_sheet: ^3.0.0
|
||||
firebase_messaging: ^16.0.3
|
||||
firebase_messaging: ^16.0.4
|
||||
flutter_udid: ^4.0.0
|
||||
firebase_core: ^4.2.0
|
||||
firebase_core: ^4.2.1
|
||||
web_socket_channel: ^3.0.3
|
||||
material_symbols_icons: ^4.2874.0
|
||||
drift: ^2.28.2
|
||||
@@ -93,7 +94,7 @@ dependencies:
|
||||
relative_time: ^5.0.0
|
||||
dropdown_button2: ^2.3.9
|
||||
riverpod_paging_utils: ^0.8.1
|
||||
crypto: ^3.0.6
|
||||
crypto: ^3.0.7
|
||||
avatar_stack: ^3.0.0
|
||||
markdown_widget: ^2.3.2+8
|
||||
visibility_detector: ^0.4.0+2
|
||||
@@ -115,7 +116,7 @@ dependencies:
|
||||
flutter_timezone: ^5.0.1
|
||||
fl_chart: ^1.1.1
|
||||
sign_in_with_apple: ^7.0.1
|
||||
flutter_svg: ^2.2.1
|
||||
flutter_svg: ^2.2.2
|
||||
native_exif: ^0.6.2
|
||||
local_auth: ^3.0.0
|
||||
flutter_secure_storage: ^9.2.4
|
||||
@@ -134,13 +135,13 @@ dependencies:
|
||||
flutter_app_update: ^3.2.2
|
||||
archive: ^4.0.7
|
||||
process_run: ^1.2.4
|
||||
firebase_crashlytics: ^5.0.3
|
||||
firebase_analytics: ^12.0.3
|
||||
firebase_crashlytics: ^5.0.4
|
||||
firebase_analytics: ^12.0.4
|
||||
material_color_utilities: ^0.11.1
|
||||
screenshot: ^3.0.0
|
||||
flutter_card_swiper: ^7.1.0
|
||||
flutter_card_swiper: ^7.2.0
|
||||
file_saver: ^0.3.1
|
||||
tray_manager: ^0.5.1
|
||||
tray_manager: ^0.5.2
|
||||
flutter_webrtc: ^1.2.0
|
||||
flutter_local_notifications: ^19.5.0
|
||||
wakelock_plus: ^1.4.0
|
||||
@@ -158,13 +159,14 @@ dependencies:
|
||||
talker_logger: ^5.0.2
|
||||
talker_dio_logger: ^5.0.2
|
||||
talker_riverpod_logger: ^5.0.1
|
||||
app_links: ^6.4.1
|
||||
syncfusion_flutter_pdfviewer: ^31.1.21
|
||||
swipe_to: ^1.0.6
|
||||
fl_heatmap: ^0.4.6
|
||||
dio_smart_retry: ^7.0.1
|
||||
flutter_expandable_fab: ^2.5.2
|
||||
event_bus: ^2.0.1
|
||||
convert: ^3.1.2
|
||||
desktop_drop: ^0.7.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
; ==================================================
|
||||
#define AppVersion "3.2.0"
|
||||
#define BuildNumber "134"
|
||||
#define AppVersion "3.3.0"
|
||||
#define BuildNumber "144"
|
||||
; ==================================================
|
||||
|
||||
#define FullVersion AppVersion + "." + BuildNumber
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||
#include <dart_ipc/dart_ipc_plugin_c_api.h>
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
#include <pasteboard/pasteboard_plugin.h>
|
||||
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
|
||||
#include <record_windows/record_windows_plugin_c_api.h>
|
||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
@@ -38,12 +39,12 @@
|
||||
#include <windows_notification/windows_notification_plugin_c_api.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AppLinksPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
DartIpcPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DartIpcPluginCApi"));
|
||||
DesktopDropPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
||||
FileSaverPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSaverPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
@@ -76,6 +77,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||
PasteboardPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||
ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi"));
|
||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
connectivity_plus
|
||||
dart_ipc
|
||||
desktop_drop
|
||||
file_saver
|
||||
file_selector_windows
|
||||
firebase_core
|
||||
@@ -22,6 +22,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
media_kit_libs_windows_video
|
||||
media_kit_video
|
||||
pasteboard
|
||||
protocol_handler_windows
|
||||
record_windows
|
||||
screen_retriever_windows
|
||||
share_plus
|
||||
|
||||
@@ -1,51 +1,23 @@
|
||||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
#include <windows.h>
|
||||
#include "app_links/app_links_plugin_c_api.h"
|
||||
|
||||
#include "flutter_window.h"
|
||||
#include "utils.h"
|
||||
|
||||
bool SendAppLinkToInstance(const std::wstring& title) {
|
||||
// Find our exact window
|
||||
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", title.c_str());
|
||||
|
||||
if (hwnd) {
|
||||
// Dispatch new link to current window
|
||||
SendAppLink(hwnd);
|
||||
|
||||
// (Optional) Restore our window to front in same state
|
||||
WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) };
|
||||
GetWindowPlacement(hwnd, &place);
|
||||
|
||||
switch(place.showCmd) {
|
||||
case SW_SHOWMAXIMIZED:
|
||||
ShowWindow(hwnd, SW_SHOWMAXIMIZED);
|
||||
break;
|
||||
case SW_SHOWMINIMIZED:
|
||||
ShowWindow(hwnd, SW_RESTORE);
|
||||
break;
|
||||
default:
|
||||
ShowWindow(hwnd, SW_NORMAL);
|
||||
break;
|
||||
}
|
||||
|
||||
SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE);
|
||||
SetForegroundWindow(hwnd);
|
||||
// END (Optional) Restore
|
||||
|
||||
// Window has been found, don't create another one.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
_In_ wchar_t *command_line, _In_ int show_command)
|
||||
{
|
||||
if (SendAppLinkToInstance(L"solian")) {
|
||||
return EXIT_SUCCESS;
|
||||
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"Solian");
|
||||
if (hwnd != NULL)
|
||||
{
|
||||
DispatchToProtocolHandler(hwnd);
|
||||
|
||||
::ShowWindow(hwnd, SW_NORMAL);
|
||||
::SetForegroundWindow(hwnd);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Attach to console when present (e.g., 'flutter run') or create a
|
||||
|
||||
Reference in New Issue
Block a user