Compare commits
15 Commits
3.3.0+144
...
1395d65b76
| Author | SHA1 | Date | |
|---|---|---|---|
|
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,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>
|
||||
@@ -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,6 +51,12 @@ 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();
|
||||
|
||||
|
||||
54
lib/models/upload_task.dart
Normal file
54
lib/models/upload_task.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
|
||||
part 'upload_task.freezed.dart';
|
||||
part 'upload_task.g.dart';
|
||||
|
||||
enum UploadTaskStatus {
|
||||
pending,
|
||||
inProgress,
|
||||
paused,
|
||||
completed,
|
||||
failed,
|
||||
expired,
|
||||
cancelled,
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class UploadTask with _$UploadTask {
|
||||
const UploadTask._();
|
||||
|
||||
const factory UploadTask({
|
||||
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 UploadTaskStatus status,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
String? errorMessage,
|
||||
SnCloudFile? result,
|
||||
String? poolId,
|
||||
String? bundleId,
|
||||
String? encryptPassword,
|
||||
String? expiredAt,
|
||||
}) = _UploadTask;
|
||||
|
||||
factory UploadTask.fromJson(Map<String, dynamic> json) =>
|
||||
_$UploadTaskFromJson(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());
|
||||
}
|
||||
}
|
||||
343
lib/models/upload_task.freezed.dart
Normal file
343
lib/models/upload_task.freezed.dart
Normal file
@@ -0,0 +1,343 @@
|
||||
// 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 'upload_task.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$UploadTask {
|
||||
|
||||
String get id; String get taskId; String get fileName; String get contentType; int get fileSize; int get uploadedBytes; int get totalChunks; int get uploadedChunks; UploadTaskStatus get status; DateTime get createdAt; DateTime get updatedAt; String? get errorMessage; SnCloudFile? get result; String? get poolId; String? get bundleId; String? get encryptPassword; String? get expiredAt;
|
||||
/// Create a copy of UploadTask
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$UploadTaskCopyWith<UploadTask> get copyWith => _$UploadTaskCopyWithImpl<UploadTask>(this as UploadTask, _$identity);
|
||||
|
||||
/// Serializes this UploadTask to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is UploadTask&&(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.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(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.hash(runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,errorMessage,result,poolId,bundleId,encryptPassword,expiredAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UploadTask(id: $id, taskId: $taskId, fileName: $fileName, contentType: $contentType, fileSize: $fileSize, uploadedBytes: $uploadedBytes, totalChunks: $totalChunks, uploadedChunks: $uploadedChunks, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, errorMessage: $errorMessage, result: $result, poolId: $poolId, bundleId: $bundleId, encryptPassword: $encryptPassword, expiredAt: $expiredAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $UploadTaskCopyWith<$Res> {
|
||||
factory $UploadTaskCopyWith(UploadTask value, $Res Function(UploadTask) _then) = _$UploadTaskCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt
|
||||
});
|
||||
|
||||
|
||||
$SnCloudFileCopyWith<$Res>? get result;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$UploadTaskCopyWithImpl<$Res>
|
||||
implements $UploadTaskCopyWith<$Res> {
|
||||
_$UploadTaskCopyWithImpl(this._self, this._then);
|
||||
|
||||
final UploadTask _self;
|
||||
final $Res Function(UploadTask) _then;
|
||||
|
||||
/// Create a copy of UploadTask
|
||||
/// 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? errorMessage = 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 UploadTaskStatus,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,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // 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 UploadTask
|
||||
/// 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 [UploadTask].
|
||||
extension UploadTaskPatterns on UploadTask {
|
||||
/// 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( _UploadTask value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _UploadTask() 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( _UploadTask value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _UploadTask():
|
||||
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( _UploadTask value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _UploadTask() 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, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _UploadTask() 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.errorMessage,_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, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _UploadTask():
|
||||
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.errorMessage,_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, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _UploadTask() 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.errorMessage,_that.result,_that.poolId,_that.bundleId,_that.encryptPassword,_that.expiredAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _UploadTask extends UploadTask {
|
||||
const _UploadTask({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, this.errorMessage, this.result, this.poolId, this.bundleId, this.encryptPassword, this.expiredAt}): super._();
|
||||
factory _UploadTask.fromJson(Map<String, dynamic> json) => _$UploadTaskFromJson(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 UploadTaskStatus status;
|
||||
@override final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
@override final String? errorMessage;
|
||||
@override final SnCloudFile? result;
|
||||
@override final String? poolId;
|
||||
@override final String? bundleId;
|
||||
@override final String? encryptPassword;
|
||||
@override final String? expiredAt;
|
||||
|
||||
/// Create a copy of UploadTask
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$UploadTaskCopyWith<_UploadTask> get copyWith => __$UploadTaskCopyWithImpl<_UploadTask>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$UploadTaskToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UploadTask&&(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.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(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.hash(runtimeType,id,taskId,fileName,contentType,fileSize,uploadedBytes,totalChunks,uploadedChunks,status,createdAt,updatedAt,errorMessage,result,poolId,bundleId,encryptPassword,expiredAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UploadTask(id: $id, taskId: $taskId, fileName: $fileName, contentType: $contentType, fileSize: $fileSize, uploadedBytes: $uploadedBytes, totalChunks: $totalChunks, uploadedChunks: $uploadedChunks, status: $status, createdAt: $createdAt, updatedAt: $updatedAt, errorMessage: $errorMessage, result: $result, poolId: $poolId, bundleId: $bundleId, encryptPassword: $encryptPassword, expiredAt: $expiredAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$UploadTaskCopyWith<$Res> implements $UploadTaskCopyWith<$Res> {
|
||||
factory _$UploadTaskCopyWith(_UploadTask value, $Res Function(_UploadTask) _then) = __$UploadTaskCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String taskId, String fileName, String contentType, int fileSize, int uploadedBytes, int totalChunks, int uploadedChunks, UploadTaskStatus status, DateTime createdAt, DateTime updatedAt, String? errorMessage, SnCloudFile? result, String? poolId, String? bundleId, String? encryptPassword, String? expiredAt
|
||||
});
|
||||
|
||||
|
||||
@override $SnCloudFileCopyWith<$Res>? get result;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$UploadTaskCopyWithImpl<$Res>
|
||||
implements _$UploadTaskCopyWith<$Res> {
|
||||
__$UploadTaskCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _UploadTask _self;
|
||||
final $Res Function(_UploadTask) _then;
|
||||
|
||||
/// Create a copy of UploadTask
|
||||
/// 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? errorMessage = freezed,Object? result = freezed,Object? poolId = freezed,Object? bundleId = freezed,Object? encryptPassword = freezed,Object? expiredAt = freezed,}) {
|
||||
return _then(_UploadTask(
|
||||
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 UploadTaskStatus,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,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // 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 UploadTask
|
||||
/// 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
|
||||
61
lib/models/upload_task.g.dart
Normal file
61
lib/models/upload_task.g.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'upload_task.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_UploadTask _$UploadTaskFromJson(Map<String, dynamic> json) => _UploadTask(
|
||||
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(_$UploadTaskStatusEnumMap, json['status']),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
errorMessage: json['error_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> _$UploadTaskToJson(_UploadTask 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': _$UploadTaskStatusEnumMap[instance.status]!,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'error_message': instance.errorMessage,
|
||||
'result': instance.result?.toJson(),
|
||||
'pool_id': instance.poolId,
|
||||
'bundle_id': instance.bundleId,
|
||||
'encrypt_password': instance.encryptPassword,
|
||||
'expired_at': instance.expiredAt,
|
||||
};
|
||||
|
||||
const _$UploadTaskStatusEnumMap = {
|
||||
UploadTaskStatus.pending: 'pending',
|
||||
UploadTaskStatus.inProgress: 'inProgress',
|
||||
UploadTaskStatus.paused: 'paused',
|
||||
UploadTaskStatus.completed: 'completed',
|
||||
UploadTaskStatus.failed: 'failed',
|
||||
UploadTaskStatus.expired: 'expired',
|
||||
UploadTaskStatus.cancelled: 'cancelled',
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -28,7 +28,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;
|
||||
@@ -438,7 +438,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
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');
|
||||
@@ -474,7 +474,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
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'b1d5d583199941d55dfdc707e1a22eec9616b7f1';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
317
lib/pods/upload_tasks.dart
Normal file
317
lib/pods/upload_tasks.dart
Normal file
@@ -0,0 +1,317 @@
|
||||
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/upload_task.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
|
||||
final uploadTasksProvider =
|
||||
StateNotifierProvider<UploadTasksNotifier, List<UploadTask>>(
|
||||
(ref) => UploadTasksNotifier(ref),
|
||||
);
|
||||
|
||||
class UploadTasksNotifier extends StateNotifier<List<UploadTask>> {
|
||||
final Ref ref;
|
||||
StreamSubscription? _websocketSubscription;
|
||||
|
||||
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('upload.')) {
|
||||
final data = packet.data;
|
||||
if (data == null) return;
|
||||
|
||||
final taskId = data['task_id'] as String?;
|
||||
if (taskId == null) return;
|
||||
|
||||
switch (packet.type) {
|
||||
case 'upload.progress':
|
||||
_handleProgressUpdate(taskId, data);
|
||||
break;
|
||||
case 'upload.completed':
|
||||
_handleUploadCompleted(taskId, data);
|
||||
break;
|
||||
case 'upload.failed':
|
||||
_handleUploadFailed(taskId, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleProgressUpdate(String taskId, Map<String, dynamic> data) {
|
||||
final uploadedChunks = data['chunksUploaded'] as int? ?? 0;
|
||||
final uploadedBytes =
|
||||
(data['progress'] as num? ?? 0.0) /
|
||||
100.0 *
|
||||
(data['fileSize'] as int? ?? 0);
|
||||
|
||||
state =
|
||||
state.map((task) {
|
||||
if (task.taskId == taskId) {
|
||||
return task.copyWith(
|
||||
uploadedChunks: uploadedChunks,
|
||||
uploadedBytes: uploadedBytes.toInt(),
|
||||
status: UploadTaskStatus.inProgress,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return task;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void _handleUploadCompleted(String taskId, Map<String, dynamic> data) {
|
||||
final fileData = data['file'];
|
||||
if (fileData != null) {
|
||||
// Assuming the file data comes in the expected format
|
||||
// You might need to adjust this based on the actual API response
|
||||
}
|
||||
|
||||
state =
|
||||
state.map((task) {
|
||||
if (task.taskId == taskId) {
|
||||
return task.copyWith(
|
||||
status: UploadTaskStatus.completed,
|
||||
uploadedChunks: task.totalChunks,
|
||||
uploadedBytes: task.fileSize,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return task;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void _handleUploadFailed(String taskId, Map<String, dynamic> data) {
|
||||
final errorMessage = data['error'] as String? ?? 'Upload failed';
|
||||
|
||||
state =
|
||||
state.map((task) {
|
||||
if (task.taskId == taskId) {
|
||||
return task.copyWith(
|
||||
status: UploadTaskStatus.failed,
|
||||
errorMessage: errorMessage,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return task;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void addUploadTask(UploadTask task) {
|
||||
state = [...state, task];
|
||||
}
|
||||
|
||||
void updateTaskStatus(
|
||||
String taskId,
|
||||
UploadTaskStatus 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 removeTask(String taskId) {
|
||||
state = state.where((task) => task.taskId != taskId).toList();
|
||||
}
|
||||
|
||||
UploadTask? getTask(String taskId) {
|
||||
return state.where((task) => task.taskId == taskId).firstOrNull;
|
||||
}
|
||||
|
||||
List<UploadTask> getActiveTasks() {
|
||||
return state
|
||||
.where(
|
||||
(task) =>
|
||||
task.status == UploadTaskStatus.pending ||
|
||||
task.status == UploadTaskStatus.inProgress ||
|
||||
task.status == UploadTaskStatus.paused,
|
||||
)
|
||||
.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,
|
||||
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,
|
||||
);
|
||||
|
||||
if (createResponse['file_exists'] == true) {
|
||||
// File already exists, return the existing file
|
||||
return SnCloudFile.fromJson(createResponse['file']);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// Create upload task and add to state
|
||||
final uploadTask = UploadTask(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
taskId: taskId,
|
||||
fileName: fileName,
|
||||
contentType: contentType,
|
||||
fileSize: totalSize,
|
||||
uploadedBytes: 0,
|
||||
totalChunks: chunksCount,
|
||||
uploadedChunks: 0,
|
||||
status: UploadTaskStatus.pending,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
poolId: poolId,
|
||||
bundleId: bundleId,
|
||||
encryptPassword: encryptPassword,
|
||||
expiredAt: expiredAt,
|
||||
);
|
||||
|
||||
ref.read(uploadTasksProvider.notifier).addUploadTask(uploadTask);
|
||||
|
||||
// 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);
|
||||
},
|
||||
);
|
||||
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);
|
||||
},
|
||||
);
|
||||
bytesUploaded += chunks[i].length;
|
||||
}
|
||||
} else {
|
||||
throw ArgumentError('Invalid fileData type');
|
||||
}
|
||||
|
||||
// Step 3: Complete upload
|
||||
onProgress?.call(null, Duration.zero);
|
||||
return await completeUpload(taskId);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,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';
|
||||
@@ -507,19 +506,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -573,7 +571,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';
|
||||
@@ -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: [
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
@@ -16,16 +16,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,
|
||||
@@ -34,8 +75,17 @@ class FileUploader {
|
||||
String? expiredAt,
|
||||
int? chunkSize,
|
||||
}) 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',
|
||||
@@ -60,6 +110,7 @@ class FileUploader {
|
||||
required String taskId,
|
||||
required int chunkIndex,
|
||||
required Uint8List chunkData,
|
||||
ProgressCallback? onSendProgress,
|
||||
}) async {
|
||||
final formData = FormData.fromMap({
|
||||
'chunk': MultipartFile.fromBytes(
|
||||
@@ -71,19 +122,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 +149,12 @@ class FileUploader {
|
||||
String? encryptPassword,
|
||||
String? expiredAt,
|
||||
int? customChunkSize,
|
||||
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,
|
||||
@@ -112,41 +172,64 @@ 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);
|
||||
}
|
||||
|
||||
@@ -155,7 +238,7 @@ class FileUploader {
|
||||
required Dio client,
|
||||
String? poolId,
|
||||
FileUploadMode? mode,
|
||||
Function(double progress, Duration estimate)? onProgress,
|
||||
Function(double? progress, Duration estimate)? onProgress,
|
||||
}) {
|
||||
final completer = Completer<SnCloudFile?>();
|
||||
|
||||
@@ -189,7 +272,7 @@ class FileUploader {
|
||||
await exif.writeAttributes(gpsAttributes);
|
||||
})
|
||||
.then(
|
||||
(_) => _processUpload(
|
||||
(_) => _processUploadWithEnhancedUploader(
|
||||
fileData,
|
||||
client,
|
||||
poolId,
|
||||
@@ -199,7 +282,7 @@ class FileUploader {
|
||||
)
|
||||
.catchError((e) {
|
||||
debugPrint('Error removing GPS EXIF data: $e');
|
||||
return _processUpload(
|
||||
return _processUploadWithEnhancedUploader(
|
||||
fileData,
|
||||
client,
|
||||
poolId,
|
||||
@@ -212,33 +295,45 @@ class FileUploader {
|
||||
}
|
||||
}
|
||||
|
||||
_processUpload(fileData, client, poolId, onProgress, completer);
|
||||
_processUploadWithEnhancedUploader(
|
||||
fileData,
|
||||
client,
|
||||
poolId,
|
||||
onProgress,
|
||||
completer,
|
||||
);
|
||||
return completer;
|
||||
}
|
||||
|
||||
// Helper method to process the upload
|
||||
static Completer<SnCloudFile?> _processUpload(
|
||||
// Helper method to process the upload with enhanced uploader
|
||||
static Completer<SnCloudFile?> _processUploadWithEnhancedUploader(
|
||||
UniversalFile fileData,
|
||||
Dio client,
|
||||
String? poolId,
|
||||
Function(double progress, Duration estimate)? onProgress,
|
||||
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;
|
||||
_performUploadWithEnhancedUploader(
|
||||
fileData: data,
|
||||
fileName: fileData.displayName ?? data.name,
|
||||
contentType: actualMimetype,
|
||||
client: client,
|
||||
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 +347,76 @@ class FileUploader {
|
||||
return completer;
|
||||
}
|
||||
|
||||
if (bytes != null) {
|
||||
_performUploadWithEnhancedUploader(
|
||||
fileData: bytes,
|
||||
fileName: actualFilename,
|
||||
contentType: actualMimetype,
|
||||
client: client,
|
||||
poolId: poolId,
|
||||
onProgress: onProgress,
|
||||
completer: completer,
|
||||
);
|
||||
}
|
||||
|
||||
return completer;
|
||||
}
|
||||
|
||||
// Helper method to perform the actual upload
|
||||
static void _performUpload({
|
||||
required dynamic fileData,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
required Dio client,
|
||||
String? poolId,
|
||||
Function(double? progress, Duration estimate)? onProgress,
|
||||
required Completer<SnCloudFile?> completer,
|
||||
}) {
|
||||
final uploader = FileUploader(client);
|
||||
|
||||
// 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,
|
||||
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;
|
||||
// Helper method to perform the actual upload with enhanced uploader
|
||||
static void _performUploadWithEnhancedUploader({
|
||||
required dynamic fileData,
|
||||
required String fileName,
|
||||
required String contentType,
|
||||
required Dio client,
|
||||
String? poolId,
|
||||
Function(double? progress, Duration estimate)? onProgress,
|
||||
required Completer<SnCloudFile?> completer,
|
||||
}) {
|
||||
// Use the enhanced uploader from Riverpod context
|
||||
// This will be called from a context where we have access to Riverpod
|
||||
// For now, fall back to the regular uploader
|
||||
_performUpload(
|
||||
fileData: fileData,
|
||||
fileName: fileName,
|
||||
contentType: contentType,
|
||||
client: client,
|
||||
poolId: poolId,
|
||||
onProgress: onProgress,
|
||||
completer: 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);
|
||||
}
|
||||
|
||||
@@ -411,10 +411,7 @@ class AttachmentPreview extends HookConsumerWidget {
|
||||
),
|
||||
Gap(6),
|
||||
Center(
|
||||
child: LinearProgressIndicator(
|
||||
value:
|
||||
progress != null ? progress! / 100.0 : null,
|
||||
),
|
||||
child: LinearProgressIndicator(value: progress),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -112,23 +112,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;
|
||||
|
||||
@@ -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),
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -246,7 +246,8 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
|
||||
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) {
|
||||
|
||||
327
lib/widgets/upload_overlay.dart
Normal file
327
lib/widgets/upload_overlay.dart
Normal file
@@ -0,0 +1,327 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/upload_task.dart';
|
||||
import 'package:island/pods/upload_tasks.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:easy_localization/easy_localization.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 == UploadTaskStatus.pending ||
|
||||
task.status == UploadTaskStatus.inProgress ||
|
||||
task.status == UploadTaskStatus.paused,
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (activeTasks.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: Material(
|
||||
elevation: 8,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Container(
|
||||
width: 320,
|
||||
constraints: BoxConstraints(maxHeight: 400),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.upload,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'uploadTasks'.tr(),
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${activeTasks.length}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Task list
|
||||
Flexible(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: activeTasks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final task = activeTasks[index];
|
||||
return UploadTaskTile(task: task);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UploadTaskTile extends HookConsumerWidget {
|
||||
final UploadTask task;
|
||||
|
||||
const UploadTaskTile({super.key, required this.task});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isExpanded = useState(false);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => isExpanded.value = !isExpanded.value,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// Status icon
|
||||
_buildStatusIcon(context),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// File info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
task.fileName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_formatFileSize(task.fileSize),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Progress indicator
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
value: task.progress,
|
||||
strokeWidth: 3,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
),
|
||||
|
||||
// Expand/collapse button
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isExpanded.value
|
||||
? Symbols.expand_less
|
||||
: Symbols.expand_more,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () => isExpanded.value = !isExpanded.value,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Expanded details
|
||||
if (isExpanded.value) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildExpandedDetails(context),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusIcon(BuildContext context) {
|
||||
IconData icon;
|
||||
Color color;
|
||||
|
||||
switch (task.status) {
|
||||
case UploadTaskStatus.pending:
|
||||
icon = Symbols.schedule;
|
||||
color = Theme.of(context).colorScheme.secondary;
|
||||
break;
|
||||
case UploadTaskStatus.inProgress:
|
||||
icon = Symbols.upload;
|
||||
color = Theme.of(context).colorScheme.primary;
|
||||
break;
|
||||
case UploadTaskStatus.paused:
|
||||
icon = Symbols.pause_circle;
|
||||
color = Theme.of(context).colorScheme.tertiary;
|
||||
break;
|
||||
case UploadTaskStatus.completed:
|
||||
icon = Symbols.check_circle;
|
||||
color = Colors.green;
|
||||
break;
|
||||
case UploadTaskStatus.failed:
|
||||
icon = Symbols.error;
|
||||
color = Theme.of(context).colorScheme.error;
|
||||
break;
|
||||
case UploadTaskStatus.cancelled:
|
||||
icon = Symbols.cancel;
|
||||
color = Theme.of(context).colorScheme.error;
|
||||
break;
|
||||
case UploadTaskStatus.expired:
|
||||
icon = Symbols.timer_off;
|
||||
color = Theme.of(context).colorScheme.error;
|
||||
break;
|
||||
}
|
||||
|
||||
return Icon(icon, size: 16, 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: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Progress text
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${(task.progress * 100).toStringAsFixed(1)}%',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(
|
||||
'${task.uploadedChunks}/${task.totalChunks} chunks',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Progress bar
|
||||
LinearProgressIndicator(
|
||||
value: task.progress,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Speed and ETA
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_formatBytesPerSecond(task),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (task.status == UploadTaskStatus.inProgress)
|
||||
Text(
|
||||
'ETA: ${_formatDuration(task.estimatedTimeRemaining)}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Error message if failed
|
||||
if (task.errorMessage != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
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(UploadTask 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,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>
|
||||
@@ -51,9 +50,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);
|
||||
|
||||
@@ -10,7 +10,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_timezone
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
gtk
|
||||
irondash_engine_context
|
||||
livekit_client
|
||||
media_kit_libs_linux
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import app_links
|
||||
import connectivity_plus
|
||||
import device_info_plus
|
||||
import file_picker
|
||||
@@ -31,6 +30,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,7 +47,6 @@ import wakelock_plus
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
@@ -73,6 +72,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,6 +1,4 @@
|
||||
PODS:
|
||||
- app_links (6.4.1):
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- croppy (0.0.1):
|
||||
@@ -21,19 +19,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 +197,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,7 +256,6 @@ 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`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
@@ -284,6 +283,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,8 +323,6 @@ 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:
|
||||
@@ -379,6 +377,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,7 +409,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
|
||||
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
|
||||
croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43
|
||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||
@@ -417,10 +416,10 @@ SPEC CHECKSUMS:
|
||||
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 +453,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>
|
||||
210
pubspec.lock
210
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:
|
||||
@@ -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:
|
||||
@@ -429,10 +397,10 @@ packages:
|
||||
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:
|
||||
@@ -637,34 +605,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 +645,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 +746,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 +1071,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 +1169,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 +1185,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 +1213,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:
|
||||
@@ -1957,6 +1917,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 +2403,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 +2551,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_core
|
||||
sha256: a24e9ec04e03c2c14b7b41b1afe60e455adef09b244ab4c425ce6c5b8f58c9ce
|
||||
sha256: "825670efc828e18e14ff310cbcf6de91c8ff73b55c75ae6868a2b3cd87e88b6c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_flutter_pdf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_pdf
|
||||
sha256: "8d98edae5c5d3aba2125de49bd37882da124409021d4f3de5730eb93d8247a81"
|
||||
sha256: "50fc39ba628167949e89374488de67cc788646d6c0dce2a9fd047dbeecb841c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_flutter_pdfviewer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2575,50 +2575,50 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_signaturepad
|
||||
sha256: d2f87273133283efd550370403462739329ad0ad1bdae6a73998be1fb30e9ee1
|
||||
sha256: "6e60af61cec5ee7436b01ecb3fd944602aed42887789a67a27314678ad04d38a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_pdfviewer_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_linux
|
||||
sha256: "1edc9c3408526ad25c7a0d67b0f12a3e427225fd7e87d67319cd6e19bbfaeb45"
|
||||
sha256: fea25c996ed8850504c80c8fe7541aa3dce3d5159af0e92519d13e10a9509601
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_pdfviewer_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_macos
|
||||
sha256: "962911d8cba4d3f5f0bf5dee5ef87cc0b31651431adfad56a51c47057859fb50"
|
||||
sha256: "389326ef84ad9d14858d4f5f14da36267faa894134c38080ae30d55d2e3f4ce9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_pdfviewer_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_platform_interface
|
||||
sha256: a701825a971f1bb8540ad39611872ebc08ed0955a0a9600f263cb6cb85826ce2
|
||||
sha256: "2c3098dc644965feee66f4bf726ef433a51eecc16ccea71e052ba19897f3c2c5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_pdfviewer_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_web
|
||||
sha256: e3eda11636a013a7ebab01a573b079d3a52c695474ac7c5239f65d5952d8da82
|
||||
sha256: db5b91493aefb2e9faeb6425ea4f3c5f8eb7907a29ffca2e33564987a9c1c1f4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
syncfusion_pdfviewer_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_windows
|
||||
sha256: "9f8def51da7277bda5796ba27fff357a697689e226be397d7c52e353824cf961"
|
||||
sha256: d2e4d64e5cd96ea678b1ff66588897ce59e17e2685c1153995af53d91327a143
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.4"
|
||||
version: "31.2.5"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2719,10 +2719,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 +2840,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 +2984,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:
|
||||
|
||||
27
pubspec.yaml
27
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,7 +67,8 @@ 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
|
||||
@@ -78,9 +79,9 @@ dependencies:
|
||||
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,13 @@ 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
|
||||
|
||||
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,7 +6,6 @@
|
||||
|
||||
#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 <file_saver/file_saver_plugin.h>
|
||||
@@ -25,6 +24,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,8 +38,6 @@
|
||||
#include <windows_notification/windows_notification_plugin_c_api.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AppLinksPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
DartIpcPluginCApiRegisterWithRegistrar(
|
||||
@@ -76,6 +74,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||
PasteboardPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||
ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi"));
|
||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
connectivity_plus
|
||||
dart_ipc
|
||||
file_saver
|
||||
@@ -22,6 +21,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