From 269a64cabbd8f70d53513cce881d5c3ab1599354 Mon Sep 17 00:00:00 2001 From: Texas0295 Date: Sun, 21 Sep 2025 14:44:18 +0800 Subject: [PATCH 1/8] add general file upload support with pool-aware tus client - add "uploadFile" i18n key (en, zh-CN, zh-TW) - introduce putFileToPool for tus upload with X-FilePool header - add ComposeLogic.pickGeneralFile for arbitrary files - extend uploadAttachment to support poolId override - add toolbar button for general file upload Signed-off-by: Texas0295 --- assets/i18n/en-US.json | 1 + assets/i18n/zh-CN.json | 1 + assets/i18n/zh-TW.json | 1 + lib/services/file.dart | 49 ++++++++++++++ lib/widgets/post/compose_shared.dart | 92 ++++++++++++++++++++------- lib/widgets/post/compose_toolbar.dart | 10 +++ 6 files changed, 130 insertions(+), 24 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 120b957d..df3e0db7 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -168,6 +168,7 @@ "addPhoto": "Add photo", "addAudio": "Add audio", "addFile": "Add file", + "uploadFile": "Upload File", "recordAudio": "Record Audio", "linkAttachment": "Link Attachment", "fileIdCannotBeEmpty": "File ID cannot be empty", diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index c4d307e9..91bae0c4 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -122,6 +122,7 @@ "addVideo": "添加视频", "addPhoto": "添加照片", "addFile": "添加文件", + "uploadFile": "上传文件", "createDirectMessage": "创建新私人消息", "gotoDirectMessage": "前往私信", "react": "反应", diff --git a/assets/i18n/zh-TW.json b/assets/i18n/zh-TW.json index d8d56cfe..8dac4c3e 100644 --- a/assets/i18n/zh-TW.json +++ b/assets/i18n/zh-TW.json @@ -122,6 +122,7 @@ "addVideo": "添加視頻", "addPhoto": "添加照片", "addFile": "添加文件", + "uploadFile": "上傳文件", "createDirectMessage": "創建新私人消息", "gotoDirectMessage": "前往私信", "react": "反應", diff --git a/lib/services/file.dart b/lib/services/file.dart index 4971a987..ffc26090 100644 --- a/lib/services/file.dart +++ b/lib/services/file.dart @@ -40,6 +40,55 @@ Future cropImage( ); } +Completer putFileToPool({ + required UniversalFile fileData, + required String atk, + required String baseUrl, + required String poolId, + String? filename, + String? mimetype, + Function(double progress, Duration estimate)? onProgress, +}) { + final completer = Completer(); + + final data = fileData.data; + if (data is! XFile) { + completer.completeError( + ArgumentError('Unsupported fileData type for putFileToPool'), + ); + return completer; + } + + final actualFilename = filename ?? data.name; + final actualMimetype = mimetype ?? data.mimeType ?? 'application/octet-stream'; + + final metadata = { + 'filename': actualFilename, + 'content-type': actualMimetype, + }; + + final client = TusClient(data); + client + .upload( + uri: Uri.parse('$baseUrl/drive/tus'), + headers: { + 'Authorization': 'AtField $atk', + 'X-FilePool': poolId, + }, + metadata: metadata, + onComplete: (lastResponse) { + final resp = jsonDecode(lastResponse!.headers['x-fileinfo']!); + completer.complete(SnCloudFile.fromJson(resp)); + }, + onProgress: (progress, est) { + onProgress?.call(progress, est); + }, + ) + .catchError(completer.completeError); + + return completer; +} + Completer putMediaToCloud({ required UniversalFile fileData, required String atk, diff --git a/lib/widgets/post/compose_shared.dart b/lib/widgets/post/compose_shared.dart index 84b36b16..1684ce9f 100644 --- a/lib/widgets/post/compose_shared.dart +++ b/lib/widgets/post/compose_shared.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:mime/mime.dart'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:file_picker/file_picker.dart'; @@ -386,6 +387,30 @@ class ComposeLogic { }; } + static Future pickGeneralFile(WidgetRef ref, ComposeState state) async { + final result = await FilePicker.platform.pickFiles( + type: FileType.any, + allowMultiple: true, + ); + if (result == null || result.count == 0) return; + + final newFiles = []; + + for (final f in result.files) { + if (f.path == null) continue; + + final mimeType = + lookupMimeType(f.path!, headerBytes: f.bytes) ?? + 'application/octet-stream'; + final xfile = XFile(f.path!, name: f.name, mimeType: mimeType); + + final uf = UniversalFile(data: xfile, type: UniversalFileType.file); + newFiles.add(uf); + } + + state.attachments.value = [...state.attachments.value, ...newFiles]; + } + static Future pickPhotoMedia(WidgetRef ref, ComposeState state) async { final result = await FilePicker.platform.pickFiles( type: FileType.image, @@ -479,8 +504,9 @@ class ComposeLogic { static Future uploadAttachment( WidgetRef ref, ComposeState state, - int index, - ) async { + int index, { + String? poolId, + }) async { final attachment = state.attachments.value[index]; if (attachment.isOnCloud) return; @@ -489,42 +515,61 @@ class ComposeLogic { if (token == null) throw ArgumentError('Token is null'); try { - // Update progress state state.attachmentProgress.value = { ...state.attachmentProgress.value, index: 0, }; - // Upload file to cloud - final cloudFile = - await putMediaToCloud( - fileData: attachment, - atk: token, - baseUrl: baseUrl, - filename: attachment.data.name ?? 'Post media', - mimetype: - attachment.data.mimeType ?? - getMimeTypeFromFileType(attachment.type), - onProgress: (progress, _) { - state.attachmentProgress.value = { - ...state.attachmentProgress.value, - index: progress, - }; - }, - ).future; + SnCloudFile? cloudFile; + + if (attachment.type == UniversalFileType.file) { + cloudFile = + await putFileToPool( + fileData: attachment, + atk: token, + baseUrl: baseUrl, + // TODO: Generic Pool ID (Now: Solian Network Driver) + poolId: poolId ?? '500e5ed8-bd44-4359-bc0a-ec85e2adf447', + filename: attachment.data.name ?? 'General file', + mimetype: + attachment.data.mimeType ?? + getMimeTypeFromFileType(attachment.type), + onProgress: (progress, _) { + state.attachmentProgress.value = { + ...state.attachmentProgress.value, + index: progress, + }; + }, + ).future; + } else { + cloudFile = + await putMediaToCloud( + fileData: attachment, + atk: token, + baseUrl: baseUrl, + filename: attachment.data.name ?? 'Post media', + mimetype: + attachment.data.mimeType ?? + getMimeTypeFromFileType(attachment.type), + onProgress: (progress, _) { + state.attachmentProgress.value = { + ...state.attachmentProgress.value, + index: progress, + }; + }, + ).future; + } if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); } - // Update attachments list with cloud file final clone = List.of(state.attachments.value); clone[index] = UniversalFile(data: cloudFile, type: attachment.type); state.attachments.value = clone; } catch (err) { - showErrorAlert(err); + showErrorAlert(err.toString()); } finally { - // Clean up progress state state.attachmentProgress.value = {...state.attachmentProgress.value} ..remove(index); } @@ -643,7 +688,6 @@ class ComposeLogic { .where((entry) => entry.value.isOnDevice) .map((entry) => uploadAttachment(ref, state, entry.key)), ); - // Prepare API request final client = ref.watch(apiClientProvider); final isNewPost = originalPost == null; diff --git a/lib/widgets/post/compose_toolbar.dart b/lib/widgets/post/compose_toolbar.dart index 65d3a354..55745d37 100644 --- a/lib/widgets/post/compose_toolbar.dart +++ b/lib/widgets/post/compose_toolbar.dart @@ -25,6 +25,10 @@ class ComposeToolbar extends HookConsumerWidget { ComposeLogic.pickVideoMedia(ref, state); } + void pickGeneralFile() { + ComposeLogic.pickGeneralFile(ref, state); + } + void addAudio() { ComposeLogic.recordAudioMedia(ref, state, context); } @@ -96,6 +100,12 @@ class ComposeToolbar extends HookConsumerWidget { icon: const Icon(Symbols.mic), color: colorScheme.primary, ), + IconButton( + onPressed: pickGeneralFile, + tooltip: 'uploadFile'.tr(), + icon: const Icon(Symbols.file_upload), + color: colorScheme.primary, + ), IconButton( onPressed: linkAttachment, icon: const Icon(Symbols.attach_file), From b638343f021d2b69066a021afeb956853caaf65f Mon Sep 17 00:00:00 2001 From: Texas0295 Date: Sun, 21 Sep 2025 14:54:16 +0800 Subject: [PATCH 2/8] add pool fetching service and provider - define FilePool model - implement PoolService with /drive/pools endpoint - add Riverpod providers (poolServiceProvider, poolsProvider) Signed-off-by: Texas0295 --- lib/models/file_pool.dart | 36 ++++++++++++++++++++++++++++++++++ lib/pods/pool_provider.dart | 14 +++++++++++++ lib/services/pool_service.dart | 19 ++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 lib/models/file_pool.dart create mode 100644 lib/pods/pool_provider.dart create mode 100644 lib/services/pool_service.dart diff --git a/lib/models/file_pool.dart b/lib/models/file_pool.dart new file mode 100644 index 00000000..1f65c862 --- /dev/null +++ b/lib/models/file_pool.dart @@ -0,0 +1,36 @@ +class FilePool { + final String id; + final String name; + final String? description; + final Map storageConfig; + final Map billingConfig; + final Map policyConfig; + final bool isHidden; + + FilePool({ + required this.id, + required this.name, + this.description, + required this.storageConfig, + required this.billingConfig, + required this.policyConfig, + required this.isHidden, + }); + + factory FilePool.fromJson(Map json) { + return FilePool( + id: json['id'] as String, + name: json['name'] as String, + description: json['description'] as String?, + storageConfig: json['storage_config'] as Map, + billingConfig: json['billing_config'] as Map, + policyConfig: json['policy_config'] as Map, + isHidden: json['is_hidden'] as bool, + ); + } + + static List listFromResponse(dynamic data) { + final parsed = data as List; + return parsed.map((e) => FilePool.fromJson(e)).toList(); + } +} diff --git a/lib/pods/pool_provider.dart b/lib/pods/pool_provider.dart new file mode 100644 index 00000000..95ce2dc6 --- /dev/null +++ b/lib/pods/pool_provider.dart @@ -0,0 +1,14 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../services/pool_service.dart'; +import '../models/file_pool.dart'; +import 'package:island/pods/network.dart'; + +final poolServiceProvider = Provider((ref) { + final dio = ref.watch(apiClientProvider); + return PoolService(dio); +}); + +final poolsProvider = FutureProvider>((ref) async { + final service = ref.watch(poolServiceProvider); + return service.fetchPools(); +}); diff --git a/lib/services/pool_service.dart b/lib/services/pool_service.dart new file mode 100644 index 00000000..bbd120a2 --- /dev/null +++ b/lib/services/pool_service.dart @@ -0,0 +1,19 @@ +import 'package:dio/dio.dart'; + +import 'package:island/models/file_pool.dart'; + +class PoolService { + final Dio _dio; + + PoolService(this._dio); + + Future> fetchPools() async { + final response = await _dio.get('/drive/pools'); + + if (response.statusCode == 200) { + return FilePool.listFromResponse(response.data); + } else { + throw Exception('Failed to fetch pools: ${response.statusCode}'); + } + } +} From 3621ea77441146f5cd3d714142140dd56fbe6b9b Mon Sep 17 00:00:00 2001 From: Texas0295 Date: Sun, 21 Sep 2025 15:23:08 +0800 Subject: [PATCH 3/8] support default file pool selection - add defaultPoolId to AppSettings + persistence - extend SettingsScreen with pool dropdown - update uploadAttachment to use defaultPoolId with fallback Signed-off-by: Texas0295 --- assets/i18n/en-US.json | 2 + assets/i18n/zh-CN.json | 2 + assets/i18n/zh-TW.json | 3 ++ lib/pods/config.dart | 12 +++++ lib/pods/config.freezed.dart | 45 ++++++++++--------- lib/pods/config.g.dart | 2 +- lib/screens/chat/room.g.dart | 2 +- lib/screens/notification.g.dart | 4 +- lib/screens/settings.dart | 65 +++++++++++++++++++++++++++- lib/widgets/post/compose_shared.dart | 7 +-- 10 files changed, 116 insertions(+), 28 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index df3e0db7..43c89616 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -30,6 +30,8 @@ "fieldEmailAddressMustBeValid": "The email address must be valid.", "logout": "Logout", "updateYourProfile": "Profile Settings", + "settingsDefaultPool": "Default file pool", + "settingsDefaultPoolHelper": "Select the default storage pool for file uploads", "accountBasicInfo": "Basic Info", "accountProfile": "Your Profile", "saveChanges": "Save Changes", diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 91bae0c4..69004ba2 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -123,6 +123,8 @@ "addPhoto": "添加照片", "addFile": "添加文件", "uploadFile": "上传文件", + "settingsDefaultPool": "选择文件池", + "settingsDefaultPoolHelper": "为文件上传选择一个默认池", "createDirectMessage": "创建新私人消息", "gotoDirectMessage": "前往私信", "react": "反应", diff --git a/assets/i18n/zh-TW.json b/assets/i18n/zh-TW.json index 8dac4c3e..e3d6f836 100644 --- a/assets/i18n/zh-TW.json +++ b/assets/i18n/zh-TW.json @@ -123,6 +123,9 @@ "addPhoto": "添加照片", "addFile": "添加文件", "uploadFile": "上傳文件", + "settingsDefaultPool": "選擇文件池", + "settingsDefaultPoolHelper": "爲文件上傳選擇一個默認池", + "createDirectMessage": "創建新私人消息", "gotoDirectMessage": "前往私信", "react": "反應", diff --git a/lib/pods/config.dart b/lib/pods/config.dart index 52def773..0c6eb49a 100644 --- a/lib/pods/config.dart +++ b/lib/pods/config.dart @@ -25,6 +25,7 @@ const kAppSoundEffects = 'app_sound_effects'; const kAppAprilFoolFeatures = 'app_april_fool_features'; const kAppWindowSize = 'app_window_size'; const kAppEnterToSend = 'app_enter_to_send'; +const kAppDefaultPoolId = 'app_default_pool_id'; const kFeaturedPostsCollapsedId = 'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post @@ -65,6 +66,7 @@ sealed class AppSettings with _$AppSettings { required String? customFonts, required int? appColorScheme, // The color stored via the int type required Size? windowSize, // The window size for desktop platforms + required String? defaultPoolId, }) = _AppSettings; } @@ -84,6 +86,7 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { customFonts: prefs.getString(kAppCustomFonts), appColorScheme: prefs.getInt(kAppColorSchemeStoreKey), windowSize: _getWindowSizeFromPrefs(prefs), + defaultPoolId: prefs.getString(kAppDefaultPoolId), ); } @@ -103,6 +106,15 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { } return null; } + void setDefaultPoolId(String? value) { + final prefs = ref.read(sharedPreferencesProvider); + if (value != null) { + prefs.setString(kAppDefaultPoolId, value); + } else { + prefs.remove(kAppDefaultPoolId); + } + state = state.copyWith(defaultPoolId: value); + } void setAutoTranslate(bool value) { final prefs = ref.read(sharedPreferencesProvider); diff --git a/lib/pods/config.freezed.dart b/lib/pods/config.freezed.dart index aa460bb0..95dd93a9 100644 --- a/lib/pods/config.freezed.dart +++ b/lib/pods/config.freezed.dart @@ -15,7 +15,8 @@ T _$identity(T value) => value; mixin _$AppSettings { bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type - Size? get windowSize; + Size? get windowSize;// The window size for desktop platforms + String? get defaultPoolId; /// Create a copy of AppSettings /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -26,16 +27,16 @@ $AppSettingsCopyWith get copyWith => _$AppSettingsCopyWithImpl Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize); +int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId); @override String toString() { - return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)'; + return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId)'; } @@ -46,7 +47,7 @@ abstract mixin class $AppSettingsCopyWith<$Res> { factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl; @useResult $Res call({ - bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize + bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId }); @@ -63,7 +64,7 @@ class _$AppSettingsCopyWithImpl<$Res> /// Create a copy of AppSettings /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,}) { return _then(_self.copyWith( autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable @@ -75,7 +76,8 @@ as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundI as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable -as Size?, +as Size?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable +as String?, )); } @@ -157,10 +159,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _AppSettings() when $default != null: -return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);case _: +return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId);case _: return orElse(); } @@ -178,10 +180,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId) $default,) {final _that = this; switch (_that) { case _AppSettings(): -return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);} +return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId);} } /// A variant of `when` that fallback to returning `null` /// @@ -195,10 +197,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId)? $default,) {final _that = this; switch (_that) { case _AppSettings() when $default != null: -return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);case _: +return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId);case _: return null; } @@ -210,7 +212,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha class _AppSettings implements AppSettings { - const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize}); + const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize, required this.defaultPoolId}); @override final bool autoTranslate; @@ -224,6 +226,8 @@ class _AppSettings implements AppSettings { @override final int? appColorScheme; // The color stored via the int type @override final Size? windowSize; +// The window size for desktop platforms +@override final String? defaultPoolId; /// Create a copy of AppSettings /// with the given fields replaced by the non-null parameter values. @@ -235,16 +239,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_ @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)); } @override -int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize); +int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId); @override String toString() { - return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)'; + return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId)'; } @@ -255,7 +259,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl; @override @useResult $Res call({ - bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize + bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId }); @@ -272,7 +276,7 @@ class __$AppSettingsCopyWithImpl<$Res> /// Create a copy of AppSettings /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,}) { return _then(_AppSettings( autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable @@ -284,7 +288,8 @@ as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundI as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable -as Size?, +as Size?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable +as String?, )); } diff --git a/lib/pods/config.g.dart b/lib/pods/config.g.dart index 1d9731cc..7af42646 100644 --- a/lib/pods/config.g.dart +++ b/lib/pods/config.g.dart @@ -7,7 +7,7 @@ part of 'config.dart'; // ************************************************************************** String _$appSettingsNotifierHash() => - r'cd18bff2614a94e3523634e6c577cefad0367eba'; + r'a623ad859b71f42d0527b7f8b75bd37a6fd5d5c7'; /// See also [AppSettingsNotifier]. @ProviderFor(AppSettingsNotifier) diff --git a/lib/screens/chat/room.g.dart b/lib/screens/chat/room.g.dart index 079eae90..7836dfdd 100644 --- a/lib/screens/chat/room.g.dart +++ b/lib/screens/chat/room.g.dart @@ -6,7 +6,7 @@ part of 'room.dart'; // RiverpodGenerator // ************************************************************************** -String _$messagesNotifierHash() => r'fc3b66dfb8dd3fc55d142dae5c5e7bdc67eca5d4'; +String _$messagesNotifierHash() => r'82a91344328ec44dfe934c80a4a770431d864bff'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/screens/notification.g.dart b/lib/screens/notification.g.dart index 28604162..381a30f4 100644 --- a/lib/screens/notification.g.dart +++ b/lib/screens/notification.g.dart @@ -7,7 +7,7 @@ part of 'notification.dart'; // ************************************************************************** String _$notificationUnreadCountNotifierHash() => - r'0763b66bd64e5a9b7c317887e109ab367515dfa4'; + r'08c773809958d96a7ce82acf04af1f9e0b23e119'; /// See also [NotificationUnreadCountNotifier]. @ProviderFor(NotificationUnreadCountNotifier) @@ -28,7 +28,7 @@ final notificationUnreadCountNotifierProvider = typedef _$NotificationUnreadCountNotifier = AutoDisposeAsyncNotifier; String _$notificationListNotifierHash() => - r'5099466db475bbcf1ab6b514eb072f1dc4c6f930'; + r'260046e11f45b0d67ab25bcbdc8604890d71ccc7'; /// See also [NotificationListNotifier]. @ProviderFor(NotificationListNotifier) diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index c818d5ac..dcce6205 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -20,6 +20,7 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:path_provider/path_provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:island/pods/config.dart'; +import 'package:island/pods/pool_provider.dart'; class SettingsScreen extends HookConsumerWidget { const SettingsScreen({super.key}); @@ -33,7 +34,7 @@ class SettingsScreen extends HookConsumerWidget { final isDesktop = !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux); final isWide = isWideScreen(context); - + final poolsAsync = ref.watch(poolsProvider); final docBasepath = useState(null); useEffect(() { @@ -367,6 +368,68 @@ class SettingsScreen extends HookConsumerWidget { ), ), ), + poolsAsync.when( + data: (pools) { + return ListTile( + isThreeLine: true, + minLeadingWidth: 48, + title: Text('settingsDefaultPool').tr(), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.cloud), + subtitle: Text( + settings.defaultPoolId != null + ? pools + .firstWhereOrNull( + (p) => p.id == settings.defaultPoolId, + ) + ?.description ?? + 'settingsDefaultPoolHelper'.tr() + : 'settingsDefaultPoolHelper'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + trailing: DropdownButtonHideUnderline( + child: DropdownButton2( + isExpanded: true, + items: + pools.map((p) { + return DropdownMenuItem( + value: p.id, + child: Text(p.name).fontSize(14), + ); + }).toList(), + value: + settings.defaultPoolId ?? + (pools.isNotEmpty ? pools.first.id : null), + onChanged: (value) { + ref + .read(appSettingsNotifierProvider.notifier) + .setDefaultPoolId(value); + showSnackBar('settingsApplied'.tr()); + }, + buttonStyleData: const ButtonStyleData( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5), + height: 40, + width: 220, + ), + menuItemStyleData: const MenuItemStyleData(height: 40), + ), + ), + ); + }, + loading: + () => const ListTile( + minLeadingWidth: 48, + title: Text('Loading pools...'), + leading: CircularProgressIndicator(), + ), + error: + (err, st) => ListTile( + minLeadingWidth: 48, + title: Text('settingsDefaultPool').tr(), + subtitle: Text('Error: $err'), + leading: const Icon(Icons.error, color: Colors.red), + ), + ), ]; final behaviorSettings = [ diff --git a/lib/widgets/post/compose_shared.dart b/lib/widgets/post/compose_shared.dart index 1684ce9f..c30df65a 100644 --- a/lib/widgets/post/compose_shared.dart +++ b/lib/widgets/post/compose_shared.dart @@ -505,7 +505,7 @@ class ComposeLogic { WidgetRef ref, ComposeState state, int index, { - String? poolId, + String? poolId, // For Unit Test }) async { final attachment = state.attachments.value[index]; if (attachment.isOnCloud) return; @@ -522,14 +522,15 @@ class ComposeLogic { SnCloudFile? cloudFile; + final settings = ref.watch(appSettingsNotifierProvider); + final selectedPoolId = poolId ?? settings.defaultPoolId ?? '500e5ed8-bd44-4359-bc0a-ec85e2adf447'; if (attachment.type == UniversalFileType.file) { cloudFile = await putFileToPool( fileData: attachment, atk: token, baseUrl: baseUrl, - // TODO: Generic Pool ID (Now: Solian Network Driver) - poolId: poolId ?? '500e5ed8-bd44-4359-bc0a-ec85e2adf447', + poolId: selectedPoolId, filename: attachment.data.name ?? 'General file', mimetype: attachment.data.mimeType ?? From 1a703b7eba0f1ada2c3c7fab1bf21da149482ee7 Mon Sep 17 00:00:00 2001 From: Texas0295 Date: Sun, 21 Sep 2025 16:14:57 +0800 Subject: [PATCH 4/8] add default pool selection with validation and fallback - extend AppSettings with defaultPoolId - add pool filtering utility to exclude media-only pools - add resolveDefaultPoolId with fallback to safe pool - update SettingsScreen with default pool dropdown - integrate uploadAttachment with default pool resolution Signed-off-by: Texas0295 --- lib/screens/settings.dart | 23 +++++++++--------- lib/utils/pool_utils.dart | 35 ++++++++++++++++++++++++++++ lib/widgets/post/compose_shared.dart | 6 +++-- 3 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 lib/utils/pool_utils.dart diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index dcce6205..c3a591ec 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -21,6 +21,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/pool_provider.dart'; +import 'package:island/utils/pool_utils.dart'; class SettingsScreen extends HookConsumerWidget { const SettingsScreen({super.key}); @@ -368,8 +369,12 @@ class SettingsScreen extends HookConsumerWidget { ), ), ), + poolsAsync.when( data: (pools) { + final validPools = filterValidPools(pools); + final currentPoolId = resolveDefaultPoolId(ref, pools); + return ListTile( isThreeLine: true, minLeadingWidth: 48, @@ -377,29 +382,23 @@ class SettingsScreen extends HookConsumerWidget { contentPadding: const EdgeInsets.only(left: 24, right: 17), leading: const Icon(Symbols.cloud), subtitle: Text( - settings.defaultPoolId != null - ? pools - .firstWhereOrNull( - (p) => p.id == settings.defaultPoolId, - ) - ?.description ?? - 'settingsDefaultPoolHelper'.tr() - : 'settingsDefaultPoolHelper'.tr(), + validPools + .firstWhereOrNull((p) => p.id == currentPoolId) + ?.description ?? + 'settingsDefaultPoolHelper'.tr(), style: Theme.of(context).textTheme.bodySmall, ), trailing: DropdownButtonHideUnderline( child: DropdownButton2( isExpanded: true, items: - pools.map((p) { + validPools.map((p) { return DropdownMenuItem( value: p.id, child: Text(p.name).fontSize(14), ); }).toList(), - value: - settings.defaultPoolId ?? - (pools.isNotEmpty ? pools.first.id : null), + value: currentPoolId, onChanged: (value) { ref .read(appSettingsNotifierProvider.notifier) diff --git a/lib/utils/pool_utils.dart b/lib/utils/pool_utils.dart new file mode 100644 index 00000000..12d34d54 --- /dev/null +++ b/lib/utils/pool_utils.dart @@ -0,0 +1,35 @@ + +import '../models/file_pool.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../pods/config.dart'; + +List filterValidPools(List pools) { + return pools.where((p) { + final accept = p.policyConfig['accept_types']; + if (accept != null) { + final acceptsOnlyMedia = accept.every((t) => + t.startsWith('image/') || + t.startsWith('video/') || + t.startsWith('audio/')); + if (acceptsOnlyMedia) return false; + } + return true; + }).toList(); +} + +String resolveDefaultPoolId(WidgetRef ref, List pools) { + final settings = ref.watch(appSettingsNotifierProvider); + final validPools = filterValidPools(pools); + + if (settings.defaultPoolId != null && + validPools.any((p) => p.id == settings.defaultPoolId)) { + return settings.defaultPoolId!; + } + + if (validPools.isNotEmpty) { + return validPools.first.id; + } + // DEFAULT: Solar Network Driver + return '500e5ed8-bd44-4359-bc0a-ec85e2adf447'; +} + diff --git a/lib/widgets/post/compose_shared.dart b/lib/widgets/post/compose_shared.dart index c30df65a..e94f0e5d 100644 --- a/lib/widgets/post/compose_shared.dart +++ b/lib/widgets/post/compose_shared.dart @@ -20,6 +20,8 @@ import 'package:island/widgets/alert.dart'; import 'package:island/widgets/post/compose_link_attachments.dart'; import 'package:island/widgets/post/compose_poll.dart'; import 'package:island/widgets/post/compose_recorder.dart'; +import 'package:island/pods/pool_provider.dart'; +import 'package:island/utils/pool_utils.dart'; import 'package:pasteboard/pasteboard.dart'; import 'package:textfield_tags/textfield_tags.dart'; import 'dart:async'; @@ -522,8 +524,8 @@ class ComposeLogic { SnCloudFile? cloudFile; - final settings = ref.watch(appSettingsNotifierProvider); - final selectedPoolId = poolId ?? settings.defaultPoolId ?? '500e5ed8-bd44-4359-bc0a-ec85e2adf447'; + final pools = await ref.read(poolsProvider.future); + final selectedPoolId = resolveDefaultPoolId(ref, pools); if (attachment.type == UniversalFileType.file) { cloudFile = await putFileToPool( From b80d91825aa1a968f6135dc83133eaa43fa01a14 Mon Sep 17 00:00:00 2001 From: Texas0295 Date: Sun, 21 Sep 2025 20:29:20 +0800 Subject: [PATCH 5/8] migrate file upload from tus to FileUploader API The tus-based upload flow (/drive/tus) has been removed upstream in favor of a new multipart upload protocol. This commit replaces all TusClient usage with the new FileUploader service that follows the official /drive/files/upload/{create,chunk,complete} endpoints. Changes include: - remove tus_client_dart dependency and related code - add putFileToPool() backed by FileUploader.uploadFile() - update uploadAttachment() to call the new putFileToPool - preserve poolId support, filename, and mimetype handling - ensure progress callbacks fire at start and completion This aligns the client with the new upload protocol while keeping the same Compose UI and settings logic introduced in earlier patches. Signed-off-by: Texas0295 --- lib/services/file.dart | 46 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/services/file.dart b/lib/services/file.dart index ffc26090..a2ac2a26 100644 --- a/lib/services/file.dart +++ b/lib/services/file.dart @@ -50,7 +50,6 @@ Completer putFileToPool({ Function(double progress, Duration estimate)? onProgress, }) { final completer = Completer(); - final data = fileData.data; if (data is! XFile) { completer.completeError( @@ -62,33 +61,34 @@ Completer putFileToPool({ final actualFilename = filename ?? data.name; final actualMimetype = mimetype ?? data.mimeType ?? 'application/octet-stream'; - final metadata = { - 'filename': actualFilename, - 'content-type': actualMimetype, - }; + final dio = Dio(BaseOptions( + baseUrl: baseUrl, + headers: { + 'Authorization': 'AtField $atk', + 'Accept': 'application/json', + }, + )); - final client = TusClient(data); - client - .upload( - uri: Uri.parse('$baseUrl/drive/tus'), - headers: { - 'Authorization': 'AtField $atk', - 'X-FilePool': poolId, - }, - metadata: metadata, - onComplete: (lastResponse) { - final resp = jsonDecode(lastResponse!.headers['x-fileinfo']!); - completer.complete(SnCloudFile.fromJson(resp)); - }, - onProgress: (progress, est) { - onProgress?.call(progress, est); - }, - ) - .catchError(completer.completeError); + final uploader = FileUploader(dio); + final fileObj = File(data.path); + + onProgress?.call(0.0, Duration.zero); + uploader.uploadFile( + file: fileObj, + fileName: actualFilename, + contentType: actualMimetype, + poolId: poolId, + ).then((result) { + onProgress?.call(1.0, Duration.zero); + completer.complete(result); + }).catchError((e) { + completer.completeError(e); + }); return completer; } + Completer putMediaToCloud({ required UniversalFile fileData, required String atk, From cbdc7acdcd241e177c799a82cc8050b9dc7afe5a Mon Sep 17 00:00:00 2001 From: Texas0295 Date: Sun, 21 Sep 2025 21:37:07 +0800 Subject: [PATCH 6/8] [REF] refactor file pool model and imports - Refactor SnFilePool using freezed + sealed for consistency with other Sn models - Add extension method listFromResponse for PoolService compatibility - Update PoolService and utils to use SnFilePool - Replace relative imports (../) with package imports for clarity and maintainability Signed-off-by: Texas0295 --- lib/models/file_pool.dart | 63 ++--- lib/models/file_pool.freezed.dart | 328 +++++++++++++++++++++++++ lib/models/file_pool.g.dart | 47 ++++ lib/pods/pool_provider.dart | 6 +- lib/screens/account/profile.g.dart | 2 +- lib/screens/auth/captcha.config.g.dart | 2 +- lib/screens/posts/pub_profile.g.dart | 2 +- lib/screens/realm/realm_detail.g.dart | 2 +- lib/screens/settings.dart | 16 +- lib/services/pool_service.dart | 4 +- lib/utils/pool_utils.dart | 34 +-- 11 files changed, 443 insertions(+), 63 deletions(-) create mode 100644 lib/models/file_pool.freezed.dart create mode 100644 lib/models/file_pool.g.dart diff --git a/lib/models/file_pool.dart b/lib/models/file_pool.dart index 1f65c862..f84461f8 100644 --- a/lib/models/file_pool.dart +++ b/lib/models/file_pool.dart @@ -1,36 +1,37 @@ -class FilePool { - final String id; - final String name; - final String? description; - final Map storageConfig; - final Map billingConfig; - final Map policyConfig; - final bool isHidden; +import 'package:freezed_annotation/freezed_annotation.dart'; - FilePool({ - required this.id, - required this.name, - this.description, - required this.storageConfig, - required this.billingConfig, - required this.policyConfig, - required this.isHidden, - }); +part 'file_pool.freezed.dart'; +part 'file_pool.g.dart'; - factory FilePool.fromJson(Map json) { - return FilePool( - id: json['id'] as String, - name: json['name'] as String, - description: json['description'] as String?, - storageConfig: json['storage_config'] as Map, - billingConfig: json['billing_config'] as Map, - policyConfig: json['policy_config'] as Map, - isHidden: json['is_hidden'] as bool, - ); - } +@freezed +sealed class SnFilePool with _$SnFilePool { + const factory SnFilePool({ + required String id, + required String name, + String? description, + Map? storageConfig, + Map? billingConfig, + Map? policyConfig, + bool? isHidden, + String? accountId, + String? resourceIdentifier, + DateTime? createdAt, + DateTime? updatedAt, + DateTime? deletedAt, + }) = _SnFilePool; - static List listFromResponse(dynamic data) { - final parsed = data as List; - return parsed.map((e) => FilePool.fromJson(e)).toList(); + factory SnFilePool.fromJson(Map json) => + _$SnFilePoolFromJson(json); +} + +extension SnFilePoolList on SnFilePool { + static List listFromResponse(dynamic data) { + if (data is List) { + return data + .whereType>() + .map(SnFilePool.fromJson) + .toList(); + } + throw ArgumentError('Unexpected response format: $data'); } } diff --git a/lib/models/file_pool.freezed.dart b/lib/models/file_pool.freezed.dart new file mode 100644 index 00000000..e262caa5 --- /dev/null +++ b/lib/models/file_pool.freezed.dart @@ -0,0 +1,328 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'file_pool.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$SnFilePool { + + String get id; String get name; String? get description; Map? get storageConfig; Map? get billingConfig; Map? get policyConfig; bool? get isHidden; String? get accountId; String? get resourceIdentifier; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; +/// Create a copy of SnFilePool +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SnFilePoolCopyWith get copyWith => _$SnFilePoolCopyWithImpl(this as SnFilePool, _$identity); + + /// Serializes this SnFilePool to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnFilePool&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.storageConfig, storageConfig)&&const DeepCollectionEquality().equals(other.billingConfig, billingConfig)&&const DeepCollectionEquality().equals(other.policyConfig, policyConfig)&&(identical(other.isHidden, isHidden) || other.isHidden == isHidden)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(storageConfig),const DeepCollectionEquality().hash(billingConfig),const DeepCollectionEquality().hash(policyConfig),isHidden,accountId,resourceIdentifier,createdAt,updatedAt,deletedAt); + +@override +String toString() { + return 'SnFilePool(id: $id, name: $name, description: $description, storageConfig: $storageConfig, billingConfig: $billingConfig, policyConfig: $policyConfig, isHidden: $isHidden, accountId: $accountId, resourceIdentifier: $resourceIdentifier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class $SnFilePoolCopyWith<$Res> { + factory $SnFilePoolCopyWith(SnFilePool value, $Res Function(SnFilePool) _then) = _$SnFilePoolCopyWithImpl; +@useResult +$Res call({ + String id, String name, String? description, Map? storageConfig, Map? billingConfig, Map? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt +}); + + + + +} +/// @nodoc +class _$SnFilePoolCopyWithImpl<$Res> + implements $SnFilePoolCopyWith<$Res> { + _$SnFilePoolCopyWithImpl(this._self, this._then); + + final SnFilePool _self; + final $Res Function(SnFilePool) _then; + +/// Create a copy of SnFilePool +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? storageConfig = freezed,Object? billingConfig = freezed,Object? policyConfig = freezed,Object? isHidden = freezed,Object? accountId = freezed,Object? resourceIdentifier = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,storageConfig: freezed == storageConfig ? _self.storageConfig : storageConfig // ignore: cast_nullable_to_non_nullable +as Map?,billingConfig: freezed == billingConfig ? _self.billingConfig : billingConfig // ignore: cast_nullable_to_non_nullable +as Map?,policyConfig: freezed == policyConfig ? _self.policyConfig : policyConfig // ignore: cast_nullable_to_non_nullable +as Map?,isHidden: freezed == isHidden ? _self.isHidden : isHidden // ignore: cast_nullable_to_non_nullable +as bool?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable +as String?,resourceIdentifier: freezed == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable +as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [SnFilePool]. +extension SnFilePoolPatterns on SnFilePool { +/// 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 Function( _SnFilePool value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SnFilePool() 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 Function( _SnFilePool value) $default,){ +final _that = this; +switch (_that) { +case _SnFilePool(): +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? Function( _SnFilePool value)? $default,){ +final _that = this; +switch (_that) { +case _SnFilePool() 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 Function( String id, String name, String? description, Map? storageConfig, Map? billingConfig, Map? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SnFilePool() when $default != null: +return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String name, String? description, Map? storageConfig, Map? billingConfig, Map? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt) $default,) {final _that = this; +switch (_that) { +case _SnFilePool(): +return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String name, String? description, Map? storageConfig, Map? billingConfig, Map? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt)? $default,) {final _that = this; +switch (_that) { +case _SnFilePool() when $default != null: +return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _SnFilePool implements SnFilePool { + const _SnFilePool({required this.id, required this.name, this.description, final Map? storageConfig, final Map? billingConfig, final Map? policyConfig, this.isHidden, this.accountId, this.resourceIdentifier, this.createdAt, this.updatedAt, this.deletedAt}): _storageConfig = storageConfig,_billingConfig = billingConfig,_policyConfig = policyConfig; + factory _SnFilePool.fromJson(Map json) => _$SnFilePoolFromJson(json); + +@override final String id; +@override final String name; +@override final String? description; + final Map? _storageConfig; +@override Map? get storageConfig { + final value = _storageConfig; + if (value == null) return null; + if (_storageConfig is EqualUnmodifiableMapView) return _storageConfig; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); +} + + final Map? _billingConfig; +@override Map? get billingConfig { + final value = _billingConfig; + if (value == null) return null; + if (_billingConfig is EqualUnmodifiableMapView) return _billingConfig; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); +} + + final Map? _policyConfig; +@override Map? get policyConfig { + final value = _policyConfig; + if (value == null) return null; + if (_policyConfig is EqualUnmodifiableMapView) return _policyConfig; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); +} + +@override final bool? isHidden; +@override final String? accountId; +@override final String? resourceIdentifier; +@override final DateTime? createdAt; +@override final DateTime? updatedAt; +@override final DateTime? deletedAt; + +/// Create a copy of SnFilePool +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SnFilePoolCopyWith<_SnFilePool> get copyWith => __$SnFilePoolCopyWithImpl<_SnFilePool>(this, _$identity); + +@override +Map toJson() { + return _$SnFilePoolToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnFilePool&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._storageConfig, _storageConfig)&&const DeepCollectionEquality().equals(other._billingConfig, _billingConfig)&&const DeepCollectionEquality().equals(other._policyConfig, _policyConfig)&&(identical(other.isHidden, isHidden) || other.isHidden == isHidden)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_storageConfig),const DeepCollectionEquality().hash(_billingConfig),const DeepCollectionEquality().hash(_policyConfig),isHidden,accountId,resourceIdentifier,createdAt,updatedAt,deletedAt); + +@override +String toString() { + return 'SnFilePool(id: $id, name: $name, description: $description, storageConfig: $storageConfig, billingConfig: $billingConfig, policyConfig: $policyConfig, isHidden: $isHidden, accountId: $accountId, resourceIdentifier: $resourceIdentifier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$SnFilePoolCopyWith<$Res> implements $SnFilePoolCopyWith<$Res> { + factory _$SnFilePoolCopyWith(_SnFilePool value, $Res Function(_SnFilePool) _then) = __$SnFilePoolCopyWithImpl; +@override @useResult +$Res call({ + String id, String name, String? description, Map? storageConfig, Map? billingConfig, Map? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt +}); + + + + +} +/// @nodoc +class __$SnFilePoolCopyWithImpl<$Res> + implements _$SnFilePoolCopyWith<$Res> { + __$SnFilePoolCopyWithImpl(this._self, this._then); + + final _SnFilePool _self; + final $Res Function(_SnFilePool) _then; + +/// Create a copy of SnFilePool +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? storageConfig = freezed,Object? billingConfig = freezed,Object? policyConfig = freezed,Object? isHidden = freezed,Object? accountId = freezed,Object? resourceIdentifier = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,}) { + return _then(_SnFilePool( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,storageConfig: freezed == storageConfig ? _self._storageConfig : storageConfig // ignore: cast_nullable_to_non_nullable +as Map?,billingConfig: freezed == billingConfig ? _self._billingConfig : billingConfig // ignore: cast_nullable_to_non_nullable +as Map?,policyConfig: freezed == policyConfig ? _self._policyConfig : policyConfig // ignore: cast_nullable_to_non_nullable +as Map?,isHidden: freezed == isHidden ? _self.isHidden : isHidden // ignore: cast_nullable_to_non_nullable +as bool?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable +as String?,resourceIdentifier: freezed == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable +as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} + + +} + +// dart format on diff --git a/lib/models/file_pool.g.dart b/lib/models/file_pool.g.dart new file mode 100644 index 00000000..a2b2cb4d --- /dev/null +++ b/lib/models/file_pool.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'file_pool.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_SnFilePool _$SnFilePoolFromJson(Map json) => _SnFilePool( + id: json['id'] as String, + name: json['name'] as String, + description: json['description'] as String?, + storageConfig: json['storage_config'] as Map?, + billingConfig: json['billing_config'] as Map?, + policyConfig: json['policy_config'] as Map?, + isHidden: json['is_hidden'] as bool?, + accountId: json['account_id'] as String?, + resourceIdentifier: json['resource_identifier'] as String?, + createdAt: + json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: + json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + deletedAt: + json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), +); + +Map _$SnFilePoolToJson(_SnFilePool instance) => + { + 'id': instance.id, + 'name': instance.name, + 'description': instance.description, + 'storage_config': instance.storageConfig, + 'billing_config': instance.billingConfig, + 'policy_config': instance.policyConfig, + 'is_hidden': instance.isHidden, + 'account_id': instance.accountId, + 'resource_identifier': instance.resourceIdentifier, + 'created_at': instance.createdAt?.toIso8601String(), + 'updated_at': instance.updatedAt?.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + }; diff --git a/lib/pods/pool_provider.dart b/lib/pods/pool_provider.dart index 95ce2dc6..b267fed8 100644 --- a/lib/pods/pool_provider.dart +++ b/lib/pods/pool_provider.dart @@ -1,6 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../services/pool_service.dart'; -import '../models/file_pool.dart'; +import 'package:island/services/pool_service.dart'; +import 'package:island/models/file_pool.dart'; import 'package:island/pods/network.dart'; final poolServiceProvider = Provider((ref) { @@ -8,7 +8,7 @@ final poolServiceProvider = Provider((ref) { return PoolService(dio); }); -final poolsProvider = FutureProvider>((ref) async { +final poolsProvider = FutureProvider>((ref) async { final service = ref.watch(poolServiceProvider); return service.fetchPools(); }); diff --git a/lib/screens/account/profile.g.dart b/lib/screens/account/profile.g.dart index b977e0f7..318d54da 100644 --- a/lib/screens/account/profile.g.dart +++ b/lib/screens/account/profile.g.dart @@ -268,7 +268,7 @@ class _AccountBadgesProviderElement } String _$accountAppbarForcegroundColorHash() => - r'8ee0cae10817b77fb09548a482f5247662b4374c'; + r'127fcc7fd6ec6a41ac4a6975276b5271aa4fa7d0'; /// See also [accountAppbarForcegroundColor]. @ProviderFor(accountAppbarForcegroundColor) diff --git a/lib/screens/auth/captcha.config.g.dart b/lib/screens/auth/captcha.config.g.dart index 24ce9089..578422f0 100644 --- a/lib/screens/auth/captcha.config.g.dart +++ b/lib/screens/auth/captcha.config.g.dart @@ -6,7 +6,7 @@ part of 'captcha.config.dart'; // RiverpodGenerator // ************************************************************************** -String _$captchaUrlHash() => r'bbed0d18272dd205069642b3c6583ea2eef735d1'; +String _$captchaUrlHash() => r'd46bc43032cef504547cd528a40c23cf76f27cc8'; /// See also [captchaUrl]. @ProviderFor(captchaUrl) diff --git a/lib/screens/posts/pub_profile.g.dart b/lib/screens/posts/pub_profile.g.dart index 03919de3..3f3ed561 100644 --- a/lib/screens/posts/pub_profile.g.dart +++ b/lib/screens/posts/pub_profile.g.dart @@ -400,7 +400,7 @@ class _PublisherSubscriptionStatusProviderElement } String _$publisherAppbarForcegroundColorHash() => - r'd781a806a242aea5c1609ec98c97c52fdd9f7db1'; + r'cd9a9816177a6eecc2bc354acebbbd48892ffdd7'; /// See also [publisherAppbarForcegroundColor]. @ProviderFor(publisherAppbarForcegroundColor) diff --git a/lib/screens/realm/realm_detail.g.dart b/lib/screens/realm/realm_detail.g.dart index c369db6c..75494f99 100644 --- a/lib/screens/realm/realm_detail.g.dart +++ b/lib/screens/realm/realm_detail.g.dart @@ -7,7 +7,7 @@ part of 'realm_detail.dart'; // ************************************************************************** String _$realmAppbarForegroundColorHash() => - r'14b5563d861996ea182d0d2db7aa5c2bb3bbaf48'; + r'8131c047a984318a4cc3fbb5daa5ef0ce44dfae5'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index c3a591ec..cd28b948 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -373,7 +373,7 @@ class SettingsScreen extends HookConsumerWidget { poolsAsync.when( data: (pools) { final validPools = filterValidPools(pools); - final currentPoolId = resolveDefaultPoolId(ref, pools); + final currentPoolId = resolveDefaultPoolId(ref, validPools); return ListTile( isThreeLine: true, @@ -392,12 +392,14 @@ class SettingsScreen extends HookConsumerWidget { child: DropdownButton2( isExpanded: true, items: - validPools.map((p) { - return DropdownMenuItem( - value: p.id, - child: Text(p.name).fontSize(14), - ); - }).toList(), + validPools + .map( + (p) => DropdownMenuItem( + value: p.id, + child: Text(p.name).fontSize(14), + ), + ) + .toList(), value: currentPoolId, onChanged: (value) { ref diff --git a/lib/services/pool_service.dart b/lib/services/pool_service.dart index bbd120a2..8cef98e6 100644 --- a/lib/services/pool_service.dart +++ b/lib/services/pool_service.dart @@ -7,11 +7,11 @@ class PoolService { PoolService(this._dio); - Future> fetchPools() async { + Future> fetchPools() async { final response = await _dio.get('/drive/pools'); if (response.statusCode == 200) { - return FilePool.listFromResponse(response.data); + return SnFilePoolList.listFromResponse(response.data); } else { throw Exception('Failed to fetch pools: ${response.statusCode}'); } diff --git a/lib/utils/pool_utils.dart b/lib/utils/pool_utils.dart index 12d34d54..e3476a90 100644 --- a/lib/utils/pool_utils.dart +++ b/lib/utils/pool_utils.dart @@ -1,35 +1,37 @@ - -import '../models/file_pool.dart'; +import 'package:island/models/file_pool.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../pods/config.dart'; +import 'package:island/pods/config.dart'; -List filterValidPools(List pools) { +List filterValidPools(List pools) { return pools.where((p) { - final accept = p.policyConfig['accept_types']; - if (accept != null) { + final accept = p.policyConfig?['accept_types']; + + if (accept is List) { final acceptsOnlyMedia = accept.every((t) => - t.startsWith('image/') || - t.startsWith('video/') || - t.startsWith('audio/')); + t is String && + (t.startsWith('image/') || + t.startsWith('video/') || + t.startsWith('audio/'))); if (acceptsOnlyMedia) return false; } + return true; }).toList(); } -String resolveDefaultPoolId(WidgetRef ref, List pools) { +String resolveDefaultPoolId(WidgetRef ref, List pools) { final settings = ref.watch(appSettingsNotifierProvider); final validPools = filterValidPools(pools); - if (settings.defaultPoolId != null && - validPools.any((p) => p.id == settings.defaultPoolId)) { - return settings.defaultPoolId!; + final configuredId = settings.defaultPoolId; + if (configuredId != null && + validPools.any((p) => p.id == configuredId)) { + return configuredId; } if (validPools.isNotEmpty) { return validPools.first.id; } - // DEFAULT: Solar Network Driver - return '500e5ed8-bd44-4359-bc0a-ec85e2adf447'; -} + return '500e5ed8-bd44-4359-bc0a-ec85e2adf447'; // Solar Network Driver +} From 1391fa0ddefef9c3ed3ac8a1c7c91c29b65ac5e9 Mon Sep 17 00:00:00 2001 From: Texas0295 Date: Sun, 21 Sep 2025 22:10:25 +0800 Subject: [PATCH 7/8] [REF] unify pool handling with extension methods - Move pool filtering and parsing logic into SnFilePool extension - Replace PoolService and pool_utils with unified extension - Update settings screen to use pools.filterValid() + resolveDefaultPoolId - Cleanup references in compose_shared.dart - Remove obsolete files: pool_service.dart, pool_utils.dart Signed-off-by: Texas0295 --- lib/models/file_pool.dart | 18 +++++++++++++- lib/pods/pool_provider.dart | 30 ++++++++++++++++------ lib/screens/settings.dart | 20 +++++++-------- lib/services/pool_service.dart | 19 -------------- lib/utils/pool_utils.dart | 37 ---------------------------- lib/widgets/post/compose_shared.dart | 1 - 6 files changed, 48 insertions(+), 77 deletions(-) delete mode 100644 lib/services/pool_service.dart delete mode 100644 lib/utils/pool_utils.dart diff --git a/lib/models/file_pool.dart b/lib/models/file_pool.dart index f84461f8..3659a667 100644 --- a/lib/models/file_pool.dart +++ b/lib/models/file_pool.dart @@ -24,7 +24,7 @@ sealed class SnFilePool with _$SnFilePool { _$SnFilePoolFromJson(json); } -extension SnFilePoolList on SnFilePool { +extension SnFilePoolList on List { static List listFromResponse(dynamic data) { if (data is List) { return data @@ -34,4 +34,20 @@ extension SnFilePoolList on SnFilePool { } throw ArgumentError('Unexpected response format: $data'); } + + List filterValid() { + return where((p) { + final accept = p.policyConfig?['accept_types']; + + if (accept is List) { + final acceptsOnlyMedia = accept.every((t) => + t is String && + (t.startsWith('image/') || + t.startsWith('video/') || + t.startsWith('audio/'))); + if (acceptsOnlyMedia) return false; + } + return true; + }).toList(); + } } diff --git a/lib/pods/pool_provider.dart b/lib/pods/pool_provider.dart index b267fed8..c9de0159 100644 --- a/lib/pods/pool_provider.dart +++ b/lib/pods/pool_provider.dart @@ -1,14 +1,28 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:island/services/pool_service.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file_pool.dart'; +import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; -final poolServiceProvider = Provider((ref) { +final poolsProvider = FutureProvider>((ref) async { final dio = ref.watch(apiClientProvider); - return PoolService(dio); + final response = await dio.get('/drive/pools'); + final pools = SnFilePoolList.listFromResponse(response.data); + return pools.filterValid(); }); -final poolsProvider = FutureProvider>((ref) async { - final service = ref.watch(poolServiceProvider); - return service.fetchPools(); -}); +String resolveDefaultPoolId(WidgetRef ref, List pools) { + final settings = ref.watch(appSettingsNotifierProvider); + final validPools = pools.filterValid(); + + final configuredId = settings.defaultPoolId; + if (configuredId != null && validPools.any((p) => p.id == configuredId)) { + return configuredId; + } + + if (validPools.isNotEmpty) { + return validPools.first.id; + } + + // DEFAULT: Solar Network Driver + return '500e5ed8-bd44-4359-bc0a-ec85e2adf447'; } + diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index cd28b948..272babd9 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -21,7 +21,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/pool_provider.dart'; -import 'package:island/utils/pool_utils.dart'; +import 'package:island/models/file_pool.dart'; class SettingsScreen extends HookConsumerWidget { const SettingsScreen({super.key}); @@ -372,8 +372,8 @@ class SettingsScreen extends HookConsumerWidget { poolsAsync.when( data: (pools) { - final validPools = filterValidPools(pools); - final currentPoolId = resolveDefaultPoolId(ref, validPools); + final validPools = pools.filterValid(); + final currentPoolId = resolveDefaultPoolId(ref, pools); return ListTile( isThreeLine: true, @@ -392,14 +392,12 @@ class SettingsScreen extends HookConsumerWidget { child: DropdownButton2( isExpanded: true, items: - validPools - .map( - (p) => DropdownMenuItem( - value: p.id, - child: Text(p.name).fontSize(14), - ), - ) - .toList(), + validPools.map((p) { + return DropdownMenuItem( + value: p.id, + child: Text(p.name).fontSize(14), + ); + }).toList(), value: currentPoolId, onChanged: (value) { ref diff --git a/lib/services/pool_service.dart b/lib/services/pool_service.dart deleted file mode 100644 index 8cef98e6..00000000 --- a/lib/services/pool_service.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:dio/dio.dart'; - -import 'package:island/models/file_pool.dart'; - -class PoolService { - final Dio _dio; - - PoolService(this._dio); - - Future> fetchPools() async { - final response = await _dio.get('/drive/pools'); - - if (response.statusCode == 200) { - return SnFilePoolList.listFromResponse(response.data); - } else { - throw Exception('Failed to fetch pools: ${response.statusCode}'); - } - } -} diff --git a/lib/utils/pool_utils.dart b/lib/utils/pool_utils.dart deleted file mode 100644 index e3476a90..00000000 --- a/lib/utils/pool_utils.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:island/models/file_pool.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:island/pods/config.dart'; - -List filterValidPools(List pools) { - return pools.where((p) { - final accept = p.policyConfig?['accept_types']; - - if (accept is List) { - final acceptsOnlyMedia = accept.every((t) => - t is String && - (t.startsWith('image/') || - t.startsWith('video/') || - t.startsWith('audio/'))); - if (acceptsOnlyMedia) return false; - } - - return true; - }).toList(); -} - -String resolveDefaultPoolId(WidgetRef ref, List pools) { - final settings = ref.watch(appSettingsNotifierProvider); - final validPools = filterValidPools(pools); - - final configuredId = settings.defaultPoolId; - if (configuredId != null && - validPools.any((p) => p.id == configuredId)) { - return configuredId; - } - - if (validPools.isNotEmpty) { - return validPools.first.id; - } - - return '500e5ed8-bd44-4359-bc0a-ec85e2adf447'; // Solar Network Driver -} diff --git a/lib/widgets/post/compose_shared.dart b/lib/widgets/post/compose_shared.dart index e94f0e5d..c9302376 100644 --- a/lib/widgets/post/compose_shared.dart +++ b/lib/widgets/post/compose_shared.dart @@ -21,7 +21,6 @@ import 'package:island/widgets/post/compose_link_attachments.dart'; import 'package:island/widgets/post/compose_poll.dart'; import 'package:island/widgets/post/compose_recorder.dart'; import 'package:island/pods/pool_provider.dart'; -import 'package:island/utils/pool_utils.dart'; import 'package:pasteboard/pasteboard.dart'; import 'package:textfield_tags/textfield_tags.dart'; import 'dart:async'; From ace302111aec9dd6e4b3b9389ad53192230b8494 Mon Sep 17 00:00:00 2001 From: Texas0295 Date: Sun, 21 Sep 2025 22:38:49 +0800 Subject: [PATCH 8/8] [REF] unify file upload logic and pool utils - merge putMediaToCloud and putFileToPool into putFileToCloud with FileUploadMode for media-safe vs generic uploads Signed-off-by: Texas0295 --- lib/screens/account/me/profile_update.dart | 2 +- lib/screens/chat/chat.dart | 2 +- lib/screens/chat/room.dart | 2 +- lib/screens/creators/publishers.dart | 2 +- lib/screens/developers/edit_app.dart | 2 +- lib/screens/developers/edit_bot.dart | 2 +- lib/screens/realm/realms.dart | 2 +- lib/services/file.dart | 114 ++++++--------------- lib/widgets/content/cloud_file_picker.dart | 2 +- lib/widgets/post/compose_shared.dart | 68 ++++++------ lib/widgets/share/share_sheet.dart | 2 +- 11 files changed, 69 insertions(+), 131 deletions(-) diff --git a/lib/screens/account/me/profile_update.dart b/lib/screens/account/me/profile_update.dart index 41cd808a..6a7241ad 100644 --- a/lib/screens/account/me/profile_update.dart +++ b/lib/screens/account/me/profile_update.dart @@ -66,7 +66,7 @@ class UpdateProfileScreen extends HookConsumerWidget { final token = await getToken(ref.watch(tokenProvider)); if (token == null) throw ArgumentError('Token is null'); final cloudFile = - await putMediaToCloud( + await putFileToCloud( fileData: UniversalFile( data: result, type: UniversalFileType.image, diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 6c08093e..64175afa 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -543,7 +543,7 @@ class EditChatScreen extends HookConsumerWidget { final token = await getToken(ref.watch(tokenProvider)); if (token == null) throw ArgumentError('Token is null'); final cloudFile = - await putMediaToCloud( + await putFileToCloud( fileData: UniversalFile( data: result, type: UniversalFileType.image, diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 7332c910..4eba2c06 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -649,7 +649,7 @@ Future loadMore() async { var cloudAttachments = List.empty(growable: true); for (var idx = 0; idx < attachments.length; idx++) { final cloudFile = - await putMediaToCloud( + await putFileToCloud( fileData: attachments[idx], atk: token, baseUrl: baseUrl, diff --git a/lib/screens/creators/publishers.dart b/lib/screens/creators/publishers.dart index 6a329616..cc96720e 100644 --- a/lib/screens/creators/publishers.dart +++ b/lib/screens/creators/publishers.dart @@ -98,7 +98,7 @@ class EditPublisherScreen extends HookConsumerWidget { final token = await getToken(ref.watch(tokenProvider)); if (token == null) throw ArgumentError('Token is null'); final cloudFile = - await putMediaToCloud( + await putFileToCloud( fileData: UniversalFile( data: result, type: UniversalFileType.image, diff --git a/lib/screens/developers/edit_app.dart b/lib/screens/developers/edit_app.dart index e4464e0d..fda47fae 100644 --- a/lib/screens/developers/edit_app.dart +++ b/lib/screens/developers/edit_app.dart @@ -141,7 +141,7 @@ class EditAppScreen extends HookConsumerWidget { final token = await getToken(ref.watch(tokenProvider)); if (token == null) throw ArgumentError('Token is null'); final cloudFile = - await putMediaToCloud( + await putFileToCloud( fileData: UniversalFile( data: result, type: UniversalFileType.image, diff --git a/lib/screens/developers/edit_bot.dart b/lib/screens/developers/edit_bot.dart index fb0b8fcc..3350fafb 100644 --- a/lib/screens/developers/edit_bot.dart +++ b/lib/screens/developers/edit_bot.dart @@ -127,7 +127,7 @@ class EditBotScreen extends HookConsumerWidget { final token = await getToken(ref.watch(tokenProvider)); if (token == null) throw ArgumentError('Token is null'); final cloudFile = - await putMediaToCloud( + await putFileToCloud( fileData: UniversalFile( data: result, type: UniversalFileType.image, diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index 01a8b483..d421e593 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -211,7 +211,7 @@ class EditRealmScreen extends HookConsumerWidget { final token = await getToken(ref.watch(tokenProvider)); if (token == null) throw ArgumentError('Access token is null'); final cloudFile = - await putMediaToCloud( + await putFileToCloud( fileData: UniversalFile( data: result, type: UniversalFileType.image, diff --git a/lib/services/file.dart b/lib/services/file.dart index a2ac2a26..02b3ff30 100644 --- a/lib/services/file.dart +++ b/lib/services/file.dart @@ -11,6 +11,8 @@ import 'package:island/services/file_uploader.dart'; import 'package:native_exif/native_exif.dart'; import 'package:path_provider/path_provider.dart'; +enum FileUploadMode { generic, mediaSafe } + Future cropImage( BuildContext context, { required XFile image, @@ -40,98 +42,46 @@ Future cropImage( ); } -Completer putFileToPool({ - required UniversalFile fileData, - required String atk, - required String baseUrl, - required String poolId, - String? filename, - String? mimetype, - Function(double progress, Duration estimate)? onProgress, -}) { - final completer = Completer(); - final data = fileData.data; - if (data is! XFile) { - completer.completeError( - ArgumentError('Unsupported fileData type for putFileToPool'), - ); - return completer; - } - - final actualFilename = filename ?? data.name; - final actualMimetype = mimetype ?? data.mimeType ?? 'application/octet-stream'; - - final dio = Dio(BaseOptions( - baseUrl: baseUrl, - headers: { - 'Authorization': 'AtField $atk', - 'Accept': 'application/json', - }, - )); - - final uploader = FileUploader(dio); - final fileObj = File(data.path); - - onProgress?.call(0.0, Duration.zero); - uploader.uploadFile( - file: fileObj, - fileName: actualFilename, - contentType: actualMimetype, - poolId: poolId, - ).then((result) { - onProgress?.call(1.0, Duration.zero); - completer.complete(result); - }).catchError((e) { - completer.completeError(e); - }); - - return completer; -} - - -Completer putMediaToCloud({ +Completer putFileToCloud({ required UniversalFile fileData, required String atk, required String baseUrl, String? poolId, String? filename, String? mimetype, + FileUploadMode? mode, Function(double progress, Duration estimate)? onProgress, }) { final completer = Completer(); - // Process the image to remove GPS EXIF data if needed - if (fileData.isOnDevice && fileData.type == UniversalFileType.image) { + final effectiveMode = + mode ?? + (fileData.type == UniversalFileType.file + ? FileUploadMode.generic + : FileUploadMode.mediaSafe); + + if (effectiveMode == FileUploadMode.mediaSafe && + fileData.isOnDevice && + fileData.type == UniversalFileType.image) { final data = fileData.data; if (data is XFile && !kIsWeb && (Platform.isIOS || Platform.isAndroid)) { - // Use native_exif to selectively remove GPS data Exif.fromPath(data.path) - .then((exif) { - // Remove GPS-related attributes - final gpsAttributes = [ - 'GPSLatitude', - 'GPSLatitudeRef', - 'GPSLongitude', - 'GPSLongitudeRef', - 'GPSAltitude', - 'GPSAltitudeRef', - 'GPSTimeStamp', - 'GPSProcessingMethod', - 'GPSDateStamp', - ]; - - // Create a map of attributes to clear - final clearAttributes = {}; - for (final attr in gpsAttributes) { - clearAttributes[attr] = ''; - } - - // Write empty values to remove GPS data - return exif.writeAttributes(clearAttributes); + .then((exif) async { + final gpsAttributes = { + 'GPSLatitude': '', + 'GPSLatitudeRef': '', + 'GPSLongitude': '', + 'GPSLongitudeRef': '', + 'GPSAltitude': '', + 'GPSAltitudeRef': '', + 'GPSTimeStamp': '', + 'GPSProcessingMethod': '', + 'GPSDateStamp': '', + }; + await exif.writeAttributes(gpsAttributes); }) - .then((_) { - // Continue with upload after GPS data is removed - _processUpload( + .then( + (_) => _processUpload( fileData, atk, baseUrl, @@ -140,12 +90,11 @@ Completer putMediaToCloud({ mimetype, onProgress, completer, - ); - }) + ), + ) .catchError((e) { - // If there's an error, continue with the original file debugPrint('Error removing GPS EXIF data: $e'); - _processUpload( + return _processUpload( fileData, atk, baseUrl, @@ -161,7 +110,6 @@ Completer putMediaToCloud({ } } - // If not an image or on web, continue with normal upload _processUpload( fileData, atk, diff --git a/lib/widgets/content/cloud_file_picker.dart b/lib/widgets/content/cloud_file_picker.dart index 3cc8c685..0101855b 100644 --- a/lib/widgets/content/cloud_file_picker.dart +++ b/lib/widgets/content/cloud_file_picker.dart @@ -55,7 +55,7 @@ class CloudFilePicker extends HookConsumerWidget { uploadPosition.value = idx; final file = files.value[idx]; final cloudFile = - await putMediaToCloud( + await putFileToCloud( fileData: file, atk: token, baseUrl: baseUrl, diff --git a/lib/widgets/post/compose_shared.dart b/lib/widgets/post/compose_shared.dart index c9302376..15de2b4f 100644 --- a/lib/widgets/post/compose_shared.dart +++ b/lib/widgets/post/compose_shared.dart @@ -185,7 +185,7 @@ class ComposeLogic { if (attachment.data is! SnCloudFile) { try { final cloudFile = - await putMediaToCloud( + await putFileToCloud( fileData: attachment, atk: token, baseUrl: baseUrl, @@ -523,44 +523,34 @@ class ComposeLogic { SnCloudFile? cloudFile; - final pools = await ref.read(poolsProvider.future); - final selectedPoolId = resolveDefaultPoolId(ref, pools); - if (attachment.type == UniversalFileType.file) { - cloudFile = - await putFileToPool( - fileData: attachment, - atk: token, - baseUrl: baseUrl, - poolId: selectedPoolId, - filename: attachment.data.name ?? 'General file', - mimetype: - attachment.data.mimeType ?? - getMimeTypeFromFileType(attachment.type), - onProgress: (progress, _) { - state.attachmentProgress.value = { - ...state.attachmentProgress.value, - index: progress, - }; - }, - ).future; - } else { - cloudFile = - await putMediaToCloud( - fileData: attachment, - atk: token, - baseUrl: baseUrl, - filename: attachment.data.name ?? 'Post media', - mimetype: - attachment.data.mimeType ?? - getMimeTypeFromFileType(attachment.type), - onProgress: (progress, _) { - state.attachmentProgress.value = { - ...state.attachmentProgress.value, - index: progress, - }; - }, - ).future; - } + final pools = await ref.read(poolsProvider.future); + final selectedPoolId = resolveDefaultPoolId(ref, pools); + + cloudFile = + await putFileToCloud( + fileData: attachment, + atk: token, + baseUrl: baseUrl, + poolId: selectedPoolId, + filename: + attachment.data.name ?? + (attachment.type == UniversalFileType.file + ? 'General file' + : 'Post media'), + mimetype: + attachment.data.mimeType ?? + getMimeTypeFromFileType(attachment.type), + mode: + attachment.type == UniversalFileType.file + ? FileUploadMode.generic + : FileUploadMode.mediaSafe, + onProgress: (progress, _) { + state.attachmentProgress.value = { + ...state.attachmentProgress.value, + index: progress, + }; + }, + ).future; if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); diff --git a/lib/widgets/share/share_sheet.dart b/lib/widgets/share/share_sheet.dart index 037f8203..96b6be82 100644 --- a/lib/widgets/share/share_sheet.dart +++ b/lib/widgets/share/share_sheet.dart @@ -247,7 +247,7 @@ class _ShareSheetState extends ConsumerState { for (var idx = 0; idx < universalFiles.length; idx++) { final file = universalFiles[idx]; final cloudFile = - await putMediaToCloud( + await putFileToCloud( fileData: file, atk: token, baseUrl: serverUrl,