diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 120b957d..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", @@ -168,6 +170,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..69004ba2 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -122,6 +122,9 @@ "addVideo": "添加视频", "addPhoto": "添加照片", "addFile": "添加文件", + "uploadFile": "上传文件", + "settingsDefaultPool": "选择文件池", + "settingsDefaultPoolHelper": "为文件上传选择一个默认池", "createDirectMessage": "创建新私人消息", "gotoDirectMessage": "前往私信", "react": "反应", diff --git a/assets/i18n/zh-TW.json b/assets/i18n/zh-TW.json index d8d56cfe..e3d6f836 100644 --- a/assets/i18n/zh-TW.json +++ b/assets/i18n/zh-TW.json @@ -122,6 +122,10 @@ "addVideo": "添加視頻", "addPhoto": "添加照片", "addFile": "添加文件", + "uploadFile": "上傳文件", + "settingsDefaultPool": "選擇文件池", + "settingsDefaultPoolHelper": "爲文件上傳選擇一個默認池", + "createDirectMessage": "創建新私人消息", "gotoDirectMessage": "前往私信", "react": "反應", diff --git a/lib/models/file_pool.dart b/lib/models/file_pool.dart new file mode 100644 index 00000000..3659a667 --- /dev/null +++ b/lib/models/file_pool.dart @@ -0,0 +1,53 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'file_pool.freezed.dart'; +part 'file_pool.g.dart'; + +@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; + + factory SnFilePool.fromJson(Map json) => + _$SnFilePoolFromJson(json); +} + +extension SnFilePoolList on List { + static List listFromResponse(dynamic data) { + if (data is List) { + return data + .whereType>() + .map(SnFilePool.fromJson) + .toList(); + } + 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/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/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/pods/pool_provider.dart b/lib/pods/pool_provider.dart new file mode 100644 index 00000000..c9de0159 --- /dev/null +++ b/lib/pods/pool_provider.dart @@ -0,0 +1,28 @@ +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 poolsProvider = FutureProvider>((ref) async { + final dio = ref.watch(apiClientProvider); + final response = await dio.get('/drive/pools'); + final pools = SnFilePoolList.listFromResponse(response.data); + return pools.filterValid(); +}); + +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/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/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/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/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/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/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/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/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/screens/settings.dart b/lib/screens/settings.dart index c818d5ac..272babd9 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -20,6 +20,8 @@ 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'; +import 'package:island/models/file_pool.dart'; class SettingsScreen extends HookConsumerWidget { const SettingsScreen({super.key}); @@ -33,7 +35,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 +369,66 @@ class SettingsScreen extends HookConsumerWidget { ), ), ), + + poolsAsync.when( + data: (pools) { + final validPools = pools.filterValid(); + final currentPoolId = resolveDefaultPoolId(ref, 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( + validPools + .firstWhereOrNull((p) => p.id == currentPoolId) + ?.description ?? + 'settingsDefaultPoolHelper'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + trailing: DropdownButtonHideUnderline( + child: DropdownButton2( + isExpanded: true, + items: + validPools.map((p) { + return DropdownMenuItem( + value: p.id, + child: Text(p.name).fontSize(14), + ); + }).toList(), + value: currentPoolId, + 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/services/file.dart b/lib/services/file.dart index 4971a987..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,49 +42,46 @@ Future cropImage( ); } -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, @@ -91,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, @@ -112,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 84b36b16..15de2b4f 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'; @@ -19,6 +20,7 @@ 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:pasteboard/pasteboard.dart'; import 'package:textfield_tags/textfield_tags.dart'; import 'dart:async'; @@ -183,7 +185,7 @@ class ComposeLogic { if (attachment.data is! SnCloudFile) { try { final cloudFile = - await putMediaToCloud( + await putFileToCloud( fileData: attachment, atk: token, baseUrl: baseUrl, @@ -386,6 +388,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 +505,9 @@ class ComposeLogic { static Future uploadAttachment( WidgetRef ref, ComposeState state, - int index, - ) async { + int index, { + String? poolId, // For Unit Test + }) async { final attachment = state.attachments.value[index]; if (attachment.isOnCloud) return; @@ -489,22 +516,34 @@ 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( + SnCloudFile? cloudFile; + + final pools = await ref.read(poolsProvider.future); + final selectedPoolId = resolveDefaultPoolId(ref, pools); + + cloudFile = + await putFileToCloud( fileData: attachment, atk: token, baseUrl: baseUrl, - filename: attachment.data.name ?? 'Post media', + 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, @@ -517,14 +556,12 @@ class ComposeLogic { 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 +680,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), 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,