Compare commits
	
		
			27 Commits
		
	
	
		
			ecc100ac45
			...
			3.2.0+131
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6f9d51673b | |||
| f8c6887769 | |||
| cd2a507b7f | |||
| 3cafce00a2 | |||
| 837f3fbe98 | |||
| ca7cc5d7ee | |||
| ef2c14daa2 | |||
| 3a17837cc6 | |||
| 2617a64acf | |||
| afe1e12a3b | |||
| be80f5ff85 | |||
| 3281d69eba | |||
| 77b6ce9937 | |||
| 39275f61b5 | |||
| 72193ba8f3 | |||
| 98dd9b6617 | |||
| a22b94a263 | |||
| 9c75eafdb3 | |||
| 28fda3d0c7 | |||
| 187c2ea43e | |||
| ae7d967461 | |||
| 1ce71f1fa1 | |||
| 9b68808c77 | |||
|  | 99b7bf8199 | ||
|  | eb9bb73c31 | ||
|  | a8c3830d67 | ||
|  | 07a5a19141 | 
| @@ -195,6 +195,7 @@ | |||||||
|   "checkInResultLevel2": "A Normal Day", |   "checkInResultLevel2": "A Normal Day", | ||||||
|   "checkInResultLevel3": "Good Luck", |   "checkInResultLevel3": "Good Luck", | ||||||
|   "checkInResultLevel4": "Best Luck", |   "checkInResultLevel4": "Best Luck", | ||||||
|  |   "checkInResultLevel5": "Happy Birthday 🥳", | ||||||
|   "checkInActivityTitle": "{} checked in on {} and got a {}", |   "checkInActivityTitle": "{} checked in on {} and got a {}", | ||||||
|   "eventCalander": "Event Calander", |   "eventCalander": "Event Calander", | ||||||
|   "eventCalanderEmpty": "No events on that day.", |   "eventCalanderEmpty": "No events on that day.", | ||||||
| @@ -228,6 +229,8 @@ | |||||||
|   "settings": "Settings", |   "settings": "Settings", | ||||||
|   "language": "Language", |   "language": "Language", | ||||||
|   "accountLanguageHint": "This language will be used for email and push notifications.", |   "accountLanguageHint": "This language will be used for email and push notifications.", | ||||||
|  |   "region": "Region", | ||||||
|  |   "accountRegionHint": "This region will be used for content delivery and localization.", | ||||||
|   "settingsDisplayLanguage": "Display Language", |   "settingsDisplayLanguage": "Display Language", | ||||||
|   "languageFollowSystem": "Follow System", |   "languageFollowSystem": "Follow System", | ||||||
|   "postsCreatedCount": "Posts", |   "postsCreatedCount": "Posts", | ||||||
| @@ -349,6 +352,8 @@ | |||||||
|   "chatBreakNone": "None", |   "chatBreakNone": "None", | ||||||
|   "settingsRealmCompactView": "Compact Realm View", |   "settingsRealmCompactView": "Compact Realm View", | ||||||
|   "settingsMixedFeed": "Mixed Feed", |   "settingsMixedFeed": "Mixed Feed", | ||||||
|  |   "settingsDataSavingMode": "Data Saving Mode", | ||||||
|  |   "dataSavingHint": "Data Saving Mode", | ||||||
|   "settingsAutoTranslate": "Auto Translate", |   "settingsAutoTranslate": "Auto Translate", | ||||||
|   "settingsHideBottomNav": "Hide Bottom Navigation", |   "settingsHideBottomNav": "Hide Bottom Navigation", | ||||||
|   "settingsSoundEffects": "Sound Effects", |   "settingsSoundEffects": "Sound Effects", | ||||||
| @@ -971,5 +976,6 @@ | |||||||
|   "shuffle": "Shuffle", |   "shuffle": "Shuffle", | ||||||
|   "pinned": "Pinned", |   "pinned": "Pinned", | ||||||
|   "noResultsFound": "No results found", |   "noResultsFound": "No results found", | ||||||
|   "toggleFilters": "Toggle filters" |   "toggleFilters": "Toggle filters", | ||||||
|  |   "notableDayNext": "{} is in" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -158,11 +158,12 @@ | |||||||
|   "checkIn": "签到", |   "checkIn": "签到", | ||||||
|   "checkInNone": "尚未签到", |   "checkInNone": "尚未签到", | ||||||
|   "checkInNoneHint": "通过签到获取您的财富提示和每日奖励。", |   "checkInNoneHint": "通过签到获取您的财富提示和每日奖励。", | ||||||
|   "checkInResultLevel0": "最差运气", |   "checkInResultLevel0": "大凶", | ||||||
|   "checkInResultLevel1": "坏运气", |   "checkInResultLevel1": "凶", | ||||||
|   "checkInResultLevel2": "一个普通的日常", |   "checkInResultLevel2": "中平", | ||||||
|   "checkInResultLevel3": "好运", |   "checkInResultLevel3": "吉", | ||||||
|   "checkInResultLevel4": "最佳运气", |   "checkInResultLevel4": "大吉", | ||||||
|  |   "checkInResultLevel5": "生日快乐 🥳", | ||||||
|   "checkInActivityTitle": "{} 在 {} 签到并获得了 {}", |   "checkInActivityTitle": "{} 在 {} 签到并获得了 {}", | ||||||
|   "eventCalander": "活动日历", |   "eventCalander": "活动日历", | ||||||
|   "eventCalanderEmpty": "该日无活动。", |   "eventCalanderEmpty": "该日无活动。", | ||||||
| @@ -315,6 +316,8 @@ | |||||||
|   "chatBreakNone": "无", |   "chatBreakNone": "无", | ||||||
|   "settingsRealmCompactView": "紧凑领域视图", |   "settingsRealmCompactView": "紧凑领域视图", | ||||||
|   "settingsMixedFeed": "混合动态", |   "settingsMixedFeed": "混合动态", | ||||||
|  |   "settingsDataSavingMode": "流量节省模式", | ||||||
|  |   "dataSavingHint": "流量节省模式", | ||||||
|   "settingsAutoTranslate": "自动翻译", |   "settingsAutoTranslate": "自动翻译", | ||||||
|   "settingsHideBottomNav": "隐藏底部导航", |   "settingsHideBottomNav": "隐藏底部导航", | ||||||
|   "settingsSoundEffects": "音效", |   "settingsSoundEffects": "音效", | ||||||
| @@ -860,5 +863,6 @@ | |||||||
|   "statusPresent": "至今", |   "statusPresent": "至今", | ||||||
|   "accountAutomated": "机器人", |   "accountAutomated": "机器人", | ||||||
|   "openInBrowser": "在浏览器中打开", |   "openInBrowser": "在浏览器中打开", | ||||||
|   "highlightPost": "精选帖子" |   "highlightPost": "精选帖子", | ||||||
|  |   "notableDayNext": "距离 {} 还有" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -315,6 +315,8 @@ | |||||||
|     "settingsRealmCompactView": "緊湊領域視圖", |     "settingsRealmCompactView": "緊湊領域視圖", | ||||||
|     "settingsMixedFeed": "混合動態", |     "settingsMixedFeed": "混合動態", | ||||||
|     "settingsAutoTranslate": "自動翻譯", |     "settingsAutoTranslate": "自動翻譯", | ||||||
|  |     "settingsDataSavingMode": "低數據模式", | ||||||
|  |     "dataSavingHint": "低數據模式", | ||||||
|     "settingsHideBottomNav": "隱藏底部導航", |     "settingsHideBottomNav": "隱藏底部導航", | ||||||
|     "settingsSoundEffects": "音效", |     "settingsSoundEffects": "音效", | ||||||
|     "settingsAprilFoolFeatures": "愚人節功能", |     "settingsAprilFoolFeatures": "愚人節功能", | ||||||
|   | |||||||
| @@ -136,6 +136,8 @@ PODS: | |||||||
|     - OrderedSet (~> 6.0.3) |     - OrderedSet (~> 6.0.3) | ||||||
|   - flutter_keyboard_visibility (0.0.1): |   - flutter_keyboard_visibility (0.0.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|  |   - flutter_local_notifications (0.0.1): | ||||||
|  |     - Flutter | ||||||
|   - flutter_native_splash (2.4.3): |   - flutter_native_splash (2.4.3): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - flutter_platform_alert (0.0.1): |   - flutter_platform_alert (0.0.1): | ||||||
| @@ -314,6 +316,7 @@ DEPENDENCIES: | |||||||
|   - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`) |   - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`) | ||||||
|   - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) |   - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) | ||||||
|   - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) |   - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) | ||||||
|  |   - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) | ||||||
|   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) |   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) | ||||||
|   - flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`) |   - flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`) | ||||||
|   - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) |   - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) | ||||||
| @@ -402,6 +405,8 @@ EXTERNAL SOURCES: | |||||||
|     :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" |     :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" | ||||||
|   flutter_keyboard_visibility: |   flutter_keyboard_visibility: | ||||||
|     :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" |     :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" | ||||||
|  |   flutter_local_notifications: | ||||||
|  |     :path: ".symlinks/plugins/flutter_local_notifications/ios" | ||||||
|   flutter_native_splash: |   flutter_native_splash: | ||||||
|     :path: ".symlinks/plugins/flutter_native_splash/ios" |     :path: ".symlinks/plugins/flutter_native_splash/ios" | ||||||
|   flutter_platform_alert: |   flutter_platform_alert: | ||||||
| @@ -488,6 +493,7 @@ SPEC CHECKSUMS: | |||||||
|   flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9 |   flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9 | ||||||
|   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 |   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 | ||||||
|   flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 |   flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 | ||||||
|  |   flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb | ||||||
|   flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf |   flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf | ||||||
|   flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 |   flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 | ||||||
|   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 |   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 | ||||||
|   | |||||||
| @@ -47,6 +47,7 @@ class NotificationService: UNNotificationServiceExtension { | |||||||
|     private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws { |     private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws { | ||||||
|         switch content.userInfo["type"] as? String { |         switch content.userInfo["type"] as? String { | ||||||
|         case "messages.new": |         case "messages.new": | ||||||
|  |             content.categoryIdentifier = "REPLYABLE_MESSAGE" | ||||||
|             try handleMessagingNotification(request: request, content: content) |             try handleMessagingNotification(request: request, content: content) | ||||||
|         default: |         default: | ||||||
|             try handleDefaultNotification(content: content) |             try handleDefaultNotification(content: content) | ||||||
| @@ -60,8 +61,6 @@ class NotificationService: UNNotificationServiceExtension { | |||||||
|          |          | ||||||
|         let pfpIdentifier = meta["pfp"] as? String |         let pfpIdentifier = meta["pfp"] as? String | ||||||
|          |          | ||||||
|         content.categoryIdentifier = "REPLYABLE_MESSAGE" |  | ||||||
|          |  | ||||||
|         let metaCopy = meta as? [String: Any] ?? [:] |         let metaCopy = meta as? [String: Any] ?? [:] | ||||||
|         let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil |         let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil | ||||||
|          |          | ||||||
|   | |||||||
| @@ -240,6 +240,7 @@ class IslandApp extends HookConsumerWidget { | |||||||
|       themeMode: ThemeMode.system, |       themeMode: ThemeMode.system, | ||||||
|       routerConfig: router, |       routerConfig: router, | ||||||
|       supportedLocales: context.supportedLocales, |       supportedLocales: context.supportedLocales, | ||||||
|  |       scrollBehavior: AppScrollBehavior(), | ||||||
|       localizationsDelegates: [ |       localizationsDelegates: [ | ||||||
|         ...context.localizationDelegates, |         ...context.localizationDelegates, | ||||||
|         CroppyLocalizations.delegate, |         CroppyLocalizations.delegate, | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ sealed class SnAccount with _$SnAccount { | |||||||
|     required String name, |     required String name, | ||||||
|     required String nick, |     required String nick, | ||||||
|     required String language, |     required String language, | ||||||
|  |     @Default("") String region, | ||||||
|     required bool isSuperuser, |     required bool isSuperuser, | ||||||
|     required String? automatedId, |     required String? automatedId, | ||||||
|     required SnAccountProfile profile, |     required SnAccountProfile profile, | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ T _$identity<T>(T value) => value; | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnAccount { | mixin _$SnAccount { | ||||||
|  |  | ||||||
|  String get id; String get name; String get nick; String get language; bool get isSuperuser; String? get automatedId; SnAccountProfile get profile; SnWalletSubscriptionRef? get perkSubscription; List<SnAccountBadge> get badges; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; |  String get id; String get name; String get nick; String get language; String get region; bool get isSuperuser; String? get automatedId; SnAccountProfile get profile; SnWalletSubscriptionRef? get perkSubscription; List<SnAccountBadge> get badges; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||||
| /// Create a copy of SnAccount | /// Create a copy of SnAccount | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @@ -28,16 +28,16 @@ $SnAccountCopyWith<SnAccount> get copyWith => _$SnAccountCopyWithImpl<SnAccount> | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.automatedId, automatedId) || other.automatedId == automatedId)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other.badges, badges)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.region, region) || other.region == region)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.automatedId, automatedId) || other.automatedId == automatedId)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other.badges, badges)&&(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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,automatedId,profile,perkSubscription,const DeepCollectionEquality().hash(badges),createdAt,updatedAt,deletedAt); | int get hashCode => Object.hash(runtimeType,id,name,nick,language,region,isSuperuser,automatedId,profile,perkSubscription,const DeepCollectionEquality().hash(badges),createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, automatedId: $automatedId, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, region: $region, isSuperuser: $isSuperuser, automatedId: $automatedId, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -48,7 +48,7 @@ abstract mixin class $SnAccountCopyWith<$Res>  { | |||||||
|   factory $SnAccountCopyWith(SnAccount value, $Res Function(SnAccount) _then) = _$SnAccountCopyWithImpl; |   factory $SnAccountCopyWith(SnAccount value, $Res Function(SnAccount) _then) = _$SnAccountCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String name, String nick, String language, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, String name, String nick, String language, String region, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -65,12 +65,13 @@ class _$SnAccountCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnAccount | /// Create a copy of SnAccount | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// 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? nick = null,Object? language = null,Object? isSuperuser = null,Object? automatedId = freezed,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? region = null,Object? isSuperuser = null,Object? automatedId = freezed,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | 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,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||||
| as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable | as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable | ||||||
| as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable | as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,region: null == region ? _self.region : region // ignore: cast_nullable_to_non_nullable | ||||||
| as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable | as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,automatedId: freezed == automatedId ? _self.automatedId : automatedId // ignore: cast_nullable_to_non_nullable | as bool,automatedId: freezed == automatedId ? _self.automatedId : automatedId // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable | as String?,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -182,10 +183,10 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String nick,  String language,  bool isSuperuser,  String? automatedId,  SnAccountProfile profile,  SnWalletSubscriptionRef? perkSubscription,  List<SnAccountBadge> badges,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String nick,  String language,  String region,  bool isSuperuser,  String? automatedId,  SnAccountProfile profile,  SnWalletSubscriptionRef? perkSubscription,  List<SnAccountBadge> badges,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAccount() when $default != null: | case _SnAccount() when $default != null: | ||||||
| return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.name,_that.nick,_that.language,_that.region,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|   return orElse(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -203,10 +204,10 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser, | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String nick,  String language,  bool isSuperuser,  String? automatedId,  SnAccountProfile profile,  SnWalletSubscriptionRef? perkSubscription,  List<SnAccountBadge> badges,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String nick,  String language,  String region,  bool isSuperuser,  String? automatedId,  SnAccountProfile profile,  SnWalletSubscriptionRef? perkSubscription,  List<SnAccountBadge> badges,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAccount(): | case _SnAccount(): | ||||||
| return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);} | return $default(_that.id,_that.name,_that.nick,_that.language,_that.region,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||||
| } | } | ||||||
| /// A variant of `when` that fallback to returning `null` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -220,10 +221,10 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser, | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String nick,  String language,  bool isSuperuser,  String? automatedId,  SnAccountProfile profile,  SnWalletSubscriptionRef? perkSubscription,  List<SnAccountBadge> badges,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String nick,  String language,  String region,  bool isSuperuser,  String? automatedId,  SnAccountProfile profile,  SnWalletSubscriptionRef? perkSubscription,  List<SnAccountBadge> badges,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAccount() when $default != null: | case _SnAccount() when $default != null: | ||||||
| return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.name,_that.nick,_that.language,_that.region,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|   return null; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -235,13 +236,14 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser, | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnAccount implements SnAccount { | class _SnAccount implements SnAccount { | ||||||
|   const _SnAccount({required this.id, required this.name, required this.nick, required this.language, required this.isSuperuser, required this.automatedId, required this.profile, required this.perkSubscription, final  List<SnAccountBadge> badges = const [], required this.createdAt, required this.updatedAt, required this.deletedAt}): _badges = badges; |   const _SnAccount({required this.id, required this.name, required this.nick, required this.language, this.region = "", required this.isSuperuser, required this.automatedId, required this.profile, required this.perkSubscription, final  List<SnAccountBadge> badges = const [], required this.createdAt, required this.updatedAt, required this.deletedAt}): _badges = badges; | ||||||
|   factory _SnAccount.fromJson(Map<String, dynamic> json) => _$SnAccountFromJson(json); |   factory _SnAccount.fromJson(Map<String, dynamic> json) => _$SnAccountFromJson(json); | ||||||
|  |  | ||||||
| @override final  String id; | @override final  String id; | ||||||
| @override final  String name; | @override final  String name; | ||||||
| @override final  String nick; | @override final  String nick; | ||||||
| @override final  String language; | @override final  String language; | ||||||
|  | @override@JsonKey() final  String region; | ||||||
| @override final  bool isSuperuser; | @override final  bool isSuperuser; | ||||||
| @override final  String? automatedId; | @override final  String? automatedId; | ||||||
| @override final  SnAccountProfile profile; | @override final  SnAccountProfile profile; | ||||||
| @@ -270,16 +272,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.automatedId, automatedId) || other.automatedId == automatedId)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other._badges, _badges)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.region, region) || other.region == region)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.automatedId, automatedId) || other.automatedId == automatedId)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other._badges, _badges)&&(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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,automatedId,profile,perkSubscription,const DeepCollectionEquality().hash(_badges),createdAt,updatedAt,deletedAt); | int get hashCode => Object.hash(runtimeType,id,name,nick,language,region,isSuperuser,automatedId,profile,perkSubscription,const DeepCollectionEquality().hash(_badges),createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, automatedId: $automatedId, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, region: $region, isSuperuser: $isSuperuser, automatedId: $automatedId, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -290,7 +292,7 @@ abstract mixin class _$SnAccountCopyWith<$Res> implements $SnAccountCopyWith<$Re | |||||||
|   factory _$SnAccountCopyWith(_SnAccount value, $Res Function(_SnAccount) _then) = __$SnAccountCopyWithImpl; |   factory _$SnAccountCopyWith(_SnAccount value, $Res Function(_SnAccount) _then) = __$SnAccountCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String name, String nick, String language, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, String name, String nick, String language, String region, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -307,12 +309,13 @@ class __$SnAccountCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnAccount | /// Create a copy of SnAccount | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// 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? nick = null,Object? language = null,Object? isSuperuser = null,Object? automatedId = freezed,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? region = null,Object? isSuperuser = null,Object? automatedId = freezed,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_SnAccount( |   return _then(_SnAccount( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | 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,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||||
| as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable | as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable | ||||||
| as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable | as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,region: null == region ? _self.region : region // ignore: cast_nullable_to_non_nullable | ||||||
| as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable | as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,automatedId: freezed == automatedId ? _self.automatedId : automatedId // ignore: cast_nullable_to_non_nullable | as bool,automatedId: freezed == automatedId ? _self.automatedId : automatedId // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable | as String?,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount( | |||||||
|   name: json['name'] as String, |   name: json['name'] as String, | ||||||
|   nick: json['nick'] as String, |   nick: json['nick'] as String, | ||||||
|   language: json['language'] as String, |   language: json['language'] as String, | ||||||
|  |   region: json['region'] as String? ?? "", | ||||||
|   isSuperuser: json['is_superuser'] as bool, |   isSuperuser: json['is_superuser'] as bool, | ||||||
|   automatedId: json['automated_id'] as String?, |   automatedId: json['automated_id'] as String?, | ||||||
|   profile: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>), |   profile: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>), | ||||||
| @@ -39,6 +40,7 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) => | |||||||
|       'name': instance.name, |       'name': instance.name, | ||||||
|       'nick': instance.nick, |       'nick': instance.nick, | ||||||
|       'language': instance.language, |       'language': instance.language, | ||||||
|  |       'region': instance.region, | ||||||
|       'is_superuser': instance.isSuperuser, |       'is_superuser': instance.isSuperuser, | ||||||
|       'automated_id': instance.automatedId, |       'automated_id': instance.automatedId, | ||||||
|       'profile': instance.profile.toJson(), |       'profile': instance.profile.toJson(), | ||||||
|   | |||||||
| @@ -4,6 +4,20 @@ import 'package:island/models/account.dart'; | |||||||
| part 'activity.freezed.dart'; | part 'activity.freezed.dart'; | ||||||
| part 'activity.g.dart'; | part 'activity.g.dart'; | ||||||
|  |  | ||||||
|  | @freezed | ||||||
|  | sealed class SnNotableDay with _$SnNotableDay { | ||||||
|  |   const factory SnNotableDay({ | ||||||
|  |     required DateTime date, | ||||||
|  |     required String localName, | ||||||
|  |     required String globalName, | ||||||
|  |     required String countryCode, | ||||||
|  |     required List<int> holidays, | ||||||
|  |   }) = _SnNotableDay; | ||||||
|  |  | ||||||
|  |   factory SnNotableDay.fromJson(Map<String, dynamic> json) => | ||||||
|  |       _$SnNotableDayFromJson(json); | ||||||
|  | } | ||||||
|  |  | ||||||
| @freezed | @freezed | ||||||
| sealed class SnActivity with _$SnActivity { | sealed class SnActivity with _$SnActivity { | ||||||
|   const factory SnActivity({ |   const factory SnActivity({ | ||||||
|   | |||||||
| @@ -12,6 +12,281 @@ part of 'activity.dart'; | |||||||
| // dart format off | // dart format off | ||||||
| T _$identity<T>(T value) => value; | T _$identity<T>(T value) => value; | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | mixin _$SnNotableDay { | ||||||
|  |  | ||||||
|  |  DateTime get date; String get localName; String get globalName; String get countryCode; List<int> get holidays; | ||||||
|  | /// Create a copy of SnNotableDay | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | $SnNotableDayCopyWith<SnNotableDay> get copyWith => _$SnNotableDayCopyWithImpl<SnNotableDay>(this as SnNotableDay, _$identity); | ||||||
|  |  | ||||||
|  |   /// Serializes this SnNotableDay to a JSON map. | ||||||
|  |   Map<String, dynamic> toJson(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | bool operator ==(Object other) { | ||||||
|  |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnNotableDay&&(identical(other.date, date) || other.date == date)&&(identical(other.localName, localName) || other.localName == localName)&&(identical(other.globalName, globalName) || other.globalName == globalName)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&const DeepCollectionEquality().equals(other.holidays, holidays)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @override | ||||||
|  | int get hashCode => Object.hash(runtimeType,date,localName,globalName,countryCode,const DeepCollectionEquality().hash(holidays)); | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | String toString() { | ||||||
|  |   return 'SnNotableDay(date: $date, localName: $localName, globalName: $globalName, countryCode: $countryCode, holidays: $holidays)'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | abstract mixin class $SnNotableDayCopyWith<$Res>  { | ||||||
|  |   factory $SnNotableDayCopyWith(SnNotableDay value, $Res Function(SnNotableDay) _then) = _$SnNotableDayCopyWithImpl; | ||||||
|  | @useResult | ||||||
|  | $Res call({ | ||||||
|  |  DateTime date, String localName, String globalName, String countryCode, List<int> holidays | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  | /// @nodoc | ||||||
|  | class _$SnNotableDayCopyWithImpl<$Res> | ||||||
|  |     implements $SnNotableDayCopyWith<$Res> { | ||||||
|  |   _$SnNotableDayCopyWithImpl(this._self, this._then); | ||||||
|  |  | ||||||
|  |   final SnNotableDay _self; | ||||||
|  |   final $Res Function(SnNotableDay) _then; | ||||||
|  |  | ||||||
|  | /// Create a copy of SnNotableDay | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @pragma('vm:prefer-inline') @override $Res call({Object? date = null,Object? localName = null,Object? globalName = null,Object? countryCode = null,Object? holidays = null,}) { | ||||||
|  |   return _then(_self.copyWith( | ||||||
|  | date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime,localName: null == localName ? _self.localName : localName // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,globalName: null == globalName ? _self.globalName : globalName // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,holidays: null == holidays ? _self.holidays : holidays // ignore: cast_nullable_to_non_nullable | ||||||
|  | as List<int>, | ||||||
|  |   )); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /// Adds pattern-matching-related methods to [SnNotableDay]. | ||||||
|  | extension SnNotableDayPatterns on SnNotableDay { | ||||||
|  | /// A variant of `map` that fallback to returning `orElse`. | ||||||
|  | /// | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case final Subclass value: | ||||||
|  | ///     return ...; | ||||||
|  | ///   case _: | ||||||
|  | ///     return orElse(); | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnNotableDay value)?  $default,{required TResult orElse(),}){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnNotableDay() when $default != null: | ||||||
|  | return $default(_that);case _: | ||||||
|  |   return orElse(); | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  | /// A `switch`-like method, using callbacks. | ||||||
|  | /// | ||||||
|  | /// Callbacks receives the raw object, upcasted. | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case final Subclass value: | ||||||
|  | ///     return ...; | ||||||
|  | ///   case final Subclass2 value: | ||||||
|  | ///     return ...; | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnNotableDay value)  $default,){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnNotableDay(): | ||||||
|  | return $default(_that);} | ||||||
|  | } | ||||||
|  | /// A variant of `map` that fallback to returning `null`. | ||||||
|  | /// | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case final Subclass value: | ||||||
|  | ///     return ...; | ||||||
|  | ///   case _: | ||||||
|  | ///     return null; | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnNotableDay value)?  $default,){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnNotableDay() when $default != null: | ||||||
|  | return $default(_that);case _: | ||||||
|  |   return null; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  | /// A variant of `when` that fallback to an `orElse` callback. | ||||||
|  | /// | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case Subclass(:final field): | ||||||
|  | ///     return ...; | ||||||
|  | ///   case _: | ||||||
|  | ///     return orElse(); | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime date,  String localName,  String globalName,  String countryCode,  List<int> holidays)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnNotableDay() when $default != null: | ||||||
|  | return $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_that.holidays);case _: | ||||||
|  |   return orElse(); | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  | /// A `switch`-like method, using callbacks. | ||||||
|  | /// | ||||||
|  | /// As opposed to `map`, this offers destructuring. | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case Subclass(:final field): | ||||||
|  | ///     return ...; | ||||||
|  | ///   case Subclass2(:final field2): | ||||||
|  | ///     return ...; | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime date,  String localName,  String globalName,  String countryCode,  List<int> holidays)  $default,) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnNotableDay(): | ||||||
|  | return $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_that.holidays);} | ||||||
|  | } | ||||||
|  | /// A variant of `when` that fallback to returning `null` | ||||||
|  | /// | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case Subclass(:final field): | ||||||
|  | ///     return ...; | ||||||
|  | ///   case _: | ||||||
|  | ///     return null; | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime date,  String localName,  String globalName,  String countryCode,  List<int> holidays)?  $default,) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnNotableDay() when $default != null: | ||||||
|  | return $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_that.holidays);case _: | ||||||
|  |   return null; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | @JsonSerializable() | ||||||
|  |  | ||||||
|  | class _SnNotableDay implements SnNotableDay { | ||||||
|  |   const _SnNotableDay({required this.date, required this.localName, required this.globalName, required this.countryCode, required final  List<int> holidays}): _holidays = holidays; | ||||||
|  |   factory _SnNotableDay.fromJson(Map<String, dynamic> json) => _$SnNotableDayFromJson(json); | ||||||
|  |  | ||||||
|  | @override final  DateTime date; | ||||||
|  | @override final  String localName; | ||||||
|  | @override final  String globalName; | ||||||
|  | @override final  String countryCode; | ||||||
|  |  final  List<int> _holidays; | ||||||
|  | @override List<int> get holidays { | ||||||
|  |   if (_holidays is EqualUnmodifiableListView) return _holidays; | ||||||
|  |   // ignore: implicit_dynamic_type | ||||||
|  |   return EqualUnmodifiableListView(_holidays); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /// Create a copy of SnNotableDay | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | _$SnNotableDayCopyWith<_SnNotableDay> get copyWith => __$SnNotableDayCopyWithImpl<_SnNotableDay>(this, _$identity); | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | Map<String, dynamic> toJson() { | ||||||
|  |   return _$SnNotableDayToJson(this, ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | bool operator ==(Object other) { | ||||||
|  |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnNotableDay&&(identical(other.date, date) || other.date == date)&&(identical(other.localName, localName) || other.localName == localName)&&(identical(other.globalName, globalName) || other.globalName == globalName)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&const DeepCollectionEquality().equals(other._holidays, _holidays)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @override | ||||||
|  | int get hashCode => Object.hash(runtimeType,date,localName,globalName,countryCode,const DeepCollectionEquality().hash(_holidays)); | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | String toString() { | ||||||
|  |   return 'SnNotableDay(date: $date, localName: $localName, globalName: $globalName, countryCode: $countryCode, holidays: $holidays)'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | abstract mixin class _$SnNotableDayCopyWith<$Res> implements $SnNotableDayCopyWith<$Res> { | ||||||
|  |   factory _$SnNotableDayCopyWith(_SnNotableDay value, $Res Function(_SnNotableDay) _then) = __$SnNotableDayCopyWithImpl; | ||||||
|  | @override @useResult | ||||||
|  | $Res call({ | ||||||
|  |  DateTime date, String localName, String globalName, String countryCode, List<int> holidays | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  | /// @nodoc | ||||||
|  | class __$SnNotableDayCopyWithImpl<$Res> | ||||||
|  |     implements _$SnNotableDayCopyWith<$Res> { | ||||||
|  |   __$SnNotableDayCopyWithImpl(this._self, this._then); | ||||||
|  |  | ||||||
|  |   final _SnNotableDay _self; | ||||||
|  |   final $Res Function(_SnNotableDay) _then; | ||||||
|  |  | ||||||
|  | /// Create a copy of SnNotableDay | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override @pragma('vm:prefer-inline') $Res call({Object? date = null,Object? localName = null,Object? globalName = null,Object? countryCode = null,Object? holidays = null,}) { | ||||||
|  |   return _then(_SnNotableDay( | ||||||
|  | date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable | ||||||
|  | as DateTime,localName: null == localName ? _self.localName : localName // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,globalName: null == globalName ? _self.globalName : globalName // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,holidays: null == holidays ? _self._holidays : holidays // ignore: cast_nullable_to_non_nullable | ||||||
|  | as List<int>, | ||||||
|  |   )); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnActivity { | mixin _$SnActivity { | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,6 +6,27 @@ part of 'activity.dart'; | |||||||
| // JsonSerializableGenerator | // JsonSerializableGenerator | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
|  | _SnNotableDay _$SnNotableDayFromJson(Map<String, dynamic> json) => | ||||||
|  |     _SnNotableDay( | ||||||
|  |       date: DateTime.parse(json['date'] as String), | ||||||
|  |       localName: json['local_name'] as String, | ||||||
|  |       globalName: json['global_name'] as String, | ||||||
|  |       countryCode: json['country_code'] as String, | ||||||
|  |       holidays: | ||||||
|  |           (json['holidays'] as List<dynamic>) | ||||||
|  |               .map((e) => (e as num).toInt()) | ||||||
|  |               .toList(), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  | Map<String, dynamic> _$SnNotableDayToJson(_SnNotableDay instance) => | ||||||
|  |     <String, dynamic>{ | ||||||
|  |       'date': instance.date.toIso8601String(), | ||||||
|  |       'local_name': instance.localName, | ||||||
|  |       'global_name': instance.globalName, | ||||||
|  |       'country_code': instance.countryCode, | ||||||
|  |       'holidays': instance.holidays, | ||||||
|  |     }; | ||||||
|  |  | ||||||
| _SnActivity _$SnActivityFromJson(Map<String, dynamic> json) => _SnActivity( | _SnActivity _$SnActivityFromJson(Map<String, dynamic> json) => _SnActivity( | ||||||
|   id: json['id'] as String, |   id: json['id'] as String, | ||||||
|   type: json['type'] as String, |   type: json['type'] as String, | ||||||
|   | |||||||
| @@ -11,6 +11,20 @@ sealed class AppToken with _$AppToken { | |||||||
|       _$AppTokenFromJson(json); |       _$AppTokenFromJson(json); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @freezed | ||||||
|  | sealed class GeoIpLocation with _$GeoIpLocation { | ||||||
|  |   const factory GeoIpLocation({ | ||||||
|  |     required double latitude, | ||||||
|  |     required double longitude, | ||||||
|  |     required String countryCode, | ||||||
|  |     required String country, | ||||||
|  |     required String city, | ||||||
|  |   }) = _GeoIpLocation; | ||||||
|  |  | ||||||
|  |   factory GeoIpLocation.fromJson(Map<String, dynamic> json) => | ||||||
|  |       _$GeoIpLocationFromJson(json); | ||||||
|  | } | ||||||
|  |  | ||||||
| @freezed | @freezed | ||||||
| sealed class SnAuthChallenge with _$SnAuthChallenge { | sealed class SnAuthChallenge with _$SnAuthChallenge { | ||||||
|   const factory SnAuthChallenge({ |   const factory SnAuthChallenge({ | ||||||
| @@ -26,7 +40,7 @@ sealed class SnAuthChallenge with _$SnAuthChallenge { | |||||||
|     required String ipAddress, |     required String ipAddress, | ||||||
|     required String userAgent, |     required String userAgent, | ||||||
|     required String? nonce, |     required String? nonce, | ||||||
|     required String? location, |     required GeoIpLocation? location, | ||||||
|     required String accountId, |     required String accountId, | ||||||
|     required DateTime createdAt, |     required DateTime createdAt, | ||||||
|     required DateTime updatedAt, |     required DateTime updatedAt, | ||||||
|   | |||||||
| @@ -269,10 +269,279 @@ as String, | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | mixin _$GeoIpLocation { | ||||||
|  |  | ||||||
|  |  double get latitude; double get longitude; String get countryCode; String get country; String get city; | ||||||
|  | /// Create a copy of GeoIpLocation | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | $GeoIpLocationCopyWith<GeoIpLocation> get copyWith => _$GeoIpLocationCopyWithImpl<GeoIpLocation>(this as GeoIpLocation, _$identity); | ||||||
|  |  | ||||||
|  |   /// Serializes this GeoIpLocation to a JSON map. | ||||||
|  |   Map<String, dynamic> toJson(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | bool operator ==(Object other) { | ||||||
|  |   return identical(this, other) || (other.runtimeType == runtimeType&&other is GeoIpLocation&&(identical(other.latitude, latitude) || other.latitude == latitude)&&(identical(other.longitude, longitude) || other.longitude == longitude)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&(identical(other.country, country) || other.country == country)&&(identical(other.city, city) || other.city == city)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @override | ||||||
|  | int get hashCode => Object.hash(runtimeType,latitude,longitude,countryCode,country,city); | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | String toString() { | ||||||
|  |   return 'GeoIpLocation(latitude: $latitude, longitude: $longitude, countryCode: $countryCode, country: $country, city: $city)'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | abstract mixin class $GeoIpLocationCopyWith<$Res>  { | ||||||
|  |   factory $GeoIpLocationCopyWith(GeoIpLocation value, $Res Function(GeoIpLocation) _then) = _$GeoIpLocationCopyWithImpl; | ||||||
|  | @useResult | ||||||
|  | $Res call({ | ||||||
|  |  double latitude, double longitude, String countryCode, String country, String city | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  | /// @nodoc | ||||||
|  | class _$GeoIpLocationCopyWithImpl<$Res> | ||||||
|  |     implements $GeoIpLocationCopyWith<$Res> { | ||||||
|  |   _$GeoIpLocationCopyWithImpl(this._self, this._then); | ||||||
|  |  | ||||||
|  |   final GeoIpLocation _self; | ||||||
|  |   final $Res Function(GeoIpLocation) _then; | ||||||
|  |  | ||||||
|  | /// Create a copy of GeoIpLocation | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @pragma('vm:prefer-inline') @override $Res call({Object? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) { | ||||||
|  |   return _then(_self.copyWith( | ||||||
|  | latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable | ||||||
|  | as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable | ||||||
|  | as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String, | ||||||
|  |   )); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /// Adds pattern-matching-related methods to [GeoIpLocation]. | ||||||
|  | extension GeoIpLocationPatterns on GeoIpLocation { | ||||||
|  | /// A variant of `map` that fallback to returning `orElse`. | ||||||
|  | /// | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case final Subclass value: | ||||||
|  | ///     return ...; | ||||||
|  | ///   case _: | ||||||
|  | ///     return orElse(); | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _GeoIpLocation value)?  $default,{required TResult orElse(),}){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _GeoIpLocation() when $default != null: | ||||||
|  | return $default(_that);case _: | ||||||
|  |   return orElse(); | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  | /// A `switch`-like method, using callbacks. | ||||||
|  | /// | ||||||
|  | /// Callbacks receives the raw object, upcasted. | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case final Subclass value: | ||||||
|  | ///     return ...; | ||||||
|  | ///   case final Subclass2 value: | ||||||
|  | ///     return ...; | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _GeoIpLocation value)  $default,){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _GeoIpLocation(): | ||||||
|  | return $default(_that);} | ||||||
|  | } | ||||||
|  | /// A variant of `map` that fallback to returning `null`. | ||||||
|  | /// | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case final Subclass value: | ||||||
|  | ///     return ...; | ||||||
|  | ///   case _: | ||||||
|  | ///     return null; | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _GeoIpLocation value)?  $default,){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _GeoIpLocation() when $default != null: | ||||||
|  | return $default(_that);case _: | ||||||
|  |   return null; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  | /// A variant of `when` that fallback to an `orElse` callback. | ||||||
|  | /// | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case Subclass(:final field): | ||||||
|  | ///     return ...; | ||||||
|  | ///   case _: | ||||||
|  | ///     return orElse(); | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( double latitude,  double longitude,  String countryCode,  String country,  String city)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _GeoIpLocation() when $default != null: | ||||||
|  | return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _: | ||||||
|  |   return orElse(); | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  | /// A `switch`-like method, using callbacks. | ||||||
|  | /// | ||||||
|  | /// As opposed to `map`, this offers destructuring. | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case Subclass(:final field): | ||||||
|  | ///     return ...; | ||||||
|  | ///   case Subclass2(:final field2): | ||||||
|  | ///     return ...; | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( double latitude,  double longitude,  String countryCode,  String country,  String city)  $default,) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _GeoIpLocation(): | ||||||
|  | return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);} | ||||||
|  | } | ||||||
|  | /// A variant of `when` that fallback to returning `null` | ||||||
|  | /// | ||||||
|  | /// It is equivalent to doing: | ||||||
|  | /// ```dart | ||||||
|  | /// switch (sealedClass) { | ||||||
|  | ///   case Subclass(:final field): | ||||||
|  | ///     return ...; | ||||||
|  | ///   case _: | ||||||
|  | ///     return null; | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  |  | ||||||
|  | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( double latitude,  double longitude,  String countryCode,  String country,  String city)?  $default,) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _GeoIpLocation() when $default != null: | ||||||
|  | return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _: | ||||||
|  |   return null; | ||||||
|  |  | ||||||
|  | } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | @JsonSerializable() | ||||||
|  |  | ||||||
|  | class _GeoIpLocation implements GeoIpLocation { | ||||||
|  |   const _GeoIpLocation({required this.latitude, required this.longitude, required this.countryCode, required this.country, required this.city}); | ||||||
|  |   factory _GeoIpLocation.fromJson(Map<String, dynamic> json) => _$GeoIpLocationFromJson(json); | ||||||
|  |  | ||||||
|  | @override final  double latitude; | ||||||
|  | @override final  double longitude; | ||||||
|  | @override final  String countryCode; | ||||||
|  | @override final  String country; | ||||||
|  | @override final  String city; | ||||||
|  |  | ||||||
|  | /// Create a copy of GeoIpLocation | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | _$GeoIpLocationCopyWith<_GeoIpLocation> get copyWith => __$GeoIpLocationCopyWithImpl<_GeoIpLocation>(this, _$identity); | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | Map<String, dynamic> toJson() { | ||||||
|  |   return _$GeoIpLocationToJson(this, ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | bool operator ==(Object other) { | ||||||
|  |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _GeoIpLocation&&(identical(other.latitude, latitude) || other.latitude == latitude)&&(identical(other.longitude, longitude) || other.longitude == longitude)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&(identical(other.country, country) || other.country == country)&&(identical(other.city, city) || other.city == city)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @override | ||||||
|  | int get hashCode => Object.hash(runtimeType,latitude,longitude,countryCode,country,city); | ||||||
|  |  | ||||||
|  | @override | ||||||
|  | String toString() { | ||||||
|  |   return 'GeoIpLocation(latitude: $latitude, longitude: $longitude, countryCode: $countryCode, country: $country, city: $city)'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// @nodoc | ||||||
|  | abstract mixin class _$GeoIpLocationCopyWith<$Res> implements $GeoIpLocationCopyWith<$Res> { | ||||||
|  |   factory _$GeoIpLocationCopyWith(_GeoIpLocation value, $Res Function(_GeoIpLocation) _then) = __$GeoIpLocationCopyWithImpl; | ||||||
|  | @override @useResult | ||||||
|  | $Res call({ | ||||||
|  |  double latitude, double longitude, String countryCode, String country, String city | ||||||
|  | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  | /// @nodoc | ||||||
|  | class __$GeoIpLocationCopyWithImpl<$Res> | ||||||
|  |     implements _$GeoIpLocationCopyWith<$Res> { | ||||||
|  |   __$GeoIpLocationCopyWithImpl(this._self, this._then); | ||||||
|  |  | ||||||
|  |   final _GeoIpLocation _self; | ||||||
|  |   final $Res Function(_GeoIpLocation) _then; | ||||||
|  |  | ||||||
|  | /// Create a copy of GeoIpLocation | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override @pragma('vm:prefer-inline') $Res call({Object? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) { | ||||||
|  |   return _then(_GeoIpLocation( | ||||||
|  | latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable | ||||||
|  | as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable | ||||||
|  | as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String, | ||||||
|  |   )); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnAuthChallenge { | mixin _$SnAuthChallenge { | ||||||
|  |  | ||||||
|  String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; String? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; |  String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; GeoIpLocation? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||||
| /// Create a copy of SnAuthChallenge | /// Create a copy of SnAuthChallenge | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @@ -305,11 +574,11 @@ abstract mixin class $SnAuthChallengeCopyWith<$Res>  { | |||||||
|   factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl; |   factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | $GeoIpLocationCopyWith<$Res>? get location; | ||||||
|  |  | ||||||
| } | } | ||||||
| /// @nodoc | /// @nodoc | ||||||
| @@ -337,14 +606,26 @@ as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // i | |||||||
| as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable | as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable | ||||||
| as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable | as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable | as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | as GeoIpLocation?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
| as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?, | as DateTime?, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  | /// Create a copy of SnAuthChallenge | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | $GeoIpLocationCopyWith<$Res>? get location { | ||||||
|  |     if (_self.location == null) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return $GeoIpLocationCopyWith<$Res>(_self.location!, (value) { | ||||||
|  |     return _then(_self.copyWith(location: value)); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -423,7 +704,7 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  String? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAuthChallenge() when $default != null: | case _SnAuthChallenge() when $default != null: | ||||||
| return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
| @@ -444,7 +725,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that. | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  String? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAuthChallenge(): | case _SnAuthChallenge(): | ||||||
| return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} | return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||||
| @@ -461,7 +742,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that. | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  String? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAuthChallenge() when $default != null: | case _SnAuthChallenge() when $default != null: | ||||||
| return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
| @@ -509,7 +790,7 @@ class _SnAuthChallenge implements SnAuthChallenge { | |||||||
| @override final  String ipAddress; | @override final  String ipAddress; | ||||||
| @override final  String userAgent; | @override final  String userAgent; | ||||||
| @override final  String? nonce; | @override final  String? nonce; | ||||||
| @override final  String? location; | @override final  GeoIpLocation? location; | ||||||
| @override final  String accountId; | @override final  String accountId; | ||||||
| @override final  DateTime createdAt; | @override final  DateTime createdAt; | ||||||
| @override final  DateTime updatedAt; | @override final  DateTime updatedAt; | ||||||
| @@ -548,11 +829,11 @@ abstract mixin class _$SnAuthChallengeCopyWith<$Res> implements $SnAuthChallenge | |||||||
|   factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl; |   factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @override $GeoIpLocationCopyWith<$Res>? get location; | ||||||
|  |  | ||||||
| } | } | ||||||
| /// @nodoc | /// @nodoc | ||||||
| @@ -580,7 +861,7 @@ as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // i | |||||||
| as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable | as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable | ||||||
| as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable | as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable | as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | as GeoIpLocation?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
| as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -588,7 +869,19 @@ as DateTime?, | |||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// Create a copy of SnAuthChallenge | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | $GeoIpLocationCopyWith<$Res>? get location { | ||||||
|  |     if (_self.location == null) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return $GeoIpLocationCopyWith<$Res>(_self.location!, (value) { | ||||||
|  |     return _then(_self.copyWith(location: value)); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,6 +13,24 @@ Map<String, dynamic> _$AppTokenToJson(_AppToken instance) => <String, dynamic>{ | |||||||
|   'token': instance.token, |   'token': instance.token, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | _GeoIpLocation _$GeoIpLocationFromJson(Map<String, dynamic> json) => | ||||||
|  |     _GeoIpLocation( | ||||||
|  |       latitude: (json['latitude'] as num).toDouble(), | ||||||
|  |       longitude: (json['longitude'] as num).toDouble(), | ||||||
|  |       countryCode: json['country_code'] as String, | ||||||
|  |       country: json['country'] as String, | ||||||
|  |       city: json['city'] as String, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  | Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) => | ||||||
|  |     <String, dynamic>{ | ||||||
|  |       'latitude': instance.latitude, | ||||||
|  |       'longitude': instance.longitude, | ||||||
|  |       'country_code': instance.countryCode, | ||||||
|  |       'country': instance.country, | ||||||
|  |       'city': instance.city, | ||||||
|  |     }; | ||||||
|  |  | ||||||
| _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => | _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => | ||||||
|     _SnAuthChallenge( |     _SnAuthChallenge( | ||||||
|       id: json['id'] as String, |       id: json['id'] as String, | ||||||
| @@ -30,7 +48,12 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => | |||||||
|       ipAddress: json['ip_address'] as String, |       ipAddress: json['ip_address'] as String, | ||||||
|       userAgent: json['user_agent'] as String, |       userAgent: json['user_agent'] as String, | ||||||
|       nonce: json['nonce'] as String?, |       nonce: json['nonce'] as String?, | ||||||
|       location: json['location'] as String?, |       location: | ||||||
|  |           json['location'] == null | ||||||
|  |               ? null | ||||||
|  |               : GeoIpLocation.fromJson( | ||||||
|  |                 json['location'] as Map<String, dynamic>, | ||||||
|  |               ), | ||||||
|       accountId: json['account_id'] as String, |       accountId: json['account_id'] as String, | ||||||
|       createdAt: DateTime.parse(json['created_at'] as String), |       createdAt: DateTime.parse(json['created_at'] as String), | ||||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), |       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||||
| @@ -54,7 +77,7 @@ Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) => | |||||||
|       'ip_address': instance.ipAddress, |       'ip_address': instance.ipAddress, | ||||||
|       'user_agent': instance.userAgent, |       'user_agent': instance.userAgent, | ||||||
|       'nonce': instance.nonce, |       'nonce': instance.nonce, | ||||||
|       'location': instance.location, |       'location': instance.location?.toJson(), | ||||||
|       'account_id': instance.accountId, |       'account_id': instance.accountId, | ||||||
|       'created_at': instance.createdAt.toIso8601String(), |       'created_at': instance.createdAt.toIso8601String(), | ||||||
|       'updated_at': instance.updatedAt.toIso8601String(), |       'updated_at': instance.updatedAt.toIso8601String(), | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; | |||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/models/chat.dart'; | import 'package:island/models/chat.dart'; | ||||||
|  | import 'package:wakelock_plus/wakelock_plus.dart'; | ||||||
|  |  | ||||||
| part 'call.g.dart'; | part 'call.g.dart'; | ||||||
| part 'call.freezed.dart'; | part 'call.freezed.dart'; | ||||||
| @@ -54,7 +55,7 @@ sealed class CallParticipantLive with _$CallParticipantLive { | |||||||
|   bool get hasAudio => remoteParticipant.hasAudio; |   bool get hasAudio => remoteParticipant.hasAudio; | ||||||
| } | } | ||||||
|  |  | ||||||
| @riverpod | @Riverpod(keepAlive: true) | ||||||
| class CallNotifier extends _$CallNotifier { | class CallNotifier extends _$CallNotifier { | ||||||
|   Room? _room; |   Room? _room; | ||||||
|   LocalParticipant? _localParticipant; |   LocalParticipant? _localParticipant; | ||||||
| @@ -277,14 +278,27 @@ class CallNotifier extends _$CallNotifier { | |||||||
|  |  | ||||||
|         // Listen for connection updates |         // Listen for connection updates | ||||||
|         _room!.addListener(() { |         _room!.addListener(() { | ||||||
|  |           final wasConnected = state.isConnected; | ||||||
|  |           final isNowConnected = | ||||||
|  |               _room!.connectionState == ConnectionState.connected; | ||||||
|           state = state.copyWith( |           state = state.copyWith( | ||||||
|             isConnected: _room!.connectionState == ConnectionState.connected, |             isConnected: isNowConnected, | ||||||
|             isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(), |             isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(), | ||||||
|             isCameraEnabled: _localParticipant!.isCameraEnabled(), |             isCameraEnabled: _localParticipant!.isCameraEnabled(), | ||||||
|             isScreenSharing: _localParticipant!.isScreenShareEnabled(), |             isScreenSharing: _localParticipant!.isScreenShareEnabled(), | ||||||
|           ); |           ); | ||||||
|  |           // Enable wakelock when call connects | ||||||
|  |           if (!wasConnected && isNowConnected) { | ||||||
|  |             WakelockPlus.enable(); | ||||||
|  |           } | ||||||
|  |           // Disable wakelock when call disconnects | ||||||
|  |           else if (wasConnected && !isNowConnected) { | ||||||
|  |             WakelockPlus.disable(); | ||||||
|  |           } | ||||||
|         }); |         }); | ||||||
|         state = state.copyWith(isConnected: true); |         state = state.copyWith(isConnected: true); | ||||||
|  |         // Enable wakelock when call connects | ||||||
|  |         WakelockPlus.enable(); | ||||||
|       } else { |       } else { | ||||||
|         state = state.copyWith(error: 'Failed to join room'); |         state = state.copyWith(error: 'Failed to join room'); | ||||||
|       } |       } | ||||||
| @@ -344,6 +358,8 @@ class CallNotifier extends _$CallNotifier { | |||||||
|         isCameraEnabled: false, |         isCameraEnabled: false, | ||||||
|         isScreenSharing: false, |         isScreenSharing: false, | ||||||
|       ); |       ); | ||||||
|  |       // Disable wakelock when call disconnects | ||||||
|  |       WakelockPlus.disable(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -381,5 +397,7 @@ class CallNotifier extends _$CallNotifier { | |||||||
|     _durationTimer?.cancel(); |     _durationTimer?.cancel(); | ||||||
|     _roomId = null; |     _roomId = null; | ||||||
|     participantsVolumes = {}; |     participantsVolumes = {}; | ||||||
|  |     // Disable wakelock when disposing | ||||||
|  |     WakelockPlus.disable(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,22 +6,19 @@ part of 'call.dart'; | |||||||
| // RiverpodGenerator | // RiverpodGenerator | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
| String _$callNotifierHash() => r'18fb807f067eecd3ea42631c1426c3e5f1fb4280'; | String _$callNotifierHash() => r'eb9bd41b97e9b5e9d54007c8327edb6567458846'; | ||||||
|  |  | ||||||
| /// See also [CallNotifier]. | /// See also [CallNotifier]. | ||||||
| @ProviderFor(CallNotifier) | @ProviderFor(CallNotifier) | ||||||
| final callNotifierProvider = | final callNotifierProvider = NotifierProvider<CallNotifier, CallState>.internal( | ||||||
|     AutoDisposeNotifierProvider<CallNotifier, CallState>.internal( |  | ||||||
|   CallNotifier.new, |   CallNotifier.new, | ||||||
|   name: r'callNotifierProvider', |   name: r'callNotifierProvider', | ||||||
|   debugGetCreateSourceHash: |   debugGetCreateSourceHash: | ||||||
|           const bool.fromEnvironment('dart.vm.product') |       const bool.fromEnvironment('dart.vm.product') ? null : _$callNotifierHash, | ||||||
|               ? null |  | ||||||
|               : _$callNotifierHash, |  | ||||||
|   dependencies: null, |   dependencies: null, | ||||||
|   allTransitiveDependencies: null, |   allTransitiveDependencies: null, | ||||||
| ); | ); | ||||||
|  |  | ||||||
| typedef _$CallNotifier = AutoDisposeNotifier<CallState>; | typedef _$CallNotifier = Notifier<CallState>; | ||||||
| // ignore_for_file: type=lint | // ignore_for_file: type=lint | ||||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ const kAppColorSchemeStoreKey = 'app_color_scheme'; | |||||||
| const kAppNotifyWithHaptic = 'app_notify_with_haptic'; | const kAppNotifyWithHaptic = 'app_notify_with_haptic'; | ||||||
| const kAppCustomFonts = 'app_custom_fonts'; | const kAppCustomFonts = 'app_custom_fonts'; | ||||||
| const kAppAutoTranslate = 'app_auto_translate'; | const kAppAutoTranslate = 'app_auto_translate'; | ||||||
|  | const kAppDataSavingMode = 'app_data_saving_mode'; | ||||||
| const kAppSoundEffects = 'app_sound_effects'; | const kAppSoundEffects = 'app_sound_effects'; | ||||||
| const kAppAprilFoolFeatures = 'app_april_fool_features'; | const kAppAprilFoolFeatures = 'app_april_fool_features'; | ||||||
| const kAppWindowSize = 'app_window_size'; | const kAppWindowSize = 'app_window_size'; | ||||||
| @@ -55,6 +56,7 @@ final serverUrlProvider = Provider<String>((ref) { | |||||||
| sealed class AppSettings with _$AppSettings { | sealed class AppSettings with _$AppSettings { | ||||||
|   const factory AppSettings({ |   const factory AppSettings({ | ||||||
|     required bool autoTranslate, |     required bool autoTranslate, | ||||||
|  |     required bool dataSavingMode, | ||||||
|     required bool soundEffects, |     required bool soundEffects, | ||||||
|     required bool aprilFoolFeatures, |     required bool aprilFoolFeatures, | ||||||
|     required bool enterToSend, |     required bool enterToSend, | ||||||
| @@ -73,6 +75,7 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { | |||||||
|     final prefs = ref.watch(sharedPreferencesProvider); |     final prefs = ref.watch(sharedPreferencesProvider); | ||||||
|     return AppSettings( |     return AppSettings( | ||||||
|       autoTranslate: prefs.getBool(kAppAutoTranslate) ?? false, |       autoTranslate: prefs.getBool(kAppAutoTranslate) ?? false, | ||||||
|  |       dataSavingMode: prefs.getBool(kAppDataSavingMode) ?? false, | ||||||
|       soundEffects: prefs.getBool(kAppSoundEffects) ?? true, |       soundEffects: prefs.getBool(kAppSoundEffects) ?? true, | ||||||
|       aprilFoolFeatures: prefs.getBool(kAppAprilFoolFeatures) ?? true, |       aprilFoolFeatures: prefs.getBool(kAppAprilFoolFeatures) ?? true, | ||||||
|       enterToSend: prefs.getBool(kAppEnterToSend) ?? true, |       enterToSend: prefs.getBool(kAppEnterToSend) ?? true, | ||||||
| @@ -107,6 +110,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { | |||||||
|     state = state.copyWith(autoTranslate: value); |     state = state.copyWith(autoTranslate: value); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void setDataSavingMode(bool value){ | ||||||
|  |     final prefs = ref.read(sharedPreferencesProvider); | ||||||
|  |     prefs.setBool(kAppDataSavingMode, value); | ||||||
|  |     state = state.copyWith(dataSavingMode: value); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   void setSoundEffects(bool value) { |   void setSoundEffects(bool value) { | ||||||
|     final prefs = ref.read(sharedPreferencesProvider); |     final prefs = ref.read(sharedPreferencesProvider); | ||||||
|     prefs.setBool(kAppSoundEffects, value); |     prefs.setBool(kAppSoundEffects, value); | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ T _$identity<T>(T value) => value; | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$AppSettings { | mixin _$AppSettings { | ||||||
|  |  | ||||||
|  bool get autoTranslate; 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 |  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; | ||||||
| /// Create a copy of AppSettings | /// Create a copy of AppSettings | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @@ -26,16 +26,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(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)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize); | int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'AppSettings(autoTranslate: $autoTranslate, 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)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -46,7 +46,7 @@ abstract mixin class $AppSettingsCopyWith<$Res>  { | |||||||
|   factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl; |   factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  bool autoTranslate, 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 | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -63,9 +63,10 @@ class _$AppSettingsCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of AppSettings | /// Create a copy of AppSettings | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = 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,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable | 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 | ||||||
| as bool,soundEffects: null == soundEffects ? _self.soundEffects : soundEffects // ignore: cast_nullable_to_non_nullable | as bool,soundEffects: null == soundEffects ? _self.soundEffects : soundEffects // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,aprilFoolFeatures: null == aprilFoolFeatures ? _self.aprilFoolFeatures : aprilFoolFeatures // ignore: cast_nullable_to_non_nullable | as bool,aprilFoolFeatures: null == aprilFoolFeatures ? _self.aprilFoolFeatures : aprilFoolFeatures // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,enterToSend: null == enterToSend ? _self.enterToSend : enterToSend // ignore: cast_nullable_to_non_nullable | as bool,enterToSend: null == enterToSend ? _self.enterToSend : enterToSend // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -156,10 +157,10 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate,  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 extends Object?>(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; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _AppSettings() when $default != null: | case _AppSettings() when $default != null: | ||||||
| return $default(_that.autoTranslate,_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);case _: | ||||||
|   return orElse(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -177,10 +178,10 @@ return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_ | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate,  bool soundEffects,  bool aprilFoolFeatures,  bool enterToSend,  bool appBarTransparent,  bool showBackgroundImage,  String? customFonts,  int? appColorScheme,  Size? windowSize)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(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; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _AppSettings(): | case _AppSettings(): | ||||||
| return $default(_that.autoTranslate,_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);} | ||||||
| } | } | ||||||
| /// A variant of `when` that fallback to returning `null` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -194,10 +195,10 @@ return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_ | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate,  bool soundEffects,  bool aprilFoolFeatures,  bool enterToSend,  bool appBarTransparent,  bool showBackgroundImage,  String? customFonts,  int? appColorScheme,  Size? windowSize)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(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; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _AppSettings() when $default != null: | case _AppSettings() when $default != null: | ||||||
| return $default(_that.autoTranslate,_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);case _: | ||||||
|   return null; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -209,10 +210,11 @@ return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_ | |||||||
|  |  | ||||||
|  |  | ||||||
| class _AppSettings implements AppSettings { | class _AppSettings implements AppSettings { | ||||||
|   const _AppSettings({required this.autoTranslate, 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}); | ||||||
|    |    | ||||||
|  |  | ||||||
| @override final  bool autoTranslate; | @override final  bool autoTranslate; | ||||||
|  | @override final  bool dataSavingMode; | ||||||
| @override final  bool soundEffects; | @override final  bool soundEffects; | ||||||
| @override final  bool aprilFoolFeatures; | @override final  bool aprilFoolFeatures; | ||||||
| @override final  bool enterToSend; | @override final  bool enterToSend; | ||||||
| @@ -233,16 +235,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_ | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(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)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize); | int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'AppSettings(autoTranslate: $autoTranslate, 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)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -253,7 +255,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith | |||||||
|   factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl; |   factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  bool autoTranslate, 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 | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -270,9 +272,10 @@ class __$AppSettingsCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of AppSettings | /// Create a copy of AppSettings | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = 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,}) { | ||||||
|   return _then(_AppSettings( |   return _then(_AppSettings( | ||||||
| autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable | 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 | ||||||
| as bool,soundEffects: null == soundEffects ? _self.soundEffects : soundEffects // ignore: cast_nullable_to_non_nullable | as bool,soundEffects: null == soundEffects ? _self.soundEffects : soundEffects // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,aprilFoolFeatures: null == aprilFoolFeatures ? _self.aprilFoolFeatures : aprilFoolFeatures // ignore: cast_nullable_to_non_nullable | as bool,aprilFoolFeatures: null == aprilFoolFeatures ? _self.aprilFoolFeatures : aprilFoolFeatures // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,enterToSend: null == enterToSend ? _self.enterToSend : enterToSend // ignore: cast_nullable_to_non_nullable | as bool,enterToSend: null == enterToSend ? _self.enterToSend : enterToSend // ignore: cast_nullable_to_non_nullable | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ part of 'config.dart'; | |||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
| String _$appSettingsNotifierHash() => | String _$appSettingsNotifierHash() => | ||||||
|     r'e3c13307eabb0201487b85ab67b1ab493e588e71'; |     r'cd18bff2614a94e3523634e6c577cefad0367eba'; | ||||||
|  |  | ||||||
| /// See also [AppSettingsNotifier]. | /// See also [AppSettingsNotifier]. | ||||||
| @ProviderFor(AppSettingsNotifier) | @ProviderFor(AppSettingsNotifier) | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ import 'package:material_symbols_icons/symbols.dart'; | |||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
| const kServerSupportedLanguages = {'en-US': 'en-us', 'zh-CN': 'zh-hans'}; | const kServerSupportedLanguages = {'en-US': 'en-us', 'zh-CN': 'zh-hans'}; | ||||||
|  | const kServerSupportedRegions = ['US', 'JP', 'CN']; | ||||||
|  |  | ||||||
| class UpdateProfileScreen extends HookConsumerWidget { | class UpdateProfileScreen extends HookConsumerWidget { | ||||||
|   const UpdateProfileScreen({super.key}); |   const UpdateProfileScreen({super.key}); | ||||||
| @@ -97,6 +98,7 @@ class UpdateProfileScreen extends HookConsumerWidget { | |||||||
|     final usernameController = useTextEditingController(text: user.value!.name); |     final usernameController = useTextEditingController(text: user.value!.name); | ||||||
|     final nicknameController = useTextEditingController(text: user.value!.nick); |     final nicknameController = useTextEditingController(text: user.value!.nick); | ||||||
|     final language = useState(user.value!.language); |     final language = useState(user.value!.language); | ||||||
|  |     final region = useState(user.value!.region); | ||||||
|     final links = useState<List<ProfileLink>>(user.value!.profile.links); |     final links = useState<List<ProfileLink>>(user.value!.profile.links); | ||||||
|  |  | ||||||
|     void updateBasicInfo() async { |     void updateBasicInfo() async { | ||||||
| @@ -111,6 +113,7 @@ class UpdateProfileScreen extends HookConsumerWidget { | |||||||
|             'name': usernameController.text, |             'name': usernameController.text, | ||||||
|             'nick': nicknameController.text, |             'nick': nicknameController.text, | ||||||
|             'language': language.value, |             'language': language.value, | ||||||
|  |             'region': region.value, | ||||||
|           }, |           }, | ||||||
|         ); |         ); | ||||||
|         final userNotifier = ref.read(userInfoProvider.notifier); |         final userNotifier = ref.read(userInfoProvider.notifier); | ||||||
| @@ -291,6 +294,32 @@ class UpdateProfileScreen extends HookConsumerWidget { | |||||||
|                       ], |                       ], | ||||||
|                     ), |                     ), | ||||||
|                   ), |                   ), | ||||||
|  |                   DropdownButtonFormField2<String>( | ||||||
|  |                     decoration: InputDecoration( | ||||||
|  |                       labelText: 'region'.tr(), | ||||||
|  |                       helperText: 'accountRegionHint'.tr(), | ||||||
|  |                     ), | ||||||
|  |                     items: [ | ||||||
|  |                       ...kServerSupportedRegions.map( | ||||||
|  |                         (e) => DropdownMenuItem(value: e, child: Text(e)), | ||||||
|  |                       ), | ||||||
|  |                       if (!kServerSupportedRegions.contains(region.value)) | ||||||
|  |                         DropdownMenuItem( | ||||||
|  |                           value: region.value, | ||||||
|  |                           child: Text(region.value), | ||||||
|  |                         ), | ||||||
|  |                     ], | ||||||
|  |                     value: region.value, | ||||||
|  |                     onChanged: (value) { | ||||||
|  |                       region.value = value ?? region.value; | ||||||
|  |                     }, | ||||||
|  |                     customButton: Row( | ||||||
|  |                       children: [ | ||||||
|  |                         Expanded(child: Text(region.value)), | ||||||
|  |                         Icon(Symbols.arrow_drop_down), | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|                   Align( |                   Align( | ||||||
|                     alignment: Alignment.centerRight, |                     alignment: Alignment.centerRight, | ||||||
|                     child: TextButton.icon( |                     child: TextButton.icon( | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import 'package:gap/gap.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/chat.dart'; | import 'package:island/models/chat.dart'; | ||||||
| import 'package:island/models/developer.dart'; | import 'package:island/models/developer.dart'; | ||||||
|  | import 'package:island/models/publisher.dart'; | ||||||
| import 'package:island/models/relationship.dart'; | import 'package:island/models/relationship.dart'; | ||||||
| import 'package:island/models/account.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/config.dart'; | import 'package:island/pods/config.dart'; | ||||||
| @@ -39,6 +40,467 @@ import 'package:url_launcher/url_launcher_string.dart'; | |||||||
|  |  | ||||||
| part 'profile.g.dart'; | part 'profile.g.dart'; | ||||||
|  |  | ||||||
|  | class _AccountBasicInfo extends StatelessWidget { | ||||||
|  |   final SnAccount data; | ||||||
|  |   final String uname; | ||||||
|  |   final AsyncValue<SnDeveloper?> accountDeveloper; | ||||||
|  |  | ||||||
|  |   const _AccountBasicInfo({ | ||||||
|  |     required this.data, | ||||||
|  |     required this.uname, | ||||||
|  |     required this.accountDeveloper, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Padding( | ||||||
|  |       padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), | ||||||
|  |       child: Row( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: [ | ||||||
|  |           ProfilePictureWidget(file: data.profile.picture, radius: 32), | ||||||
|  |           const Gap(20), | ||||||
|  |           Expanded( | ||||||
|  |             child: Column( | ||||||
|  |               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |               children: [ | ||||||
|  |                 Row( | ||||||
|  |                   children: [ | ||||||
|  |                     AccountName(account: data, style: TextStyle(fontSize: 20)), | ||||||
|  |                     const Gap(6), | ||||||
|  |                     Flexible( | ||||||
|  |                       child: Text( | ||||||
|  |                         '@${data.name}', | ||||||
|  |                         maxLines: 1, | ||||||
|  |                         overflow: TextOverflow.ellipsis, | ||||||
|  |                       ).fontSize(14).opacity(0.85), | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |                 if (accountDeveloper.value != null) | ||||||
|  |                   Row( | ||||||
|  |                     spacing: 7, | ||||||
|  |                     children: [ | ||||||
|  |                       const Icon(Symbols.smart_toy, size: 18), | ||||||
|  |                       Text( | ||||||
|  |                         'botAutomatedBy'.tr( | ||||||
|  |                           args: [accountDeveloper.value!.publisher!.nick], | ||||||
|  |                         ), | ||||||
|  |                       ).fontSize(13), | ||||||
|  |                     ], | ||||||
|  |                   ).opacity(0.75), | ||||||
|  |                 const Gap(4), | ||||||
|  |                 AccountStatusWidget(uname: uname, padding: EdgeInsets.zero), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           IconButton( | ||||||
|  |             onPressed: () { | ||||||
|  |               SharePlus.instance.share( | ||||||
|  |                 ShareParams( | ||||||
|  |                   uri: Uri.parse('https://id.solian.app/@${data.name}'), | ||||||
|  |                 ), | ||||||
|  |               ); | ||||||
|  |             }, | ||||||
|  |             icon: const Icon(Symbols.share), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _AccountProfileBio extends StatelessWidget { | ||||||
|  |   final SnAccount data; | ||||||
|  |  | ||||||
|  |   const _AccountProfileBio({required this.data}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Card( | ||||||
|  |       child: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: [ | ||||||
|  |           Text('bio').tr().bold().fontSize(15).padding(bottom: 8), | ||||||
|  |           if (data.profile.bio.isEmpty) | ||||||
|  |             Text('descriptionNone').tr().italic() | ||||||
|  |           else | ||||||
|  |             MarkdownTextContent( | ||||||
|  |               content: data.profile.bio, | ||||||
|  |               linesMargin: EdgeInsets.zero, | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |       ).padding(horizontal: 24, vertical: 20), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _AccountProfileDetail extends StatelessWidget { | ||||||
|  |   final SnAccount data; | ||||||
|  |  | ||||||
|  |   const _AccountProfileDetail({required this.data}); | ||||||
|  |  | ||||||
|  |   List<Widget> _buildSubcolumn() { | ||||||
|  |     return [ | ||||||
|  |       Row( | ||||||
|  |         spacing: 6, | ||||||
|  |         children: [ | ||||||
|  |           const Icon(Symbols.join, size: 17, fill: 1), | ||||||
|  |           Text( | ||||||
|  |             'joinedAt'.tr(args: [data.createdAt.formatCustom('yyyy-MM-dd')]), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |       if (data.profile.birthday != null) | ||||||
|  |         Row( | ||||||
|  |           spacing: 6, | ||||||
|  |           children: [ | ||||||
|  |             const Icon(Symbols.cake, size: 17, fill: 1), | ||||||
|  |             Text(data.profile.birthday!.formatCustom('yyyy-MM-dd')), | ||||||
|  |             Text('·').bold(), | ||||||
|  |             Text( | ||||||
|  |               '${DateTime.now().difference(data.profile.birthday!).inDays ~/ 365} yrs old', | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       if (data.profile.location.isNotEmpty) | ||||||
|  |         Row( | ||||||
|  |           spacing: 6, | ||||||
|  |           children: [ | ||||||
|  |             const Icon(Symbols.location_on, size: 17, fill: 1), | ||||||
|  |             Text(data.profile.location), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       if (data.profile.pronouns.isNotEmpty || data.profile.gender.isNotEmpty) | ||||||
|  |         Row( | ||||||
|  |           spacing: 6, | ||||||
|  |           children: [ | ||||||
|  |             const Icon(Symbols.person, size: 17, fill: 1), | ||||||
|  |             Text( | ||||||
|  |               data.profile.gender.isEmpty | ||||||
|  |                   ? 'unspecified'.tr() | ||||||
|  |                   : data.profile.gender, | ||||||
|  |             ), | ||||||
|  |             Text('·').bold(), | ||||||
|  |             Text( | ||||||
|  |               data.profile.pronouns.isEmpty | ||||||
|  |                   ? 'unspecified'.tr() | ||||||
|  |                   : data.profile.pronouns, | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       if (data.profile.firstName.isNotEmpty || | ||||||
|  |           data.profile.middleName.isNotEmpty || | ||||||
|  |           data.profile.lastName.isNotEmpty) | ||||||
|  |         Row( | ||||||
|  |           spacing: 6, | ||||||
|  |           children: [ | ||||||
|  |             const Icon(Symbols.id_card, size: 17, fill: 1), | ||||||
|  |             if (data.profile.firstName.isNotEmpty) Text(data.profile.firstName), | ||||||
|  |             if (data.profile.middleName.isNotEmpty) | ||||||
|  |               Text(data.profile.middleName), | ||||||
|  |             if (data.profile.lastName.isNotEmpty) Text(data.profile.lastName), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       Tooltip( | ||||||
|  |         message: 'creditsStatus'.tr(), | ||||||
|  |         child: Row( | ||||||
|  |           spacing: 6, | ||||||
|  |           children: [ | ||||||
|  |             Icon(Symbols.star, size: 17, fill: 1).padding(right: 2), | ||||||
|  |             Text('${data.profile.socialCredits.toStringAsFixed(2)} pts'), | ||||||
|  |             Text('·').bold(), | ||||||
|  |             switch (data.profile.socialCreditsLevel) { | ||||||
|  |               -1 => Text('socialCreditsLevelPoor').tr(), | ||||||
|  |               0 => Text('socialCreditsLevelNormal').tr(), | ||||||
|  |               1 => Text('socialCreditsLevelGood').tr(), | ||||||
|  |               2 => Text('socialCreditsLevelExcellent').tr(), | ||||||
|  |               _ => Text('unknown').tr(), | ||||||
|  |             }, | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       InkWell( | ||||||
|  |         child: Row( | ||||||
|  |           spacing: 6, | ||||||
|  |           children: [ | ||||||
|  |             Icon(Symbols.fingerprint, size: 17, fill: 1).padding(right: 2), | ||||||
|  |             Flexible( | ||||||
|  |               child: Text( | ||||||
|  |                 data.id, | ||||||
|  |                 maxLines: 1, | ||||||
|  |                 overflow: TextOverflow.ellipsis, | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |         onTap: () { | ||||||
|  |           Clipboard.setData(ClipboardData(text: data.id)); | ||||||
|  |         }, | ||||||
|  |       ), | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Card( | ||||||
|  |       child: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |         spacing: 24, | ||||||
|  |         children: [ | ||||||
|  |           if (_buildSubcolumn().isNotEmpty) | ||||||
|  |             Column( | ||||||
|  |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |               spacing: 2, | ||||||
|  |               children: _buildSubcolumn(), | ||||||
|  |             ), | ||||||
|  |           if (data.profile.timeZone.isNotEmpty && !kIsWeb) | ||||||
|  |             Column( | ||||||
|  |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |               children: [ | ||||||
|  |                 Text('timeZone').tr().bold(), | ||||||
|  |                 Row( | ||||||
|  |                   crossAxisAlignment: CrossAxisAlignment.baseline, | ||||||
|  |                   textBaseline: TextBaseline.alphabetic, | ||||||
|  |                   spacing: 6, | ||||||
|  |                   children: [ | ||||||
|  |                     Text(data.profile.timeZone), | ||||||
|  |                     Text( | ||||||
|  |                       getTzInfo( | ||||||
|  |                         data.profile.timeZone, | ||||||
|  |                       ).$2.formatCustomGlobal('HH:mm'), | ||||||
|  |                     ), | ||||||
|  |                     Text( | ||||||
|  |                       getTzInfo(data.profile.timeZone).$1.formatOffsetLocal(), | ||||||
|  |                     ).fontSize(11), | ||||||
|  |                     Text( | ||||||
|  |                       'UTC${getTzInfo(data.profile.timeZone).$1.formatOffset()}', | ||||||
|  |                     ).fontSize(11).opacity(0.75), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |       ).padding(horizontal: 24, vertical: 16), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _AccountProfileLinks extends StatelessWidget { | ||||||
|  |   final SnAccount data; | ||||||
|  |  | ||||||
|  |   const _AccountProfileLinks({required this.data}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Card( | ||||||
|  |       child: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: [ | ||||||
|  |           Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4), | ||||||
|  |           for (final link in data.profile.links) | ||||||
|  |             ListTile( | ||||||
|  |               title: Text(link.name.capitalizeEachWord()), | ||||||
|  |               subtitle: Text(link.url), | ||||||
|  |               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||||
|  |               trailing: const Icon(Symbols.chevron_right), | ||||||
|  |               shape: RoundedRectangleBorder( | ||||||
|  |                 borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |               ), | ||||||
|  |               onTap: () { | ||||||
|  |                 if (!link.url.startsWith('http') && !link.url.contains('://')) { | ||||||
|  |                   launchUrlString('https://${link.url}'); | ||||||
|  |                 } else { | ||||||
|  |                   launchUrlString(link.url); | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _AccountPublisherList extends StatelessWidget { | ||||||
|  |   final List<SnPublisher> publishers; | ||||||
|  |  | ||||||
|  |   const _AccountPublisherList({required this.publishers}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     if (publishers.isEmpty) return const SizedBox.shrink(); | ||||||
|  |     return Card( | ||||||
|  |       child: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: [ | ||||||
|  |           Text( | ||||||
|  |             'publishers', | ||||||
|  |           ).tr().bold().padding(horizontal: 24, top: 12, bottom: 4), | ||||||
|  |           for (final publisher in publishers) | ||||||
|  |             ListTile( | ||||||
|  |               title: Text(publisher.nick), | ||||||
|  |               subtitle: Text( | ||||||
|  |                 publisher.bio.isNotEmpty | ||||||
|  |                     ? publisher.bio | ||||||
|  |                         .split('\n') | ||||||
|  |                         .where((line) => line.trim().isNotEmpty) | ||||||
|  |                         .join('\n') | ||||||
|  |                     : 'descriptionNone'.tr(), | ||||||
|  |                 maxLines: 3, | ||||||
|  |                 overflow: TextOverflow.ellipsis, | ||||||
|  |               ), | ||||||
|  |               leading: ProfilePictureWidget( | ||||||
|  |                 file: publisher.picture, | ||||||
|  |                 borderRadius: publisher.type == 1 ? 8 : null, | ||||||
|  |               ), | ||||||
|  |               isThreeLine: true, | ||||||
|  |               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||||
|  |               trailing: const Icon(Symbols.chevron_right), | ||||||
|  |               shape: RoundedRectangleBorder( | ||||||
|  |                 borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |               ), | ||||||
|  |               onTap: () { | ||||||
|  |                 Navigator.pop(context, true); | ||||||
|  |                 context.pushNamed( | ||||||
|  |                   'publisherProfile', | ||||||
|  |                   pathParameters: {'name': publisher.name}, | ||||||
|  |                 ); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _AccountAction extends StatelessWidget { | ||||||
|  |   final SnAccount data; | ||||||
|  |   final AsyncValue<SnRelationship?> accountRelationship; | ||||||
|  |   final AsyncValue<SnChatRoom?> accountChat; | ||||||
|  |   final VoidCallback relationshipAction; | ||||||
|  |   final VoidCallback blockAction; | ||||||
|  |   final VoidCallback directMessageAction; | ||||||
|  |  | ||||||
|  |   const _AccountAction({ | ||||||
|  |     required this.data, | ||||||
|  |     required this.accountRelationship, | ||||||
|  |     required this.accountChat, | ||||||
|  |     required this.relationshipAction, | ||||||
|  |     required this.blockAction, | ||||||
|  |     required this.directMessageAction, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Card( | ||||||
|  |       child: Column( | ||||||
|  |         spacing: 8, | ||||||
|  |         children: [ | ||||||
|  |           Row( | ||||||
|  |             spacing: 8, | ||||||
|  |             children: [ | ||||||
|  |               if (accountRelationship.value == null || | ||||||
|  |                   accountRelationship.value!.status > -100) | ||||||
|  |                 Expanded( | ||||||
|  |                   child: FilledButton.icon( | ||||||
|  |                     style: ButtonStyle( | ||||||
|  |                       backgroundColor: WidgetStatePropertyAll( | ||||||
|  |                         accountRelationship.value == null | ||||||
|  |                             ? null | ||||||
|  |                             : Theme.of(context).colorScheme.secondary, | ||||||
|  |                       ), | ||||||
|  |                       foregroundColor: WidgetStatePropertyAll( | ||||||
|  |                         accountRelationship.value == null | ||||||
|  |                             ? null | ||||||
|  |                             : Theme.of(context).colorScheme.onSecondary, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                     onPressed: relationshipAction, | ||||||
|  |                     label: | ||||||
|  |                         Text( | ||||||
|  |                           accountRelationship.value == null | ||||||
|  |                               ? 'addFriendShort' | ||||||
|  |                               : 'added', | ||||||
|  |                         ).tr(), | ||||||
|  |                     icon: | ||||||
|  |                         accountRelationship.value == null | ||||||
|  |                             ? const Icon(Symbols.person_add) | ||||||
|  |                             : const Icon(Symbols.person_check), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               if (accountRelationship.value == null || | ||||||
|  |                   accountRelationship.value!.status <= -100) | ||||||
|  |                 Expanded( | ||||||
|  |                   child: FilledButton.icon( | ||||||
|  |                     style: ButtonStyle( | ||||||
|  |                       backgroundColor: WidgetStatePropertyAll( | ||||||
|  |                         accountRelationship.value == null | ||||||
|  |                             ? null | ||||||
|  |                             : Theme.of(context).colorScheme.secondary, | ||||||
|  |                       ), | ||||||
|  |                       foregroundColor: WidgetStatePropertyAll( | ||||||
|  |                         accountRelationship.value == null | ||||||
|  |                             ? null | ||||||
|  |                             : Theme.of(context).colorScheme.onSecondary, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                     onPressed: blockAction, | ||||||
|  |                     label: | ||||||
|  |                         Text( | ||||||
|  |                           accountRelationship.value == null | ||||||
|  |                               ? 'blockUser' | ||||||
|  |                               : 'unblockUser', | ||||||
|  |                         ).tr(), | ||||||
|  |                     icon: | ||||||
|  |                         accountRelationship.value == null | ||||||
|  |                             ? const Icon(Symbols.block) | ||||||
|  |                             : const Icon(Symbols.person_cancel), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |           Row( | ||||||
|  |             spacing: 8, | ||||||
|  |             children: [ | ||||||
|  |               Expanded( | ||||||
|  |                 child: FilledButton.icon( | ||||||
|  |                   onPressed: directMessageAction, | ||||||
|  |                   icon: const Icon(Symbols.message), | ||||||
|  |                   label: | ||||||
|  |                       Text( | ||||||
|  |                         accountChat.value == null | ||||||
|  |                             ? 'createDirectMessage' | ||||||
|  |                             : 'gotoDirectMessage', | ||||||
|  |                         maxLines: 1, | ||||||
|  |                       ).tr(), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |               IconButton.filled( | ||||||
|  |                 onPressed: () { | ||||||
|  |                   showAbuseReportSheet( | ||||||
|  |                     context, | ||||||
|  |                     resourceIdentifier: 'account/${data.id}', | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |                 icon: Icon( | ||||||
|  |                   Symbols.flag, | ||||||
|  |                   color: Theme.of(context).colorScheme.onError, | ||||||
|  |                 ), | ||||||
|  |                 style: ButtonStyle( | ||||||
|  |                   visualDensity: VisualDensity.compact, | ||||||
|  |                   backgroundColor: WidgetStatePropertyAll( | ||||||
|  |                     Theme.of(context).colorScheme.error, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ).padding(horizontal: 16, vertical: 12), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| @riverpod | @riverpod | ||||||
| Future<SnAccount> account(Ref ref, String uname) async { | Future<SnAccount> account(Ref ref, String uname) async { | ||||||
|   if (uname == 'me') { |   if (uname == 'me') { | ||||||
| @@ -132,6 +594,20 @@ Future<SnDeveloper?> accountBotDeveloper(Ref ref, String uname) async { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | Future<List<SnPublisher>> accountPublishers(Ref ref, String id) async { | ||||||
|  |   final apiClient = ref.watch(apiClientProvider); | ||||||
|  |   try { | ||||||
|  |     final resp = await apiClient.get('/sphere/publishers/of/$id'); | ||||||
|  |     return resp.data | ||||||
|  |         .map((e) => SnPublisher.fromJson(e)) | ||||||
|  |         .cast<SnPublisher>() | ||||||
|  |         .toList(); | ||||||
|  |   } catch (err) { | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| class AccountProfileScreen extends HookConsumerWidget { | class AccountProfileScreen extends HookConsumerWidget { | ||||||
|   final String name; |   final String name; | ||||||
|   const AccountProfileScreen({super.key, required this.name}); |   const AccountProfileScreen({super.key, required this.name}); | ||||||
| @@ -217,354 +693,16 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     List<Widget> buildSubcolumn(SnAccount data) { |  | ||||||
|       return [ |  | ||||||
|         Row( |  | ||||||
|           spacing: 6, |  | ||||||
|           children: [ |  | ||||||
|             const Icon(Symbols.join, size: 17, fill: 1), |  | ||||||
|             Text( |  | ||||||
|               'joinedAt'.tr(args: [data.createdAt.formatCustom('yyyy-MM-dd')]), |  | ||||||
|             ), |  | ||||||
|           ], |  | ||||||
|         ), |  | ||||||
|         if (data.profile.birthday != null) |  | ||||||
|           Row( |  | ||||||
|             spacing: 6, |  | ||||||
|             children: [ |  | ||||||
|               const Icon(Symbols.cake, size: 17, fill: 1), |  | ||||||
|               Text(data.profile.birthday!.formatCustom('yyyy-MM-dd')), |  | ||||||
|               Text('·').bold(), |  | ||||||
|               Text( |  | ||||||
|                 '${DateTime.now().difference(data.profile.birthday!).inDays ~/ 365} yrs old', |  | ||||||
|               ), |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|         if (data.profile.location.isNotEmpty) |  | ||||||
|           Row( |  | ||||||
|             spacing: 6, |  | ||||||
|             children: [ |  | ||||||
|               const Icon(Symbols.location_on, size: 17, fill: 1), |  | ||||||
|               Text(data.profile.location), |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|         if (data.profile.pronouns.isNotEmpty || data.profile.gender.isNotEmpty) |  | ||||||
|           Row( |  | ||||||
|             spacing: 6, |  | ||||||
|             children: [ |  | ||||||
|               const Icon(Symbols.person, size: 17, fill: 1), |  | ||||||
|               Text( |  | ||||||
|                 data.profile.gender.isEmpty |  | ||||||
|                     ? 'unspecified'.tr() |  | ||||||
|                     : data.profile.gender, |  | ||||||
|               ), |  | ||||||
|               Text('·').bold(), |  | ||||||
|               Text( |  | ||||||
|                 data.profile.pronouns.isEmpty |  | ||||||
|                     ? 'unspecified'.tr() |  | ||||||
|                     : data.profile.pronouns, |  | ||||||
|               ), |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|         if (data.profile.firstName.isNotEmpty || |  | ||||||
|             data.profile.middleName.isNotEmpty || |  | ||||||
|             data.profile.lastName.isNotEmpty) |  | ||||||
|           Row( |  | ||||||
|             spacing: 6, |  | ||||||
|             children: [ |  | ||||||
|               const Icon(Symbols.id_card, size: 17, fill: 1), |  | ||||||
|               if (data.profile.firstName.isNotEmpty) |  | ||||||
|                 Text(data.profile.firstName), |  | ||||||
|               if (data.profile.middleName.isNotEmpty) |  | ||||||
|                 Text(data.profile.middleName), |  | ||||||
|               if (data.profile.lastName.isNotEmpty) Text(data.profile.lastName), |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|         Tooltip( |  | ||||||
|           message: 'creditsStatus'.tr(), |  | ||||||
|           child: Row( |  | ||||||
|             spacing: 6, |  | ||||||
|             children: [ |  | ||||||
|               Icon(Symbols.star, size: 17, fill: 1).padding(right: 2), |  | ||||||
|               Text('${data.profile.socialCredits.toStringAsFixed(2)} pts'), |  | ||||||
|               Text('·').bold(), |  | ||||||
|               switch (data.profile.socialCreditsLevel) { |  | ||||||
|                 -1 => Text('socialCreditsLevelPoor').tr(), |  | ||||||
|                 0 => Text('socialCreditsLevelNormal').tr(), |  | ||||||
|                 1 => Text('socialCreditsLevelGood').tr(), |  | ||||||
|                 2 => Text('socialCreditsLevelExcellent').tr(), |  | ||||||
|                 _ => Text('unknown').tr(), |  | ||||||
|               }, |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|         InkWell( |  | ||||||
|           child: Row( |  | ||||||
|             spacing: 6, |  | ||||||
|             children: [ |  | ||||||
|               Icon(Symbols.fingerprint, size: 17, fill: 1).padding(right: 2), |  | ||||||
|               Text(data.id), |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|           onTap: () { |  | ||||||
|             Clipboard.setData(ClipboardData(text: data.id)); |  | ||||||
|           }, |  | ||||||
|         ), |  | ||||||
|       ]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     final user = ref.watch(userInfoProvider); |     final user = ref.watch(userInfoProvider); | ||||||
|     final isCurrentUser = useMemoized( |     final isCurrentUser = useMemoized( | ||||||
|       () => user.value?.id == account.value?.id, |       () => user.value?.id == account.value?.id, | ||||||
|       [user, account], |       [user, account], | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     Widget accountBasicInfo(SnAccount data) => Padding( |  | ||||||
|       padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), |  | ||||||
|       child: Row( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|         children: [ |  | ||||||
|           ProfilePictureWidget(file: data.profile.picture, radius: 32), |  | ||||||
|           const Gap(20), |  | ||||||
|           Expanded( |  | ||||||
|             child: Column( |  | ||||||
|               crossAxisAlignment: CrossAxisAlignment.stretch, |  | ||||||
|               children: [ |  | ||||||
|                 Row( |  | ||||||
|                   children: [ |  | ||||||
|                     AccountName(account: data, style: TextStyle(fontSize: 20)), |  | ||||||
|                     const Gap(6), |  | ||||||
|                     Flexible( |  | ||||||
|                       child: Text( |  | ||||||
|                         '@${data.name}', |  | ||||||
|                         maxLines: 1, |  | ||||||
|                         overflow: TextOverflow.ellipsis, |  | ||||||
|                       ).fontSize(14).opacity(0.85), |  | ||||||
|                     ), |  | ||||||
|                   ], |  | ||||||
|                 ), |  | ||||||
|                 if (accountDeveloper.value != null) |  | ||||||
|                   Row( |  | ||||||
|                     spacing: 7, |  | ||||||
|                     children: [ |  | ||||||
|                       const Icon(Symbols.smart_toy, size: 18), |  | ||||||
|                       Text( |  | ||||||
|                         'botAutomatedBy'.tr( |  | ||||||
|                           args: [accountDeveloper.value!.publisher!.nick], |  | ||||||
|                         ), |  | ||||||
|                       ).fontSize(13), |  | ||||||
|                     ], |  | ||||||
|                   ).opacity(0.75), |  | ||||||
|                 const Gap(4), |  | ||||||
|                 AccountStatusWidget(uname: name, padding: EdgeInsets.zero), |  | ||||||
|               ], |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           IconButton( |  | ||||||
|             onPressed: () { |  | ||||||
|               SharePlus.instance.share( |  | ||||||
|                 ShareParams( |  | ||||||
|                   uri: Uri.parse('https://id.solian.app/@${data.name}'), |  | ||||||
|                 ), |  | ||||||
|               ); |  | ||||||
|             }, |  | ||||||
|             icon: const Icon(Symbols.share), |  | ||||||
|           ), |  | ||||||
|         ], |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     Widget accountProfileBio(SnAccount data) => Card( |  | ||||||
|       child: Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|         children: [ |  | ||||||
|           Text('bio').tr().bold().fontSize(15).padding(bottom: 8), |  | ||||||
|           if (data.profile.bio.isEmpty) |  | ||||||
|             Text('descriptionNone').tr().italic() |  | ||||||
|           else |  | ||||||
|             MarkdownTextContent( |  | ||||||
|               content: data.profile.bio, |  | ||||||
|               linesMargin: EdgeInsets.zero, |  | ||||||
|             ), |  | ||||||
|         ], |  | ||||||
|       ).padding(horizontal: 24, vertical: 20), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     Widget accountProfileDetail(SnAccount data) => Card( |  | ||||||
|       child: Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, |  | ||||||
|         spacing: 24, |  | ||||||
|         children: [ |  | ||||||
|           if (buildSubcolumn(data).isNotEmpty) |  | ||||||
|             Column( |  | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|               spacing: 2, |  | ||||||
|               children: buildSubcolumn(data), |  | ||||||
|             ), |  | ||||||
|           if (data.profile.timeZone.isNotEmpty && !kIsWeb) |  | ||||||
|             Column( |  | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|               children: [ |  | ||||||
|                 Text('timeZone').tr().bold(), |  | ||||||
|                 Row( |  | ||||||
|                   crossAxisAlignment: CrossAxisAlignment.baseline, |  | ||||||
|                   textBaseline: TextBaseline.alphabetic, |  | ||||||
|                   spacing: 6, |  | ||||||
|                   children: [ |  | ||||||
|                     Text(data.profile.timeZone), |  | ||||||
|                     Text( |  | ||||||
|                       getTzInfo( |  | ||||||
|                         data.profile.timeZone, |  | ||||||
|                       ).$2.formatCustomGlobal('HH:mm'), |  | ||||||
|                     ), |  | ||||||
|                     Text( |  | ||||||
|                       getTzInfo(data.profile.timeZone).$1.formatOffsetLocal(), |  | ||||||
|                     ).fontSize(11), |  | ||||||
|                     Text( |  | ||||||
|                       'UTC${getTzInfo(data.profile.timeZone).$1.formatOffset()}', |  | ||||||
|                     ).fontSize(11).opacity(0.75), |  | ||||||
|                   ], |  | ||||||
|                 ), |  | ||||||
|               ], |  | ||||||
|             ), |  | ||||||
|         ], |  | ||||||
|       ).padding(horizontal: 24, vertical: 16), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     Widget accountProfileLinks(SnAccount data) => Card( |  | ||||||
|       child: Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|         children: [ |  | ||||||
|           Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4), |  | ||||||
|           for (final link in data.profile.links) |  | ||||||
|             ListTile( |  | ||||||
|               title: Text(link.name.capitalizeEachWord()), |  | ||||||
|               subtitle: Text(link.url), |  | ||||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), |  | ||||||
|               trailing: const Icon(Symbols.chevron_right), |  | ||||||
|               shape: RoundedRectangleBorder( |  | ||||||
|                 borderRadius: const BorderRadius.all(Radius.circular(8)), |  | ||||||
|               ), |  | ||||||
|               onTap: () { |  | ||||||
|                 if (!link.url.startsWith('http') && !link.url.contains('://')) { |  | ||||||
|                   launchUrlString('https://${link.url}'); |  | ||||||
|                 } else { |  | ||||||
|                   launchUrlString(link.url); |  | ||||||
|                 } |  | ||||||
|               }, |  | ||||||
|             ), |  | ||||||
|         ], |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     Widget accountAction(SnAccount data) => Card( |  | ||||||
|       child: Column( |  | ||||||
|         children: [ |  | ||||||
|           Row( |  | ||||||
|             spacing: 8, |  | ||||||
|             children: [ |  | ||||||
|               if (accountRelationship.value == null || |  | ||||||
|                   accountRelationship.value!.status > -100) |  | ||||||
|                 Expanded( |  | ||||||
|                   child: FilledButton.icon( |  | ||||||
|                     style: ButtonStyle( |  | ||||||
|                       backgroundColor: WidgetStatePropertyAll( |  | ||||||
|                         accountRelationship.value == null |  | ||||||
|                             ? null |  | ||||||
|                             : Theme.of(context).colorScheme.secondary, |  | ||||||
|                       ), |  | ||||||
|                       foregroundColor: WidgetStatePropertyAll( |  | ||||||
|                         accountRelationship.value == null |  | ||||||
|                             ? null |  | ||||||
|                             : Theme.of(context).colorScheme.onSecondary, |  | ||||||
|                       ), |  | ||||||
|                     ), |  | ||||||
|                     onPressed: relationshipAction, |  | ||||||
|                     label: |  | ||||||
|                         Text( |  | ||||||
|                           accountRelationship.value == null |  | ||||||
|                               ? 'addFriendShort' |  | ||||||
|                               : 'added', |  | ||||||
|                         ).tr(), |  | ||||||
|                     icon: |  | ||||||
|                         accountRelationship.value == null |  | ||||||
|                             ? const Icon(Symbols.person_add) |  | ||||||
|                             : const Icon(Symbols.person_check), |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|               if (accountRelationship.value == null || |  | ||||||
|                   accountRelationship.value!.status <= -100) |  | ||||||
|                 Expanded( |  | ||||||
|                   child: FilledButton.icon( |  | ||||||
|                     style: ButtonStyle( |  | ||||||
|                       backgroundColor: WidgetStatePropertyAll( |  | ||||||
|                         accountRelationship.value == null |  | ||||||
|                             ? null |  | ||||||
|                             : Theme.of(context).colorScheme.secondary, |  | ||||||
|                       ), |  | ||||||
|                       foregroundColor: WidgetStatePropertyAll( |  | ||||||
|                         accountRelationship.value == null |  | ||||||
|                             ? null |  | ||||||
|                             : Theme.of(context).colorScheme.onSecondary, |  | ||||||
|                       ), |  | ||||||
|                     ), |  | ||||||
|                     onPressed: blockAction, |  | ||||||
|                     label: |  | ||||||
|                         Text( |  | ||||||
|                           accountRelationship.value == null |  | ||||||
|                               ? 'blockUser' |  | ||||||
|                               : 'unblockUser', |  | ||||||
|                         ).tr(), |  | ||||||
|                     icon: |  | ||||||
|                         accountRelationship.value == null |  | ||||||
|                             ? const Icon(Symbols.block) |  | ||||||
|                             : const Icon(Symbols.person_cancel), |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|           Row( |  | ||||||
|             spacing: 8, |  | ||||||
|             children: [ |  | ||||||
|               Expanded( |  | ||||||
|                 child: FilledButton.icon( |  | ||||||
|                   onPressed: directMessageAction, |  | ||||||
|                   icon: const Icon(Symbols.message), |  | ||||||
|                   label: |  | ||||||
|                       Text( |  | ||||||
|                         accountChat.value == null |  | ||||||
|                             ? 'createDirectMessage' |  | ||||||
|                             : 'gotoDirectMessage', |  | ||||||
|                         maxLines: 1, |  | ||||||
|                       ).tr(), |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|               IconButton.filled( |  | ||||||
|                 onPressed: () { |  | ||||||
|                   showAbuseReportSheet( |  | ||||||
|                     context, |  | ||||||
|                     resourceIdentifier: 'account/${data.id}', |  | ||||||
|                   ); |  | ||||||
|                 }, |  | ||||||
|                 icon: Icon( |  | ||||||
|                   Symbols.flag, |  | ||||||
|                   color: Theme.of(context).colorScheme.onError, |  | ||||||
|                 ), |  | ||||||
|                 style: ButtonStyle( |  | ||||||
|                   backgroundColor: WidgetStatePropertyAll( |  | ||||||
|                     Theme.of(context).colorScheme.error, |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|         ], |  | ||||||
|       ).padding(horizontal: 16, vertical: 12), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return account.when( |     return account.when( | ||||||
|       data: |       data: (data) { | ||||||
|           (data) => AppScaffold( |         final accountPublishers = ref.watch(accountPublishersProvider(data.id)); | ||||||
|  |         return AppScaffold( | ||||||
|           isNoBackground: false, |           isNoBackground: false, | ||||||
|           appBar: |           appBar: | ||||||
|               isWideScreen(context) |               isWideScreen(context) | ||||||
| @@ -595,9 +733,7 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                             style: TextStyle( |                             style: TextStyle( | ||||||
|                               color: |                               color: | ||||||
|                                   appbarColor.value ?? |                                   appbarColor.value ?? | ||||||
|                                     Theme.of( |                                   Theme.of(context).appBarTheme.foregroundColor, | ||||||
|                                       context, |  | ||||||
|                                     ).appBarTheme.foregroundColor, |  | ||||||
|                               shadows: [appbarShadow], |                               shadows: [appbarShadow], | ||||||
|                             ), |                             ), | ||||||
|                           ), |                           ), | ||||||
| @@ -613,7 +749,13 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                       Flexible( |                       Flexible( | ||||||
|                         child: CustomScrollView( |                         child: CustomScrollView( | ||||||
|                           slivers: [ |                           slivers: [ | ||||||
|                               SliverToBoxAdapter(child: accountBasicInfo(data)), |                             SliverToBoxAdapter( | ||||||
|  |                               child: _AccountBasicInfo( | ||||||
|  |                                 data: data, | ||||||
|  |                                 uname: name, | ||||||
|  |                                 accountDeveloper: accountDeveloper, | ||||||
|  |                               ), | ||||||
|  |                             ), | ||||||
|                             if (data.badges.isNotEmpty) |                             if (data.badges.isNotEmpty) | ||||||
|                               SliverToBoxAdapter( |                               SliverToBoxAdapter( | ||||||
|                                 child: Card( |                                 child: Card( | ||||||
| @@ -642,14 +784,16 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                               ).padding(horizontal: 4, top: 8), |                               ).padding(horizontal: 4, top: 8), | ||||||
|                             ), |                             ), | ||||||
|                             SliverToBoxAdapter( |                             SliverToBoxAdapter( | ||||||
|                                 child: accountProfileBio(data).padding(top: 4), |                               child: _AccountProfileBio( | ||||||
|  |                                 data: data, | ||||||
|  |                               ).padding(top: 4), | ||||||
|                             ), |                             ), | ||||||
|                             if (data.profile.links.isNotEmpty) |                             if (data.profile.links.isNotEmpty) | ||||||
|                               SliverToBoxAdapter( |                               SliverToBoxAdapter( | ||||||
|                                   child: accountProfileLinks(data), |                                 child: _AccountProfileLinks(data: data), | ||||||
|                               ), |                               ), | ||||||
|                             SliverToBoxAdapter( |                             SliverToBoxAdapter( | ||||||
|                                 child: accountProfileDetail(data), |                               child: _AccountProfileDetail(data: data), | ||||||
|                             ), |                             ), | ||||||
|                           ], |                           ], | ||||||
|                         ), |                         ), | ||||||
| @@ -658,8 +802,22 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                         child: CustomScrollView( |                         child: CustomScrollView( | ||||||
|                           slivers: [ |                           slivers: [ | ||||||
|                             SliverGap(24), |                             SliverGap(24), | ||||||
|  |                             SliverToBoxAdapter( | ||||||
|  |                               child: _AccountPublisherList( | ||||||
|  |                                 publishers: accountPublishers.value ?? [], | ||||||
|  |                               ), | ||||||
|  |                             ), | ||||||
|                             if (user.value != null && !isCurrentUser) |                             if (user.value != null && !isCurrentUser) | ||||||
|                                 SliverToBoxAdapter(child: accountAction(data)), |                               SliverToBoxAdapter( | ||||||
|  |                                 child: _AccountAction( | ||||||
|  |                                   data: data, | ||||||
|  |                                   accountRelationship: accountRelationship, | ||||||
|  |                                   accountChat: accountChat, | ||||||
|  |                                   relationshipAction: relationshipAction, | ||||||
|  |                                   blockAction: blockAction, | ||||||
|  |                                   directMessageAction: directMessageAction, | ||||||
|  |                                 ), | ||||||
|  |                               ), | ||||||
|                             SliverToBoxAdapter( |                             SliverToBoxAdapter( | ||||||
|                               child: Card( |                               child: Card( | ||||||
|                                 child: FortuneGraphWidget( |                                 child: FortuneGraphWidget( | ||||||
| @@ -715,7 +873,13 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                           ], |                           ], | ||||||
|                         ), |                         ), | ||||||
|                       ), |                       ), | ||||||
|                         SliverToBoxAdapter(child: accountBasicInfo(data)), |                       SliverToBoxAdapter( | ||||||
|  |                         child: _AccountBasicInfo( | ||||||
|  |                           data: data, | ||||||
|  |                           uname: name, | ||||||
|  |                           accountDeveloper: accountDeveloper, | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|                       if (data.badges.isNotEmpty) |                       if (data.badges.isNotEmpty) | ||||||
|                         SliverToBoxAdapter( |                         SliverToBoxAdapter( | ||||||
|                           child: Card( |                           child: Card( | ||||||
| @@ -742,22 +906,36 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                         ), |                         ), | ||||||
|                       ), |                       ), | ||||||
|                       SliverToBoxAdapter( |                       SliverToBoxAdapter( | ||||||
|                           child: accountProfileBio(data).padding(horizontal: 4), |                         child: _AccountProfileBio( | ||||||
|  |                           data: data, | ||||||
|  |                         ).padding(horizontal: 4), | ||||||
|                       ), |                       ), | ||||||
|                       if (data.profile.links.isNotEmpty) |                       if (data.profile.links.isNotEmpty) | ||||||
|                         SliverToBoxAdapter( |                         SliverToBoxAdapter( | ||||||
|                             child: accountProfileLinks( |                           child: _AccountProfileLinks( | ||||||
|                               data, |                             data: data, | ||||||
|                           ).padding(horizontal: 4), |                           ).padding(horizontal: 4), | ||||||
|                         ), |                         ), | ||||||
|                       SliverToBoxAdapter( |                       SliverToBoxAdapter( | ||||||
|                           child: accountProfileDetail( |                         child: _AccountPublisherList( | ||||||
|                             data, |                           publishers: accountPublishers.value ?? [], | ||||||
|  |                         ).padding(horizontal: 4), | ||||||
|  |                       ), | ||||||
|  |                       SliverToBoxAdapter( | ||||||
|  |                         child: _AccountProfileDetail( | ||||||
|  |                           data: data, | ||||||
|                         ).padding(horizontal: 4), |                         ).padding(horizontal: 4), | ||||||
|                       ), |                       ), | ||||||
|                       if (user.value != null && !isCurrentUser) |                       if (user.value != null && !isCurrentUser) | ||||||
|                         SliverToBoxAdapter( |                         SliverToBoxAdapter( | ||||||
|                             child: accountAction(data).padding(horizontal: 4), |                           child: _AccountAction( | ||||||
|  |                             data: data, | ||||||
|  |                             accountRelationship: accountRelationship, | ||||||
|  |                             accountChat: accountChat, | ||||||
|  |                             relationshipAction: relationshipAction, | ||||||
|  |                             blockAction: blockAction, | ||||||
|  |                             directMessageAction: directMessageAction, | ||||||
|  |                           ).padding(horizontal: 4), | ||||||
|                         ), |                         ), | ||||||
|                       SliverToBoxAdapter( |                       SliverToBoxAdapter( | ||||||
|                         child: Card( |                         child: Card( | ||||||
| @@ -769,7 +947,8 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                       ), |                       ), | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ), | ||||||
|           ), |         ); | ||||||
|  |       }, | ||||||
|       error: |       error: | ||||||
|           (error, stackTrace) => AppScaffold( |           (error, stackTrace) => AppScaffold( | ||||||
|             appBar: AppBar(leading: const PageBackButton()), |             appBar: AppBar(leading: const PageBackButton()), | ||||||
|   | |||||||
| @@ -762,5 +762,127 @@ class _AccountBotDeveloperProviderElement | |||||||
|   String get uname => (origin as AccountBotDeveloperProvider).uname; |   String get uname => (origin as AccountBotDeveloperProvider).uname; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | String _$accountPublishersHash() => r'25f5695b4a5154163d77f1769876d826bf736609'; | ||||||
|  |  | ||||||
|  | /// See also [accountPublishers]. | ||||||
|  | @ProviderFor(accountPublishers) | ||||||
|  | const accountPublishersProvider = AccountPublishersFamily(); | ||||||
|  |  | ||||||
|  | /// See also [accountPublishers]. | ||||||
|  | class AccountPublishersFamily extends Family<AsyncValue<List<SnPublisher>>> { | ||||||
|  |   /// See also [accountPublishers]. | ||||||
|  |   const AccountPublishersFamily(); | ||||||
|  |  | ||||||
|  |   /// See also [accountPublishers]. | ||||||
|  |   AccountPublishersProvider call(String id) { | ||||||
|  |     return AccountPublishersProvider(id); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   AccountPublishersProvider getProviderOverride( | ||||||
|  |     covariant AccountPublishersProvider provider, | ||||||
|  |   ) { | ||||||
|  |     return call(provider.id); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||||
|  |  | ||||||
|  |   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||||
|  |       _allTransitiveDependencies; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String? get name => r'accountPublishersProvider'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// See also [accountPublishers]. | ||||||
|  | class AccountPublishersProvider | ||||||
|  |     extends AutoDisposeFutureProvider<List<SnPublisher>> { | ||||||
|  |   /// See also [accountPublishers]. | ||||||
|  |   AccountPublishersProvider(String id) | ||||||
|  |     : this._internal( | ||||||
|  |         (ref) => accountPublishers(ref as AccountPublishersRef, id), | ||||||
|  |         from: accountPublishersProvider, | ||||||
|  |         name: r'accountPublishersProvider', | ||||||
|  |         debugGetCreateSourceHash: | ||||||
|  |             const bool.fromEnvironment('dart.vm.product') | ||||||
|  |                 ? null | ||||||
|  |                 : _$accountPublishersHash, | ||||||
|  |         dependencies: AccountPublishersFamily._dependencies, | ||||||
|  |         allTransitiveDependencies: | ||||||
|  |             AccountPublishersFamily._allTransitiveDependencies, | ||||||
|  |         id: id, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |   AccountPublishersProvider._internal( | ||||||
|  |     super._createNotifier, { | ||||||
|  |     required super.name, | ||||||
|  |     required super.dependencies, | ||||||
|  |     required super.allTransitiveDependencies, | ||||||
|  |     required super.debugGetCreateSourceHash, | ||||||
|  |     required super.from, | ||||||
|  |     required this.id, | ||||||
|  |   }) : super.internal(); | ||||||
|  |  | ||||||
|  |   final String id; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Override overrideWith( | ||||||
|  |     FutureOr<List<SnPublisher>> Function(AccountPublishersRef provider) create, | ||||||
|  |   ) { | ||||||
|  |     return ProviderOverride( | ||||||
|  |       origin: this, | ||||||
|  |       override: AccountPublishersProvider._internal( | ||||||
|  |         (ref) => create(ref as AccountPublishersRef), | ||||||
|  |         from: from, | ||||||
|  |         name: null, | ||||||
|  |         dependencies: null, | ||||||
|  |         allTransitiveDependencies: null, | ||||||
|  |         debugGetCreateSourceHash: null, | ||||||
|  |         id: id, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   AutoDisposeFutureProviderElement<List<SnPublisher>> createElement() { | ||||||
|  |     return _AccountPublishersProviderElement(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   bool operator ==(Object other) { | ||||||
|  |     return other is AccountPublishersProvider && other.id == id; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   int get hashCode { | ||||||
|  |     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||||
|  |     hash = _SystemHash.combine(hash, id.hashCode); | ||||||
|  |  | ||||||
|  |     return _SystemHash.finish(hash); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||||
|  | // ignore: unused_element | ||||||
|  | mixin AccountPublishersRef on AutoDisposeFutureProviderRef<List<SnPublisher>> { | ||||||
|  |   /// The parameter `id` of this provider. | ||||||
|  |   String get id; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _AccountPublishersProviderElement | ||||||
|  |     extends AutoDisposeFutureProviderElement<List<SnPublisher>> | ||||||
|  |     with AccountPublishersRef { | ||||||
|  |   _AccountPublishersProviderElement(super.provider); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String get id => (origin as AccountPublishersProvider).id; | ||||||
|  | } | ||||||
|  |  | ||||||
| // ignore_for_file: type=lint | // ignore_for_file: type=lint | ||||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||||
|   | |||||||
| @@ -72,7 +72,7 @@ class MarketplaceWebFeedsScreen extends HookConsumerWidget { | |||||||
|         searchController.clear(); |         searchController.clear(); | ||||||
|       } |       } | ||||||
|       return null; |       return null; | ||||||
|     }, [query.value]); |     }, [query]); | ||||||
|  |  | ||||||
|     // Clean up timer on dispose |     // Clean up timer on dispose | ||||||
|     useEffect(() { |     useEffect(() { | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ part of 'explore.dart'; | |||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
| String _$activityListNotifierHash() => | String _$activityListNotifierHash() => | ||||||
|     r'a4968856ac34b59d47cfd4a7cbb39289aef2a1b1'; |     r'167021cada54da7c8d8437eef1ffb387a92ea2e3'; | ||||||
|  |  | ||||||
| /// Copied from Dart SDK | /// Copied from Dart SDK | ||||||
| class _SystemHash { | class _SystemHash { | ||||||
|   | |||||||
| @@ -1,15 +1,26 @@ | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter/services.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
|  | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/post.dart'; | import 'package:island/models/post.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/pods/userinfo.dart'; | import 'package:island/pods/userinfo.dart'; | ||||||
|  | import 'package:island/screens/posts/compose.dart'; | ||||||
|  | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
|  | import 'package:island/widgets/extended_refresh_indicator.dart'; | ||||||
| import 'package:island/widgets/post/post_item.dart'; | import 'package:island/widgets/post/post_item.dart'; | ||||||
|  | import 'package:island/widgets/post/post_pin_sheet.dart'; | ||||||
| import 'package:island/widgets/post/post_quick_reply.dart'; | import 'package:island/widgets/post/post_quick_reply.dart'; | ||||||
| import 'package:island/widgets/post/post_replies.dart'; | import 'package:island/widgets/post/post_replies.dart'; | ||||||
| import 'package:island/widgets/response.dart'; | import 'package:island/widgets/response.dart'; | ||||||
|  | import 'package:island/utils/share_utils.dart'; | ||||||
|  | import 'package:island/widgets/safety/abuse_report_helper.dart'; | ||||||
|  | import 'package:island/widgets/share/share_sheet.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
| @@ -46,6 +57,321 @@ class PostState extends StateNotifier<AsyncValue<SnPost?>> { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class PostActionButtons extends HookConsumerWidget { | ||||||
|  |   final SnPost post; | ||||||
|  |   final EdgeInsets renderingPadding; | ||||||
|  |   final VoidCallback? onRefresh; | ||||||
|  |   final Function(SnPost)? onUpdate; | ||||||
|  |  | ||||||
|  |   const PostActionButtons({ | ||||||
|  |     super.key, | ||||||
|  |     required this.post, | ||||||
|  |     this.renderingPadding = EdgeInsets.zero, | ||||||
|  |     this.onRefresh, | ||||||
|  |     this.onUpdate, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final user = ref.watch(userInfoProvider); | ||||||
|  |     final isAuthor = | ||||||
|  |         user.value != null && user.value?.id == post.publisher.accountId; | ||||||
|  |  | ||||||
|  |     final actions = <Widget>[]; | ||||||
|  |  | ||||||
|  |     const kButtonHeight = 40.0; | ||||||
|  |     const kButtonRadius = 20.0; | ||||||
|  |  | ||||||
|  |     // 1. Author-only actions first | ||||||
|  |     if (isAuthor) { | ||||||
|  |       // Combined edit/delete actions using custom segmented-style buttons | ||||||
|  |       final editButtons = <Widget>[ | ||||||
|  |         FilledButton.tonal( | ||||||
|  |           onPressed: () { | ||||||
|  |             context.pushNamed('postEdit', pathParameters: {'id': post.id}).then( | ||||||
|  |               (value) { | ||||||
|  |                 if (value != null) { | ||||||
|  |                   onRefresh?.call(); | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |             ); | ||||||
|  |           }, | ||||||
|  |           style: FilledButton.styleFrom( | ||||||
|  |             shape: const RoundedRectangleBorder( | ||||||
|  |               borderRadius: BorderRadius.only( | ||||||
|  |                 topLeft: Radius.circular(kButtonRadius), | ||||||
|  |                 bottomLeft: Radius.circular(kButtonRadius), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           child: Row( | ||||||
|  |             mainAxisSize: MainAxisSize.min, | ||||||
|  |             children: [ | ||||||
|  |               const Icon(Symbols.edit, size: 18), | ||||||
|  |               const Gap(4), | ||||||
|  |               Text('edit'.tr()), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         Tooltip( | ||||||
|  |           message: 'delete'.tr(), | ||||||
|  |           child: FilledButton.tonal( | ||||||
|  |             onPressed: () { | ||||||
|  |               showConfirmAlert('deletePostHint'.tr(), 'deletePost'.tr()).then(( | ||||||
|  |                 confirm, | ||||||
|  |               ) { | ||||||
|  |                 if (confirm) { | ||||||
|  |                   final client = ref.watch(apiClientProvider); | ||||||
|  |                   client | ||||||
|  |                       .delete('/sphere/posts/${post.id}') | ||||||
|  |                       .catchError((err) { | ||||||
|  |                         showErrorAlert(err); | ||||||
|  |                         return err; | ||||||
|  |                       }) | ||||||
|  |                       .then((_) { | ||||||
|  |                         onRefresh?.call(); | ||||||
|  |                       }); | ||||||
|  |                 } | ||||||
|  |               }); | ||||||
|  |             }, | ||||||
|  |             style: FilledButton.styleFrom( | ||||||
|  |               shape: const RoundedRectangleBorder( | ||||||
|  |                 borderRadius: BorderRadius.only( | ||||||
|  |                   topRight: Radius.circular(kButtonRadius), | ||||||
|  |                   bottomRight: Radius.circular(kButtonRadius), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             child: const Icon(Symbols.delete, size: 18), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ]; | ||||||
|  |  | ||||||
|  |       actions.add( | ||||||
|  |         Row( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|  |           children: | ||||||
|  |               editButtons | ||||||
|  |                   .map((e) => SizedBox(height: kButtonHeight, child: e)) | ||||||
|  |                   .expand((widget) => [widget, const VerticalDivider(width: 1)]) | ||||||
|  |                   .toList() | ||||||
|  |                 ..removeLast(), | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       // Pin/Unpin actions (also author-only) | ||||||
|  |       if (post.pinMode == null) { | ||||||
|  |         actions.add( | ||||||
|  |           FilledButton.tonalIcon( | ||||||
|  |             onPressed: () { | ||||||
|  |               showModalBottomSheet( | ||||||
|  |                 context: context, | ||||||
|  |                 isScrollControlled: true, | ||||||
|  |                 builder: (context) => PostPinSheet(post: post), | ||||||
|  |               ).then((value) { | ||||||
|  |                 if (value is int) { | ||||||
|  |                   onUpdate?.call(post.copyWith(pinMode: value)); | ||||||
|  |                 } | ||||||
|  |               }); | ||||||
|  |             }, | ||||||
|  |             icon: const Icon(Symbols.keep), | ||||||
|  |             label: Text('pinPost'.tr()), | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         actions.add( | ||||||
|  |           FilledButton.tonalIcon( | ||||||
|  |             onPressed: () { | ||||||
|  |               showConfirmAlert('unpinPostHint'.tr(), 'unpinPost'.tr()).then(( | ||||||
|  |                 confirm, | ||||||
|  |               ) async { | ||||||
|  |                 if (confirm) { | ||||||
|  |                   final client = ref.watch(apiClientProvider); | ||||||
|  |                   try { | ||||||
|  |                     if (context.mounted) showLoadingModal(context); | ||||||
|  |                     await client.delete('/sphere/posts/${post.id}/pin'); | ||||||
|  |                     onUpdate?.call(post.copyWith(pinMode: null)); | ||||||
|  |                   } catch (err) { | ||||||
|  |                     showErrorAlert(err); | ||||||
|  |                   } finally { | ||||||
|  |                     if (context.mounted) hideLoadingModal(context); | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               }); | ||||||
|  |             }, | ||||||
|  |             icon: const Icon(Symbols.keep_off), | ||||||
|  |             label: Text('unpinPost'.tr()), | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 2. Replies and forwards | ||||||
|  |     final replyButtons = <Widget>[ | ||||||
|  |       FilledButton.tonal( | ||||||
|  |         onPressed: () { | ||||||
|  |           context.pushNamed( | ||||||
|  |             'postCompose', | ||||||
|  |             extra: PostComposeInitialState(replyingTo: post), | ||||||
|  |           ); | ||||||
|  |         }, | ||||||
|  |         style: FilledButton.styleFrom( | ||||||
|  |           shape: const RoundedRectangleBorder( | ||||||
|  |             borderRadius: BorderRadius.only( | ||||||
|  |               topLeft: Radius.circular(kButtonRadius), | ||||||
|  |               bottomLeft: Radius.circular(kButtonRadius), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         child: Row( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|  |           children: [ | ||||||
|  |             const Icon(Symbols.reply, size: 18), | ||||||
|  |             const Gap(4), | ||||||
|  |             Text('reply'.tr()), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       Tooltip( | ||||||
|  |         message: 'forward'.tr(), | ||||||
|  |         child: FilledButton.tonal( | ||||||
|  |           onPressed: () { | ||||||
|  |             context.pushNamed( | ||||||
|  |               'postCompose', | ||||||
|  |               extra: PostComposeInitialState(forwardingTo: post), | ||||||
|  |             ); | ||||||
|  |           }, | ||||||
|  |           style: FilledButton.styleFrom( | ||||||
|  |             shape: const RoundedRectangleBorder( | ||||||
|  |               borderRadius: BorderRadius.only( | ||||||
|  |                 topRight: Radius.circular(kButtonRadius), | ||||||
|  |                 bottomRight: Radius.circular(kButtonRadius), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           child: const Icon(Symbols.forward, size: 18), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     actions.add( | ||||||
|  |       Row( | ||||||
|  |         mainAxisSize: MainAxisSize.min, | ||||||
|  |         children: | ||||||
|  |             replyButtons | ||||||
|  |                 .map((e) => SizedBox(height: kButtonHeight, child: e)) | ||||||
|  |                 .toList(), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // 3. Share, copy link, and report | ||||||
|  |     final shareButtons = <Widget>[ | ||||||
|  |       FilledButton.tonal( | ||||||
|  |         onPressed: () { | ||||||
|  |           showShareSheetLink( | ||||||
|  |             context: context, | ||||||
|  |             link: 'https://solian.app/posts/${post.id}', | ||||||
|  |             title: 'sharePost'.tr(), | ||||||
|  |             toSystem: true, | ||||||
|  |           ); | ||||||
|  |         }, | ||||||
|  |         style: FilledButton.styleFrom( | ||||||
|  |           shape: const RoundedRectangleBorder( | ||||||
|  |             borderRadius: BorderRadius.only( | ||||||
|  |               topLeft: Radius.circular(kButtonRadius), | ||||||
|  |               bottomLeft: Radius.circular(kButtonRadius), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         child: Row( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|  |           children: [ | ||||||
|  |             const Icon(Symbols.share, size: 18), | ||||||
|  |             const Gap(4), | ||||||
|  |             Text('share'.tr()), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     if (!kIsWeb) { | ||||||
|  |       shareButtons.add( | ||||||
|  |         Tooltip( | ||||||
|  |           message: 'sharePostPhoto'.tr(), | ||||||
|  |           child: FilledButton.tonal( | ||||||
|  |             onPressed: () => sharePostAsScreenshot(context, ref, post), | ||||||
|  |             style: FilledButton.styleFrom( | ||||||
|  |               shape: const RoundedRectangleBorder( | ||||||
|  |                 borderRadius: BorderRadius.only( | ||||||
|  |                   topRight: Radius.circular(kButtonRadius), | ||||||
|  |                   bottomRight: Radius.circular(kButtonRadius), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             child: const Icon(Symbols.share_reviews, size: 18), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     actions.add( | ||||||
|  |       Row( | ||||||
|  |         mainAxisSize: MainAxisSize.min, | ||||||
|  |         children: | ||||||
|  |             shareButtons | ||||||
|  |                 .map((e) => SizedBox(height: kButtonHeight, child: e)) | ||||||
|  |                 .expand((widget) => [widget, const VerticalDivider(width: 1)]) | ||||||
|  |                 .toList() | ||||||
|  |               ..removeLast(), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     actions.add( | ||||||
|  |       FilledButton.tonalIcon( | ||||||
|  |         onPressed: () { | ||||||
|  |           Clipboard.setData( | ||||||
|  |             ClipboardData(text: 'https://solian.app/posts/${post.id}'), | ||||||
|  |           ); | ||||||
|  |         }, | ||||||
|  |         icon: const Icon(Symbols.link), | ||||||
|  |         label: Text('copyLink'.tr()), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     actions.add( | ||||||
|  |       FilledButton.tonalIcon( | ||||||
|  |         onPressed: () { | ||||||
|  |           showAbuseReportSheet(context, resourceIdentifier: 'post/${post.id}'); | ||||||
|  |         }, | ||||||
|  |         icon: const Icon(Symbols.flag), | ||||||
|  |         label: Text('abuseReport'.tr()), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // Add gaps between actions (excluding first one) using FP style | ||||||
|  |     final children = | ||||||
|  |         actions.asMap().entries.expand((entry) { | ||||||
|  |           final index = entry.key; | ||||||
|  |           final action = entry.value; | ||||||
|  |           if (index == 0) { | ||||||
|  |             return [action]; | ||||||
|  |           } else { | ||||||
|  |             return [const Gap(8), action]; | ||||||
|  |           } | ||||||
|  |         }).toList(); | ||||||
|  |  | ||||||
|  |     return Container( | ||||||
|  |       height: kButtonHeight, | ||||||
|  |       margin: const EdgeInsets.only(bottom: 12), | ||||||
|  |       child: ListView( | ||||||
|  |         scrollDirection: Axis.horizontal, | ||||||
|  |         padding: EdgeInsets.symmetric(horizontal: renderingPadding.horizontal), | ||||||
|  |         children: children, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| class PostDetailScreen extends HookConsumerWidget { | class PostDetailScreen extends HookConsumerWidget { | ||||||
|   final String id; |   final String id; | ||||||
|   const PostDetailScreen({super.key, required this.id}); |   const PostDetailScreen({super.key, required this.id}); | ||||||
| @@ -66,7 +392,13 @@ class PostDetailScreen extends HookConsumerWidget { | |||||||
|           return Stack( |           return Stack( | ||||||
|             fit: StackFit.expand, |             fit: StackFit.expand, | ||||||
|             children: [ |             children: [ | ||||||
|               CustomScrollView( |               ExtendedRefreshIndicator( | ||||||
|  |                 onRefresh: () async { | ||||||
|  |                   ref.invalidate(postProvider(id)); | ||||||
|  |                   ref.invalidate(postRepliesNotifierProvider(id)); | ||||||
|  |                 }, | ||||||
|  |                 child: CustomScrollView( | ||||||
|  |                   physics: const AlwaysScrollableScrollPhysics(), | ||||||
|                   slivers: [ |                   slivers: [ | ||||||
|                     SliverToBoxAdapter( |                     SliverToBoxAdapter( | ||||||
|                       child: Center( |                       child: Center( | ||||||
| @@ -86,10 +418,33 @@ class PostDetailScreen extends HookConsumerWidget { | |||||||
|                         ), |                         ), | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
|  |                     SliverToBoxAdapter( | ||||||
|  |                       child: Center( | ||||||
|  |                         child: ConstrainedBox( | ||||||
|  |                           constraints: BoxConstraints(maxWidth: 600), | ||||||
|  |                           child: PostActionButtons( | ||||||
|  |                             post: post, | ||||||
|  |                             renderingPadding: const EdgeInsets.symmetric( | ||||||
|  |                               horizontal: 8, | ||||||
|  |                             ), | ||||||
|  |                             onRefresh: () { | ||||||
|  |                               ref.invalidate(postProvider(id)); | ||||||
|  |                               ref.invalidate(postRepliesNotifierProvider(id)); | ||||||
|  |                             }, | ||||||
|  |                             onUpdate: (newItem) { | ||||||
|  |                               ref | ||||||
|  |                                   .read(postStateProvider(id).notifier) | ||||||
|  |                                   .updatePost(newItem); | ||||||
|  |                             }, | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|                     PostRepliesList(postId: id, maxWidth: 600), |                     PostRepliesList(postId: id, maxWidth: 600), | ||||||
|                     SliverGap(MediaQuery.of(context).padding.bottom + 80), |                     SliverGap(MediaQuery.of(context).padding.bottom + 80), | ||||||
|                   ], |                   ], | ||||||
|                 ), |                 ), | ||||||
|  |               ), | ||||||
|               if (user.value != null) |               if (user.value != null) | ||||||
|                 Positioned( |                 Positioned( | ||||||
|                   bottom: 0, |                   bottom: 0, | ||||||
| @@ -126,7 +481,7 @@ class PostDetailScreen extends HookConsumerWidget { | |||||||
|         error: |         error: | ||||||
|             (e, _) => ResponseErrorWidget( |             (e, _) => ResponseErrorWidget( | ||||||
|               error: e, |               error: e, | ||||||
|               onRetry: () => ref.invalidate(postStateProvider(id)), |               onRetry: () => ref.invalidate(postProvider(id)), | ||||||
|             ), |             ), | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -27,6 +27,224 @@ import 'package:styled_widget/styled_widget.dart'; | |||||||
|  |  | ||||||
| part 'pub_profile.g.dart'; | part 'pub_profile.g.dart'; | ||||||
|  |  | ||||||
|  | class _PublisherBasisWidget extends StatelessWidget { | ||||||
|  |   final SnPublisher data; | ||||||
|  |   final AsyncValue<SnSubscriptionStatus> subStatus; | ||||||
|  |   final ValueNotifier<bool> subscribing; | ||||||
|  |   final VoidCallback subscribe; | ||||||
|  |   final VoidCallback unsubscribe; | ||||||
|  |  | ||||||
|  |   const _PublisherBasisWidget({ | ||||||
|  |     required this.data, | ||||||
|  |     required this.subStatus, | ||||||
|  |     required this.subscribing, | ||||||
|  |     required this.subscribe, | ||||||
|  |     required this.unsubscribe, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Row( | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |       spacing: 20, | ||||||
|  |       children: [ | ||||||
|  |         GestureDetector( | ||||||
|  |           child: Badge( | ||||||
|  |             isLabelVisible: data.type == 0, | ||||||
|  |             padding: EdgeInsets.all(4), | ||||||
|  |             label: Icon( | ||||||
|  |               Symbols.launch, | ||||||
|  |               size: 16, | ||||||
|  |               color: Theme.of(context).colorScheme.onPrimary, | ||||||
|  |             ), | ||||||
|  |             backgroundColor: Theme.of(context).colorScheme.primary, | ||||||
|  |             offset: Offset(0, 48), | ||||||
|  |             child: ProfilePictureWidget( | ||||||
|  |               file: data.picture, | ||||||
|  |               radius: 32, | ||||||
|  |               borderRadius: data.type == 0 ? null : 12, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           onTap: () { | ||||||
|  |             if (data.account?.name != null) { | ||||||
|  |               Navigator.pop(context, true); | ||||||
|  |               context.pushNamed( | ||||||
|  |                 'accountProfile', | ||||||
|  |                 pathParameters: {'name': data.account!.name}, | ||||||
|  |               ); | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |         ), | ||||||
|  |         Expanded( | ||||||
|  |           child: Column( | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |             children: [ | ||||||
|  |               Row( | ||||||
|  |                 spacing: 6, | ||||||
|  |                 children: [ | ||||||
|  |                   Text(data.nick).fontSize(20), | ||||||
|  |                   if (data.verification != null) | ||||||
|  |                     VerificationMark(mark: data.verification!), | ||||||
|  |                   Expanded( | ||||||
|  |                     child: Text( | ||||||
|  |                       '@${data.name}', | ||||||
|  |                       maxLines: 1, | ||||||
|  |                       overflow: TextOverflow.ellipsis, | ||||||
|  |                     ).fontSize(14).opacity(0.85), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |               if (data.type == 0 && data.account != null) | ||||||
|  |                 Row( | ||||||
|  |                   crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |                   spacing: 6, | ||||||
|  |                   children: [ | ||||||
|  |                     Icon( | ||||||
|  |                       data.type == 0 ? Symbols.person : Symbols.workspaces, | ||||||
|  |                       fill: 1, | ||||||
|  |                       size: 17, | ||||||
|  |                     ), | ||||||
|  |                     Text( | ||||||
|  |                       'publisherBelongsTo'.tr(args: ['@${data.account!.name}']), | ||||||
|  |                     ).fontSize(14), | ||||||
|  |                   ], | ||||||
|  |                 ).opacity(0.85), | ||||||
|  |               const Gap(4), | ||||||
|  |               if (data.type == 0 && data.account != null) | ||||||
|  |                 AccountStatusWidget( | ||||||
|  |                   uname: data.account!.name, | ||||||
|  |                   padding: EdgeInsets.zero, | ||||||
|  |                 ), | ||||||
|  |               subStatus | ||||||
|  |                   .when( | ||||||
|  |                     data: | ||||||
|  |                         (status) => FilledButton.icon( | ||||||
|  |                           onPressed: | ||||||
|  |                               subscribing.value | ||||||
|  |                                   ? null | ||||||
|  |                                   : (status.isSubscribed | ||||||
|  |                                       ? unsubscribe | ||||||
|  |                                       : subscribe), | ||||||
|  |                           icon: Icon( | ||||||
|  |                             status.isSubscribed | ||||||
|  |                                 ? Symbols.remove_circle | ||||||
|  |                                 : Symbols.add_circle, | ||||||
|  |                           ), | ||||||
|  |                           label: | ||||||
|  |                               Text( | ||||||
|  |                                 status.isSubscribed | ||||||
|  |                                     ? 'unsubscribe' | ||||||
|  |                                     : 'subscribe', | ||||||
|  |                               ).tr(), | ||||||
|  |                           style: ButtonStyle( | ||||||
|  |                             visualDensity: VisualDensity(vertical: -2), | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                     error: (_, _) => const SizedBox(), | ||||||
|  |                     loading: | ||||||
|  |                         () => const SizedBox( | ||||||
|  |                           height: 36, | ||||||
|  |                           child: Center( | ||||||
|  |                             child: SizedBox( | ||||||
|  |                               width: 20, | ||||||
|  |                               height: 20, | ||||||
|  |                               child: CircularProgressIndicator(strokeWidth: 2), | ||||||
|  |                             ), | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                   ) | ||||||
|  |                   .padding(top: 8), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ).padding(horizontal: 24, top: 24); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _PublisherBadgesWidget extends StatelessWidget { | ||||||
|  |   final SnPublisher data; | ||||||
|  |   final AsyncValue<List<SnAccountBadge>> badges; | ||||||
|  |  | ||||||
|  |   const _PublisherBadgesWidget({required this.data, required this.badges}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return (badges.value?.isNotEmpty ?? false) | ||||||
|  |         ? Card( | ||||||
|  |           child: BadgeList( | ||||||
|  |             badges: badges.value!, | ||||||
|  |           ).padding(horizontal: 26, vertical: 20), | ||||||
|  |         ).padding(horizontal: 4) | ||||||
|  |         : const SizedBox.shrink(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _PublisherVerificationWidget extends StatelessWidget { | ||||||
|  |   final SnPublisher data; | ||||||
|  |  | ||||||
|  |   const _PublisherVerificationWidget({required this.data}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return (data.verification != null) | ||||||
|  |         ? Card( | ||||||
|  |           margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||||
|  |           child: VerificationStatusCard(mark: data.verification!), | ||||||
|  |         ) | ||||||
|  |         : const SizedBox.shrink(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _PublisherBioWidget extends StatelessWidget { | ||||||
|  |   final SnPublisher data; | ||||||
|  |  | ||||||
|  |   const _PublisherBioWidget({required this.data}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Card( | ||||||
|  |       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||||
|  |       child: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |         children: [ | ||||||
|  |           Text('bio').tr().bold().fontSize(15).padding(bottom: 8), | ||||||
|  |           if (data.bio.isEmpty) | ||||||
|  |             Text('descriptionNone').tr().italic() | ||||||
|  |           else | ||||||
|  |             MarkdownTextContent( | ||||||
|  |               content: data.bio, | ||||||
|  |               linesMargin: EdgeInsets.zero, | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |       ).padding(horizontal: 20, vertical: 16), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _PublisherCategoryTabWidget extends StatelessWidget { | ||||||
|  |   final TabController categoryTabController; | ||||||
|  |  | ||||||
|  |   const _PublisherCategoryTabWidget({required this.categoryTabController}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Card( | ||||||
|  |       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||||
|  |       child: TabBar( | ||||||
|  |         controller: categoryTabController, | ||||||
|  |         dividerColor: Colors.transparent, | ||||||
|  |         splashBorderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |         tabs: [ | ||||||
|  |           Tab(text: 'all'.tr()), | ||||||
|  |           Tab(text: 'postTypePost'.tr()), | ||||||
|  |           Tab(text: 'postArticle'.tr()), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| @riverpod | @riverpod | ||||||
| Future<SnPublisher> publisher(Ref ref, String uname) async { | Future<SnPublisher> publisher(Ref ref, String uname) async { | ||||||
|   final apiClient = ref.watch(apiClientProvider); |   final apiClient = ref.watch(apiClientProvider); | ||||||
| @@ -132,170 +350,6 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|       offset: Offset(1.0, 1.0), |       offset: Offset(1.0, 1.0), | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     Widget publisherBasisWidget(SnPublisher data) => Row( |  | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|       spacing: 20, |  | ||||||
|       children: [ |  | ||||||
|         GestureDetector( |  | ||||||
|           child: Badge( |  | ||||||
|             isLabelVisible: data.type == 0, |  | ||||||
|             padding: EdgeInsets.all(4), |  | ||||||
|             label: Icon( |  | ||||||
|               Symbols.launch, |  | ||||||
|               size: 16, |  | ||||||
|               color: Theme.of(context).colorScheme.onPrimary, |  | ||||||
|             ), |  | ||||||
|             backgroundColor: Theme.of(context).colorScheme.primary, |  | ||||||
|             offset: Offset(0, 48), |  | ||||||
|             child: ProfilePictureWidget( |  | ||||||
|               file: data.picture, |  | ||||||
|               radius: 32, |  | ||||||
|               borderRadius: data.type == 0 ? null : 12, |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           onTap: () { |  | ||||||
|             Navigator.pop(context, true); |  | ||||||
|             if (data.account?.name != null) { |  | ||||||
|               context.pushNamed( |  | ||||||
|                 'accountProfile', |  | ||||||
|                 pathParameters: {'name': data.account!.name}, |  | ||||||
|               ); |  | ||||||
|             } |  | ||||||
|           }, |  | ||||||
|         ), |  | ||||||
|         Expanded( |  | ||||||
|           child: Column( |  | ||||||
|             crossAxisAlignment: CrossAxisAlignment.stretch, |  | ||||||
|             children: [ |  | ||||||
|               Row( |  | ||||||
|                 spacing: 6, |  | ||||||
|                 children: [ |  | ||||||
|                   Text(data.nick).fontSize(20), |  | ||||||
|                   if (data.verification != null) |  | ||||||
|                     VerificationMark(mark: data.verification!), |  | ||||||
|                   Expanded( |  | ||||||
|                     child: Text( |  | ||||||
|                       '@${data.name}', |  | ||||||
|                       maxLines: 1, |  | ||||||
|                       overflow: TextOverflow.ellipsis, |  | ||||||
|                     ).fontSize(14).opacity(0.85), |  | ||||||
|                   ), |  | ||||||
|                 ], |  | ||||||
|               ), |  | ||||||
|               if (data.type == 0 && data.account != null) |  | ||||||
|                 Row( |  | ||||||
|                   crossAxisAlignment: CrossAxisAlignment.center, |  | ||||||
|                   spacing: 6, |  | ||||||
|                   children: [ |  | ||||||
|                     Icon( |  | ||||||
|                       data.type == 0 ? Symbols.person : Symbols.workspaces, |  | ||||||
|                       fill: 1, |  | ||||||
|                       size: 17, |  | ||||||
|                     ), |  | ||||||
|                     Text( |  | ||||||
|                       'publisherBelongsTo'.tr(args: ['@${data.account!.name}']), |  | ||||||
|                     ).fontSize(14), |  | ||||||
|                   ], |  | ||||||
|                 ).opacity(0.85), |  | ||||||
|               const Gap(4), |  | ||||||
|               if (data.type == 0 && data.account != null) |  | ||||||
|                 AccountStatusWidget( |  | ||||||
|                   uname: data.account!.name, |  | ||||||
|                   padding: EdgeInsets.zero, |  | ||||||
|                 ), |  | ||||||
|               subStatus |  | ||||||
|                   .when( |  | ||||||
|                     data: |  | ||||||
|                         (status) => FilledButton.icon( |  | ||||||
|                           onPressed: |  | ||||||
|                               subscribing.value |  | ||||||
|                                   ? null |  | ||||||
|                                   : (status.isSubscribed |  | ||||||
|                                       ? unsubscribe |  | ||||||
|                                       : subscribe), |  | ||||||
|                           icon: Icon( |  | ||||||
|                             status.isSubscribed |  | ||||||
|                                 ? Symbols.remove_circle |  | ||||||
|                                 : Symbols.add_circle, |  | ||||||
|                           ), |  | ||||||
|                           label: |  | ||||||
|                               Text( |  | ||||||
|                                 status.isSubscribed |  | ||||||
|                                     ? 'unsubscribe' |  | ||||||
|                                     : 'subscribe', |  | ||||||
|                               ).tr(), |  | ||||||
|                           style: ButtonStyle( |  | ||||||
|                             visualDensity: VisualDensity(vertical: -2), |  | ||||||
|                           ), |  | ||||||
|                         ), |  | ||||||
|                     error: (_, _) => const SizedBox(), |  | ||||||
|                     loading: |  | ||||||
|                         () => const SizedBox( |  | ||||||
|                           height: 36, |  | ||||||
|                           child: Center( |  | ||||||
|                             child: SizedBox( |  | ||||||
|                               width: 20, |  | ||||||
|                               height: 20, |  | ||||||
|                               child: CircularProgressIndicator(strokeWidth: 2), |  | ||||||
|                             ), |  | ||||||
|                           ), |  | ||||||
|                         ), |  | ||||||
|                   ) |  | ||||||
|                   .padding(top: 8), |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|       ], |  | ||||||
|     ).padding(horizontal: 24, top: 24); |  | ||||||
|  |  | ||||||
|     Widget publisherBadgesWidget(SnPublisher data) => |  | ||||||
|         (badges.value?.isNotEmpty ?? false) |  | ||||||
|             ? Card( |  | ||||||
|               child: BadgeList( |  | ||||||
|                 badges: badges.value!, |  | ||||||
|               ).padding(horizontal: 26, vertical: 20), |  | ||||||
|             ).padding(horizontal: 4) |  | ||||||
|             : const SizedBox.shrink(); |  | ||||||
|  |  | ||||||
|     Widget publisherVerificationWidget(SnPublisher data) => |  | ||||||
|         (data.verification != null) |  | ||||||
|             ? Card( |  | ||||||
|               margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), |  | ||||||
|               child: VerificationStatusCard(mark: data.verification!), |  | ||||||
|             ) |  | ||||||
|             : const SizedBox.shrink(); |  | ||||||
|  |  | ||||||
|     Widget publisherBioWidget(SnPublisher data) => Card( |  | ||||||
|       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), |  | ||||||
|       child: Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, |  | ||||||
|         children: [ |  | ||||||
|           Text('bio').tr().bold().fontSize(15).padding(bottom: 8), |  | ||||||
|           if (data.bio.isEmpty) |  | ||||||
|             Text('descriptionNone').tr().italic() |  | ||||||
|           else |  | ||||||
|             MarkdownTextContent( |  | ||||||
|               content: data.bio, |  | ||||||
|               linesMargin: EdgeInsets.zero, |  | ||||||
|             ), |  | ||||||
|         ], |  | ||||||
|       ).padding(horizontal: 20, vertical: 16), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     Widget publisherCategoryTabWidget() => Card( |  | ||||||
|       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), |  | ||||||
|       child: TabBar( |  | ||||||
|         controller: categoryTabController, |  | ||||||
|         dividerColor: Colors.transparent, |  | ||||||
|         splashBorderRadius: const BorderRadius.all(Radius.circular(8)), |  | ||||||
|         tabs: [ |  | ||||||
|           Tab(text: 'all'.tr()), |  | ||||||
|           Tab(text: 'postTypePost'.tr()), |  | ||||||
|           Tab(text: 'postArticle'.tr()), |  | ||||||
|         ], |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return publisher.when( |     return publisher.when( | ||||||
|       data: |       data: | ||||||
|           (data) => AppScaffold( |           (data) => AppScaffold( | ||||||
| @@ -351,7 +405,9 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|                               SliverGap(16), |                               SliverGap(16), | ||||||
|                               SliverPostList(pubName: name, pinned: true), |                               SliverPostList(pubName: name, pinned: true), | ||||||
|                               SliverToBoxAdapter( |                               SliverToBoxAdapter( | ||||||
|                                 child: publisherCategoryTabWidget(), |                                 child: _PublisherCategoryTabWidget( | ||||||
|  |                                   categoryTabController: categoryTabController, | ||||||
|  |                                 ), | ||||||
|                               ), |                               ), | ||||||
|                               SliverPostList( |                               SliverPostList( | ||||||
|                                 key: ValueKey(categoryTab.value), |                                 key: ValueKey(categoryTab.value), | ||||||
| @@ -377,10 +433,19 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|                               child: Column( |                               child: Column( | ||||||
|                                 crossAxisAlignment: CrossAxisAlignment.stretch, |                                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|                                 children: [ |                                 children: [ | ||||||
|                                   publisherBasisWidget(data).padding(bottom: 8), |                                   _PublisherBasisWidget( | ||||||
|                                   publisherBadgesWidget(data), |                                     data: data, | ||||||
|                                   publisherVerificationWidget(data), |                                     subStatus: subStatus, | ||||||
|                                   publisherBioWidget(data), |                                     subscribing: subscribing, | ||||||
|  |                                     subscribe: subscribe, | ||||||
|  |                                     unsubscribe: unsubscribe, | ||||||
|  |                                   ).padding(bottom: 8), | ||||||
|  |                                   _PublisherBadgesWidget( | ||||||
|  |                                     data: data, | ||||||
|  |                                     badges: badges, | ||||||
|  |                                   ), | ||||||
|  |                                   _PublisherVerificationWidget(data: data), | ||||||
|  |                                   _PublisherBioWidget(data: data), | ||||||
|                                 ], |                                 ], | ||||||
|                               ), |                               ), | ||||||
|                             ), |                             ), | ||||||
| @@ -432,15 +497,32 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|                           ), |                           ), | ||||||
|                         ), |                         ), | ||||||
|                         SliverToBoxAdapter( |                         SliverToBoxAdapter( | ||||||
|                           child: publisherBasisWidget(data).padding(bottom: 8), |                           child: _PublisherBasisWidget( | ||||||
|  |                             data: data, | ||||||
|  |                             subStatus: subStatus, | ||||||
|  |                             subscribing: subscribing, | ||||||
|  |                             subscribe: subscribe, | ||||||
|  |                             unsubscribe: unsubscribe, | ||||||
|  |                           ).padding(bottom: 8), | ||||||
|                         ), |                         ), | ||||||
|                         SliverToBoxAdapter(child: publisherBadgesWidget(data)), |  | ||||||
|                         SliverToBoxAdapter( |                         SliverToBoxAdapter( | ||||||
|                           child: publisherVerificationWidget(data), |                           child: _PublisherBadgesWidget( | ||||||
|  |                             data: data, | ||||||
|  |                             badges: badges, | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                         SliverToBoxAdapter( | ||||||
|  |                           child: _PublisherVerificationWidget(data: data), | ||||||
|  |                         ), | ||||||
|  |                         SliverToBoxAdapter( | ||||||
|  |                           child: _PublisherBioWidget(data: data), | ||||||
|                         ), |                         ), | ||||||
|                         SliverToBoxAdapter(child: publisherBioWidget(data)), |  | ||||||
|                         SliverPostList(pubName: name, pinned: true), |                         SliverPostList(pubName: name, pinned: true), | ||||||
|                         SliverToBoxAdapter(child: publisherCategoryTabWidget()), |                         SliverToBoxAdapter( | ||||||
|  |                           child: _PublisherCategoryTabWidget( | ||||||
|  |                             categoryTabController: categoryTabController, | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|                         SliverPostList( |                         SliverPostList( | ||||||
|                           key: ValueKey(categoryTab.value), |                           key: ValueKey(categoryTab.value), | ||||||
|                           pubName: name, |                           pubName: name, | ||||||
|   | |||||||
| @@ -450,6 +450,20 @@ class SettingsScreen extends HookConsumerWidget { | |||||||
|           }, |           }, | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|  |       ListTile( | ||||||
|  |         minLeadingWidth: 48, | ||||||
|  |         title: Text('settingsDataSavingMode').tr(), | ||||||
|  |         contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||||
|  |         leading: const Icon(Symbols.data_saver_on_rounded), | ||||||
|  |         trailing: Switch( | ||||||
|  |           value: settings.dataSavingMode, | ||||||
|  |           onChanged: (value) { | ||||||
|  |             ref | ||||||
|  |                 .read(appSettingsNotifierProvider.notifier) | ||||||
|  |                 .setDataSavingMode(value); | ||||||
|  |           }, | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
|     // Desktop-specific settings |     // Desktop-specific settings | ||||||
|   | |||||||
| @@ -77,7 +77,7 @@ class MarketplaceStickersScreen extends HookConsumerWidget { | |||||||
|         searchController.clear(); |         searchController.clear(); | ||||||
|       } |       } | ||||||
|       return null; |       return null; | ||||||
|     }, [query.value]); |     }, [query]); | ||||||
|  |  | ||||||
|     // Clean up timer on dispose |     // Clean up timer on dispose | ||||||
|     useEffect(() { |     useEffect(() { | ||||||
|   | |||||||
| @@ -48,7 +48,11 @@ class TrayService { | |||||||
|   void handleAction(MenuItem item) { |   void handleAction(MenuItem item) { | ||||||
|     switch (item.key) { |     switch (item.key) { | ||||||
|       case 'show_window': |       case 'show_window': | ||||||
|  |         if (appWindow.isVisible) { | ||||||
|  |           appWindow.restore(); | ||||||
|  |         } else { | ||||||
|           appWindow.show(); |           appWindow.show(); | ||||||
|  |         } | ||||||
|         break; |         break; | ||||||
|       case 'exit_app': |       case 'exit_app': | ||||||
|         appWindow.close(); |         appWindow.close(); | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import 'dart:async'; | |||||||
| import 'dart:developer'; | import 'dart:developer'; | ||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  |  | ||||||
|  | import 'package:archive/archive.dart'; | ||||||
| import 'package:dio/dio.dart'; | import 'package:dio/dio.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| @@ -10,6 +11,9 @@ import 'package:flutter_app_update/update_model.dart'; | |||||||
| import 'package:island/widgets/content/markdown.dart'; | import 'package:island/widgets/content/markdown.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:package_info_plus/package_info_plus.dart'; | import 'package:package_info_plus/package_info_plus.dart'; | ||||||
|  | import 'package:path_provider/path_provider.dart'; | ||||||
|  | import 'package:path/path.dart' as path; | ||||||
|  | import 'package:process_run/process_run.dart'; | ||||||
| import 'package:collection/collection.dart'; // Added for firstWhereOrNull | import 'package:collection/collection.dart'; // Added for firstWhereOrNull | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:url_launcher/url_launcher.dart'; | import 'package:url_launcher/url_launcher.dart'; | ||||||
| @@ -180,9 +184,13 @@ class UpdateService { | |||||||
|       useRootNavigator: true, |       useRootNavigator: true, | ||||||
|       builder: (ctx) { |       builder: (ctx) { | ||||||
|         String? androidUpdateUrl; |         String? androidUpdateUrl; | ||||||
|  |         String? windowsUpdateUrl; | ||||||
|         if (Platform.isAndroid) { |         if (Platform.isAndroid) { | ||||||
|           androidUpdateUrl = _getAndroidUpdateUrl(release.assets); |           androidUpdateUrl = _getAndroidUpdateUrl(release.assets); | ||||||
|         } |         } | ||||||
|  |         if (Platform.isWindows) { | ||||||
|  |           windowsUpdateUrl = _getWindowsUpdateUrl(); | ||||||
|  |         } | ||||||
|         return _UpdateSheet( |         return _UpdateSheet( | ||||||
|           release: release, |           release: release, | ||||||
|           onOpen: () async { |           onOpen: () async { | ||||||
| @@ -192,6 +200,7 @@ class UpdateService { | |||||||
|             } |             } | ||||||
|           }, |           }, | ||||||
|           androidUpdateUrl: androidUpdateUrl, |           androidUpdateUrl: androidUpdateUrl, | ||||||
|  |           windowsUpdateUrl: windowsUpdateUrl, | ||||||
|           useProxy: useProxy, // Pass the useProxy flag |           useProxy: useProxy, // Pass the useProxy flag | ||||||
|         ); |         ); | ||||||
|       }, |       }, | ||||||
| @@ -211,15 +220,270 @@ class UpdateService { | |||||||
|  |  | ||||||
|     // Prioritize arm64, then armeabi, then x86_64 |     // Prioritize arm64, then armeabi, then x86_64 | ||||||
|     if (arm64 != null) { |     if (arm64 != null) { | ||||||
|       return arm64.browserDownloadUrl; |       return 'https://fs.solsynth.dev/d/official/solian/${arm64.name}'; | ||||||
|     } else if (armeabi != null) { |     } else if (armeabi != null) { | ||||||
|       return armeabi.browserDownloadUrl; |       return 'https://fs.solsynth.dev/d/official/solian/${armeabi.name}'; | ||||||
|     } else if (x86_64 != null) { |     } else if (x86_64 != null) { | ||||||
|       return x86_64.browserDownloadUrl; |       return 'https://fs.solsynth.dev/d/official/solian/${x86_64.name}'; | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   String _getWindowsUpdateUrl() { | ||||||
|  |     return 'https://fs.solsynth.dev/d/official/solian/build-output-windows-installer.zip'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// Downloads the Windows installer ZIP file | ||||||
|  |   Future<String?> _downloadWindowsInstaller(String url) async { | ||||||
|  |     try { | ||||||
|  |       log('[Update] Starting Windows installer download from: $url'); | ||||||
|  |  | ||||||
|  |       final tempDir = await getTemporaryDirectory(); | ||||||
|  |       final fileName = | ||||||
|  |           'solian-installer-${DateTime.now().millisecondsSinceEpoch}.zip'; | ||||||
|  |       final filePath = path.join(tempDir.path, fileName); | ||||||
|  |  | ||||||
|  |       final response = await _dio.download( | ||||||
|  |         url, | ||||||
|  |         filePath, | ||||||
|  |         onReceiveProgress: (received, total) { | ||||||
|  |           if (total != -1) { | ||||||
|  |             log( | ||||||
|  |               '[Update] Download progress: ${(received / total * 100).toStringAsFixed(1)}%', | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (response.statusCode == 200) { | ||||||
|  |         log('[Update] Windows installer downloaded successfully to: $filePath'); | ||||||
|  |         return filePath; | ||||||
|  |       } else { | ||||||
|  |         log( | ||||||
|  |           '[Update] Failed to download Windows installer. Status: ${response.statusCode}', | ||||||
|  |         ); | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       log('[Update] Error downloading Windows installer: $e'); | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// Extracts the ZIP file to a temporary directory | ||||||
|  |   Future<String?> _extractWindowsInstaller(String zipPath) async { | ||||||
|  |     try { | ||||||
|  |       log('[Update] Extracting Windows installer from: $zipPath'); | ||||||
|  |  | ||||||
|  |       final tempDir = await getTemporaryDirectory(); | ||||||
|  |       final extractDir = path.join( | ||||||
|  |         tempDir.path, | ||||||
|  |         'solian-installer-${DateTime.now().millisecondsSinceEpoch}', | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       final zipFile = File(zipPath); | ||||||
|  |       final bytes = await zipFile.readAsBytes(); | ||||||
|  |       final archive = ZipDecoder().decodeBytes(bytes); | ||||||
|  |  | ||||||
|  |       for (final file in archive) { | ||||||
|  |         final filename = file.name; | ||||||
|  |         if (file.isFile) { | ||||||
|  |           final data = file.content as List<int>; | ||||||
|  |           final filePath = path.join(extractDir, filename); | ||||||
|  |           await Directory(path.dirname(filePath)).create(recursive: true); | ||||||
|  |           await File(filePath).writeAsBytes(data); | ||||||
|  |         } else { | ||||||
|  |           final dirPath = path.join(extractDir, filename); | ||||||
|  |           await Directory(dirPath).create(recursive: true); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       log('[Update] Windows installer extracted successfully to: $extractDir'); | ||||||
|  |       return extractDir; | ||||||
|  |     } catch (e) { | ||||||
|  |       log('[Update] Error extracting Windows installer: $e'); | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// Runs the setup.exe file | ||||||
|  |   Future<bool> _runWindowsInstaller(String extractDir) async { | ||||||
|  |     try { | ||||||
|  |       log('[Update] Running Windows installer from: $extractDir'); | ||||||
|  |  | ||||||
|  |       final setupExePath = path.join(extractDir, 'setup.exe'); | ||||||
|  |  | ||||||
|  |       if (!await File(setupExePath).exists()) { | ||||||
|  |         log('[Update] setup.exe not found in extracted directory'); | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       final shell = Shell(); | ||||||
|  |       final results = await shell.run(setupExePath); | ||||||
|  |       final result = results.first; | ||||||
|  |  | ||||||
|  |       if (result.exitCode == 0) { | ||||||
|  |         log('[Update] Windows installer completed successfully'); | ||||||
|  |         return true; | ||||||
|  |       } else { | ||||||
|  |         log( | ||||||
|  |           '[Update] Windows installer failed with exit code: ${result.exitCode}', | ||||||
|  |         ); | ||||||
|  |         log('[Update] Installer output: ${result.stdout}'); | ||||||
|  |         log('[Update] Installer errors: ${result.stderr}'); | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       log('[Update] Error running Windows installer: $e'); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// Performs automatic Windows update: download, extract, and install | ||||||
|  |   Future<void> _performAutomaticWindowsUpdate( | ||||||
|  |     BuildContext context, | ||||||
|  |     String url, | ||||||
|  |   ) async { | ||||||
|  |     if (!context.mounted) return; | ||||||
|  |  | ||||||
|  |     // Show progress dialog | ||||||
|  |     showDialog( | ||||||
|  |       context: context, | ||||||
|  |       barrierDismissible: false, | ||||||
|  |       builder: | ||||||
|  |           (context) => const AlertDialog( | ||||||
|  |             title: Text('Installing Update'), | ||||||
|  |             content: Column( | ||||||
|  |               mainAxisSize: MainAxisSize.min, | ||||||
|  |               children: [ | ||||||
|  |                 CircularProgressIndicator(), | ||||||
|  |                 SizedBox(height: 16), | ||||||
|  |                 Text('Downloading installer...'), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       // Step 1: Download | ||||||
|  |       if (!context.mounted) return; | ||||||
|  |       Navigator.of(context).pop(); // Close progress dialog | ||||||
|  |       showDialog( | ||||||
|  |         context: context, | ||||||
|  |         barrierDismissible: false, | ||||||
|  |         builder: | ||||||
|  |             (context) => const AlertDialog( | ||||||
|  |               title: Text('Installing Update'), | ||||||
|  |               content: Column( | ||||||
|  |                 mainAxisSize: MainAxisSize.min, | ||||||
|  |                 children: [ | ||||||
|  |                   CircularProgressIndicator(), | ||||||
|  |                   SizedBox(height: 16), | ||||||
|  |                   Text('Extracting installer...'), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       final zipPath = await _downloadWindowsInstaller(url); | ||||||
|  |       if (zipPath == null) { | ||||||
|  |         if (!context.mounted) return; | ||||||
|  |         Navigator.of(context).pop(); | ||||||
|  |         _showErrorDialog(context, 'Failed to download installer'); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Step 2: Extract | ||||||
|  |       if (!context.mounted) return; | ||||||
|  |       Navigator.of(context).pop(); // Close progress dialog | ||||||
|  |       showDialog( | ||||||
|  |         context: context, | ||||||
|  |         barrierDismissible: false, | ||||||
|  |         builder: | ||||||
|  |             (context) => const AlertDialog( | ||||||
|  |               title: Text('Installing Update'), | ||||||
|  |               content: Column( | ||||||
|  |                 mainAxisSize: MainAxisSize.min, | ||||||
|  |                 children: [ | ||||||
|  |                   CircularProgressIndicator(), | ||||||
|  |                   SizedBox(height: 16), | ||||||
|  |                   Text('Running installer...'), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       final extractDir = await _extractWindowsInstaller(zipPath); | ||||||
|  |       if (extractDir == null) { | ||||||
|  |         if (!context.mounted) return; | ||||||
|  |         Navigator.of(context).pop(); | ||||||
|  |         _showErrorDialog(context, 'Failed to extract installer'); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Step 3: Run installer | ||||||
|  |       if (!context.mounted) return; | ||||||
|  |       Navigator.of(context).pop(); // Close progress dialog | ||||||
|  |  | ||||||
|  |       final success = await _runWindowsInstaller(extractDir); | ||||||
|  |       if (!context.mounted) return; | ||||||
|  |  | ||||||
|  |       if (success) { | ||||||
|  |         showDialog( | ||||||
|  |           context: context, | ||||||
|  |           builder: | ||||||
|  |               (context) => AlertDialog( | ||||||
|  |                 title: const Text('Update Complete'), | ||||||
|  |                 content: const Text( | ||||||
|  |                   'The application has been updated successfully. Please restart the application.', | ||||||
|  |                 ), | ||||||
|  |                 actions: [ | ||||||
|  |                   TextButton( | ||||||
|  |                     onPressed: () { | ||||||
|  |                       Navigator.of(context).pop(); | ||||||
|  |                       // Close the update sheet | ||||||
|  |                       Navigator.of(context).pop(); | ||||||
|  |                     }, | ||||||
|  |                     child: const Text('OK'), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         _showErrorDialog(context, 'Failed to run installer'); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Cleanup | ||||||
|  |       try { | ||||||
|  |         await File(zipPath).delete(); | ||||||
|  |         await Directory(extractDir).delete(recursive: true); | ||||||
|  |       } catch (e) { | ||||||
|  |         log('[Update] Error cleaning up temporary files: $e'); | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       if (!context.mounted) return; | ||||||
|  |       Navigator.of(context).pop(); // Close any open dialogs | ||||||
|  |       _showErrorDialog(context, 'Update failed: $e'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _showErrorDialog(BuildContext context, String message) { | ||||||
|  |     showDialog( | ||||||
|  |       context: context, | ||||||
|  |       builder: | ||||||
|  |           (context) => AlertDialog( | ||||||
|  |             title: const Text('Update Failed'), | ||||||
|  |             content: Text(message), | ||||||
|  |             actions: [ | ||||||
|  |               TextButton( | ||||||
|  |                 onPressed: () => Navigator.of(context).pop(), | ||||||
|  |                 child: const Text('OK'), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /// Fetch the latest release info from GitHub. |   /// Fetch the latest release info from GitHub. | ||||||
|   /// Public so other screens (e.g., About) can manually trigger update checks. |   /// Public so other screens (e.g., About) can manually trigger update checks. | ||||||
|   Future<GithubReleaseInfo?> fetchLatestRelease() async { |   Future<GithubReleaseInfo?> fetchLatestRelease() async { | ||||||
| @@ -277,10 +541,12 @@ class _UpdateSheet extends StatefulWidget { | |||||||
|     required this.release, |     required this.release, | ||||||
|     required this.onOpen, |     required this.onOpen, | ||||||
|     this.androidUpdateUrl, |     this.androidUpdateUrl, | ||||||
|  |     this.windowsUpdateUrl, | ||||||
|     this.useProxy = false, |     this.useProxy = false, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   final String? androidUpdateUrl; |   final String? androidUpdateUrl; | ||||||
|  |   final String? windowsUpdateUrl; | ||||||
|   final bool useProxy; |   final bool useProxy; | ||||||
|   final GithubReleaseInfo release; |   final GithubReleaseInfo release; | ||||||
|   final VoidCallback onOpen; |   final VoidCallback onOpen; | ||||||
| @@ -299,8 +565,11 @@ class _UpdateSheetState extends State<_UpdateSheet> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> _installUpdate(String url) async { |   Future<void> _installUpdate(String url) async { | ||||||
|     final downloadUrl = |     String downloadUrl = url; | ||||||
|         _useProxy ? 'https://ghfast.top/${Uri.encodeComponent(url)}' : url; |     if (_useProxy) { | ||||||
|  |       final fileName = url.split('/').last; | ||||||
|  |       downloadUrl = 'https://fs.solsynth.dev/d/rainyun02/solian/$fileName'; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     UpdateModel model = UpdateModel( |     UpdateModel model = UpdateModel( | ||||||
|       downloadUrl, |       downloadUrl, | ||||||
| @@ -350,7 +619,7 @@ class _UpdateSheetState extends State<_UpdateSheet> { | |||||||
|             ), |             ), | ||||||
|             if (!kIsWeb && Platform.isAndroid) |             if (!kIsWeb && Platform.isAndroid) | ||||||
|               SwitchListTile( |               SwitchListTile( | ||||||
|                 title: const Text('Use GitHub Proxy for Download'), |                 title: const Text('Use secondary source for download'), | ||||||
|                 value: _useProxy, |                 value: _useProxy, | ||||||
|                 onChanged: (value) { |                 onChanged: (value) { | ||||||
|                   setState(() { |                   setState(() { | ||||||
| @@ -376,6 +645,25 @@ class _UpdateSheetState extends State<_UpdateSheet> { | |||||||
|                           label: const Text('Install update'), |                           label: const Text('Install update'), | ||||||
|                         ), |                         ), | ||||||
|                       ), |                       ), | ||||||
|  |                     if (!kIsWeb && | ||||||
|  |                         Platform.isWindows && | ||||||
|  |                         widget.windowsUpdateUrl != null) | ||||||
|  |                       Expanded( | ||||||
|  |                         child: FilledButton.icon( | ||||||
|  |                           onPressed: () { | ||||||
|  |                             // Access the UpdateService instance to call the automatic update method | ||||||
|  |                             final updateService = UpdateService( | ||||||
|  |                               useProxy: widget.useProxy, | ||||||
|  |                             ); | ||||||
|  |                             updateService._performAutomaticWindowsUpdate( | ||||||
|  |                               context, | ||||||
|  |                               widget.windowsUpdateUrl!, | ||||||
|  |                             ); | ||||||
|  |                           }, | ||||||
|  |                           icon: const Icon(Symbols.update), | ||||||
|  |                           label: const Text('Install update'), | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|                     Expanded( |                     Expanded( | ||||||
|                       child: FilledButton.icon( |                       child: FilledButton.icon( | ||||||
|                         onPressed: widget.onOpen, |                         onPressed: widget.onOpen, | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								lib/utils/share_utils.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								lib/utils/share_utils.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | import 'dart:io'; | ||||||
|  |  | ||||||
|  | import 'package:flutter/foundation.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/models/post.dart'; | ||||||
|  | import 'package:island/pods/config.dart'; | ||||||
|  | import 'package:island/widgets/alert.dart'; | ||||||
|  | import 'package:island/widgets/post/post_item_screenshot.dart'; | ||||||
|  | import 'package:path_provider/path_provider.dart' show getTemporaryDirectory; | ||||||
|  | import 'package:screenshot/screenshot.dart'; | ||||||
|  | import 'package:share_plus/share_plus.dart'; | ||||||
|  |  | ||||||
|  | /// Shares a post as a screenshot image | ||||||
|  | Future<void> sharePostAsScreenshot( | ||||||
|  |   BuildContext context, | ||||||
|  |   WidgetRef ref, | ||||||
|  |   SnPost post, | ||||||
|  | ) async { | ||||||
|  |   if (kIsWeb) return; | ||||||
|  |  | ||||||
|  |   final screenshotController = ScreenshotController(); | ||||||
|  |  | ||||||
|  |   showLoadingModal(context); | ||||||
|  |   await screenshotController | ||||||
|  |       .captureFromWidget( | ||||||
|  |         ProviderScope( | ||||||
|  |           overrides: [ | ||||||
|  |             sharedPreferencesProvider.overrideWithValue( | ||||||
|  |               ref.watch(sharedPreferencesProvider), | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |           child: Directionality( | ||||||
|  |             textDirection: TextDirection.ltr, | ||||||
|  |             child: SizedBox( | ||||||
|  |               width: 520, | ||||||
|  |               child: PostItemScreenshot(item: post, isFullPost: true), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         context: context, | ||||||
|  |         pixelRatio: MediaQuery.of(context).devicePixelRatio, | ||||||
|  |         delay: const Duration(seconds: 1), | ||||||
|  |       ) | ||||||
|  |       .then((Uint8List? image) async { | ||||||
|  |         if (image == null) return; | ||||||
|  |         final directory = await getTemporaryDirectory(); | ||||||
|  |         final imagePath = await File('${directory.path}/image.png').create(); | ||||||
|  |         await imagePath.writeAsBytes(image); | ||||||
|  |  | ||||||
|  |         if (!context.mounted) return; | ||||||
|  |         hideLoadingModal(context); | ||||||
|  |         final box = context.findRenderObject() as RenderBox?; | ||||||
|  |         await Share.shareXFiles([ | ||||||
|  |           XFile(imagePath.path), | ||||||
|  |         ], sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); | ||||||
|  |       }) | ||||||
|  |       .catchError((err) { | ||||||
|  |         if (context.mounted) hideLoadingModal(context); | ||||||
|  |         showErrorAlert(err); | ||||||
|  |       }); | ||||||
|  | } | ||||||
| @@ -46,6 +46,10 @@ class EventDetailsWidget extends StatelessWidget { | |||||||
|                       size: 12, |                       size: 12, | ||||||
|                       fill: 1, |                       fill: 1, | ||||||
|                     ).padding(top: 4, right: 4), |                     ).padding(top: 4, right: 4), | ||||||
|  |                     Icon( | ||||||
|  |                       tip.isPositive ? Symbols.thumb_up : Symbols.thumb_down, | ||||||
|  |                       size: 14, | ||||||
|  |                     ).padding(top: 2.5), | ||||||
|                     Expanded( |                     Expanded( | ||||||
|                       child: Column( |                       child: Column( | ||||||
|                         crossAxisAlignment: CrossAxisAlignment.start, |                         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  | import 'dart:ui'; | ||||||
| import 'package:bitsdojo_window/bitsdojo_window.dart'; | import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| @@ -15,6 +16,15 @@ import 'package:material_symbols_icons/material_symbols_icons.dart'; | |||||||
| import 'package:path_provider/path_provider.dart'; | import 'package:path_provider/path_provider.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
|  | class AppScrollBehavior extends MaterialScrollBehavior { | ||||||
|  |   @override | ||||||
|  |   Set<PointerDeviceKind> get dragDevices => { | ||||||
|  |     PointerDeviceKind.touch, // default | ||||||
|  |     PointerDeviceKind.trackpad, // default | ||||||
|  |     PointerDeviceKind.mouse, // add mouse dragging | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
| class WindowScaffold extends HookConsumerWidget { | class WindowScaffold extends HookConsumerWidget { | ||||||
|   final Widget child; |   final Widget child; | ||||||
|   const WindowScaffold({super.key, required this.child}); |   const WindowScaffold({super.key, required this.child}); | ||||||
|   | |||||||
| @@ -63,8 +63,12 @@ class AppWrapper extends HookConsumerWidget with TrayListener { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _trayIconPrimaryAction() { |   void _trayIconPrimaryAction() { | ||||||
|  |     if (appWindow.isVisible) { | ||||||
|  |       appWindow.restore(); | ||||||
|  |     } else { | ||||||
|       appWindow.show(); |       appWindow.show(); | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   void _trayIconSecondaryAction() { |   void _trayIconSecondaryAction() { | ||||||
|     trayManager.popUpContextMenu(); |     trayManager.popUpContextMenu(); | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import 'dart:convert'; | |||||||
| import 'package:dio/dio.dart'; | import 'package:dio/dio.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| @@ -14,6 +15,7 @@ import 'package:island/widgets/alert.dart'; | |||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
|  | import 'package:slide_countdown/slide_countdown.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
| part 'check_in.g.dart'; | part 'check_in.g.dart'; | ||||||
| @@ -34,6 +36,17 @@ Future<SnCheckInResult?> checkInResultToday(Ref ref) async { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | Future<SnNotableDay?> nextNotableDay(Ref ref) async { | ||||||
|  |   final client = ref.watch(apiClientProvider); | ||||||
|  |   try { | ||||||
|  |     final resp = await client.get('/id/notable/me/next'); | ||||||
|  |     return SnNotableDay.fromJson(resp.data); | ||||||
|  |   } catch (err) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| class CheckInWidget extends HookConsumerWidget { | class CheckInWidget extends HookConsumerWidget { | ||||||
|   final EdgeInsets? margin; |   final EdgeInsets? margin; | ||||||
|   final VoidCallback? onChecked; |   final VoidCallback? onChecked; | ||||||
| @@ -42,6 +55,22 @@ class CheckInWidget extends HookConsumerWidget { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final todayResult = ref.watch(checkInResultTodayProvider); |     final todayResult = ref.watch(checkInResultTodayProvider); | ||||||
|  |     final nextNotableDay = ref.watch(nextNotableDayProvider); | ||||||
|  |  | ||||||
|  |     final userinfo = ref.watch(userInfoProvider); | ||||||
|  |     final isAdult = useMemoized(() { | ||||||
|  |       final birthday = userinfo.value?.profile.birthday; | ||||||
|  |       if (birthday == null) return false; | ||||||
|  |       final now = DateTime.now(); | ||||||
|  |       final age = | ||||||
|  |           now.year - | ||||||
|  |           birthday.year - | ||||||
|  |           ((now.month < birthday.month || | ||||||
|  |                   (now.month == birthday.month && now.day < birthday.day)) | ||||||
|  |               ? 1 | ||||||
|  |               : 0); | ||||||
|  |       return age >= 18; | ||||||
|  |     }, [userinfo]); | ||||||
|  |  | ||||||
|     Future<void> checkIn({String? captchatTk}) async { |     Future<void> checkIn({String? captchatTk}) async { | ||||||
|       final client = ref.read(apiClientProvider); |       final client = ref.read(apiClientProvider); | ||||||
| @@ -71,21 +100,26 @@ class CheckInWidget extends HookConsumerWidget { | |||||||
|     return Card( |     return Card( | ||||||
|       margin: |       margin: | ||||||
|           margin ?? EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8), |           margin ?? EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8), | ||||||
|       child: Row( |       child: Column( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.center, |         crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|         spacing: 16, |         spacing: 8, | ||||||
|         children: [ |         children: [ | ||||||
|           ClipRRect( |  | ||||||
|             borderRadius: BorderRadius.circular(8), |  | ||||||
|             child: Container( |  | ||||||
|               color: Theme.of(context).colorScheme.secondaryContainer, |  | ||||||
|               width: 56, |  | ||||||
|               height: 56, |  | ||||||
|               child: |  | ||||||
|           Column( |           Column( | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |             children: [ | ||||||
|  |               Row( | ||||||
|  |                 spacing: 8, | ||||||
|                 mainAxisSize: MainAxisSize.min, |                 mainAxisSize: MainAxisSize.min, | ||||||
|                 crossAxisAlignment: CrossAxisAlignment.center, |                 crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|                 children: [ |                 children: [ | ||||||
|  |                   Icon( | ||||||
|  |                     switch (DateTime.now().weekday) { | ||||||
|  |                       6 || 7 => Symbols.weekend, | ||||||
|  |                       _ => isAdult ? Symbols.work : Symbols.school, | ||||||
|  |                     }, | ||||||
|  |                     fill: 1, | ||||||
|  |                     size: 16, | ||||||
|  |                   ).padding(right: 2), | ||||||
|                   Text(DateFormat('EEE').format(DateTime.now())) |                   Text(DateFormat('EEE').format(DateTime.now())) | ||||||
|                       .fontSize(16) |                       .fontSize(16) | ||||||
|                       .bold() |                       .bold() | ||||||
| @@ -96,11 +130,32 @@ class CheckInWidget extends HookConsumerWidget { | |||||||
|                       .fontSize(12) |                       .fontSize(12) | ||||||
|                       .textColor( |                       .textColor( | ||||||
|                         Theme.of(context).colorScheme.onSecondaryContainer, |                         Theme.of(context).colorScheme.onSecondaryContainer, | ||||||
|  |                       ) | ||||||
|  |                       .padding(top: 2), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |               Row( | ||||||
|  |                 spacing: 5, | ||||||
|  |                 children: [ | ||||||
|  |                   Text('notableDayNext') | ||||||
|  |                       .tr(args: [nextNotableDay.value?.localName ?? 'idk']) | ||||||
|  |                       .fontSize(12), | ||||||
|  |                   SlideCountdown( | ||||||
|  |                     decoration: const BoxDecoration(), | ||||||
|  |                     style: const TextStyle(fontSize: 12), | ||||||
|  |                     separatorStyle: const TextStyle(fontSize: 12), | ||||||
|  |                     padding: EdgeInsets.zero, | ||||||
|  |                     duration: nextNotableDay.value?.date.difference( | ||||||
|  |                       DateTime.now(), | ||||||
|  |                     ), | ||||||
|                   ), |                   ), | ||||||
|                 ], |                 ], | ||||||
|                   ).center(), |  | ||||||
|             ), |  | ||||||
|               ), |               ), | ||||||
|  |             ], | ||||||
|  |           ).padding(horizontal: 16, top: 8), | ||||||
|  |           const Divider(height: 1), | ||||||
|  |           Row( | ||||||
|  |             children: [ | ||||||
|               Expanded( |               Expanded( | ||||||
|                 child: AnimatedSwitcher( |                 child: AnimatedSwitcher( | ||||||
|                   duration: const Duration(milliseconds: 300), |                   duration: const Duration(milliseconds: 300), | ||||||
| @@ -113,13 +168,34 @@ class CheckInWidget extends HookConsumerWidget { | |||||||
|                           Text( |                           Text( | ||||||
|                             'checkInResultLevel${result.level}', |                             'checkInResultLevel${result.level}', | ||||||
|                           ).tr().fontSize(15).bold(), |                           ).tr().fontSize(15).bold(), | ||||||
|                       Text( |                           Wrap( | ||||||
|  |                             children: | ||||||
|                                 result.tips |                                 result.tips | ||||||
|                             .map( |                                     .map((e) { | ||||||
|                               (e) => '${e.isPositive ? '宜' : '忌'} ${e.title}', |                                       return Row( | ||||||
|  |                                         mainAxisSize: MainAxisSize.min, | ||||||
|  |                                         children: [ | ||||||
|  |                                           Icon( | ||||||
|  |                                             e.isPositive | ||||||
|  |                                                 ? Symbols.thumb_up | ||||||
|  |                                                 : Symbols.thumb_down, | ||||||
|  |                                             size: 12, | ||||||
|  |                                           ), | ||||||
|  |                                           const Gap(4), | ||||||
|  |                                           Text(e.title).fontSize(11), | ||||||
|  |                                         ], | ||||||
|  |                                       ); | ||||||
|  |                                     }) | ||||||
|  |                                     .toList() | ||||||
|  |                                     .expand( | ||||||
|  |                                       (widget) => [ | ||||||
|  |                                         widget, | ||||||
|  |                                         Text('  ·  ').fontSize(11), | ||||||
|  |                                       ], | ||||||
|                                     ) |                                     ) | ||||||
|                             .join('  ·  '), |                                     .toList() | ||||||
|                       ).fontSize(11), |                                   ..removeLast(), | ||||||
|  |                           ), | ||||||
|                         ], |                         ], | ||||||
|                       ); |                       ); | ||||||
|                     }, |                     }, | ||||||
| @@ -162,7 +238,9 @@ class CheckInWidget extends HookConsumerWidget { | |||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|             ], |             ], | ||||||
|       ).padding(horizontal: 16, vertical: 12), |           ).padding(horizontal: 16, bottom: 12, top: 4), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -26,5 +26,24 @@ final checkInResultTodayProvider = | |||||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||||
| // ignore: unused_element | // ignore: unused_element | ||||||
| typedef CheckInResultTodayRef = AutoDisposeFutureProviderRef<SnCheckInResult?>; | typedef CheckInResultTodayRef = AutoDisposeFutureProviderRef<SnCheckInResult?>; | ||||||
|  | String _$nextNotableDayHash() => r'698370bec4be28774d332412c5a701f914064c90'; | ||||||
|  |  | ||||||
|  | /// See also [nextNotableDay]. | ||||||
|  | @ProviderFor(nextNotableDay) | ||||||
|  | final nextNotableDayProvider = | ||||||
|  |     AutoDisposeFutureProvider<SnNotableDay?>.internal( | ||||||
|  |       nextNotableDay, | ||||||
|  |       name: r'nextNotableDayProvider', | ||||||
|  |       debugGetCreateSourceHash: | ||||||
|  |           const bool.fromEnvironment('dart.vm.product') | ||||||
|  |               ? null | ||||||
|  |               : _$nextNotableDayHash, | ||||||
|  |       dependencies: null, | ||||||
|  |       allTransitiveDependencies: null, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  | @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||||
|  | // ignore: unused_element | ||||||
|  | typedef NextNotableDayRef = AutoDisposeFutureProviderRef<SnNotableDay?>; | ||||||
| // ignore_for_file: type=lint | // ignore_for_file: type=lint | ||||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||||
|   | |||||||
| @@ -321,7 +321,7 @@ class AttachmentPreview extends HookConsumerWidget { | |||||||
|                               children: [ |                               children: [ | ||||||
|                                 Icon(fallbackIcon), |                                 Icon(fallbackIcon), | ||||||
|                                 const Gap(6), |                                 const Gap(6), | ||||||
|                                 Text(file.name), |                                 Text(file.name, textAlign: TextAlign.center), | ||||||
|                                 FutureBuilder( |                                 FutureBuilder( | ||||||
|                                   future: file.length(), |                                   future: file.length(), | ||||||
|                                   builder: (context, snapshot) { |                                   builder: (context, snapshot) { | ||||||
|   | |||||||
| @@ -5,11 +5,11 @@ import 'dart:ui'; | |||||||
|  |  | ||||||
| import 'package:dismissible_page/dismissible_page.dart'; | import 'package:dismissible_page/dismissible_page.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter_blurhash/flutter_blurhash.dart'; | ||||||
| import 'package:file_saver/file_saver.dart'; | import 'package:file_saver/file_saver.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/services.dart'; | import 'package:flutter/services.dart'; | ||||||
| import 'package:flutter_blurhash/flutter_blurhash.dart'; |  | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:gal/gal.dart'; | import 'package:gal/gal.dart'; | ||||||
| @@ -804,155 +804,86 @@ class _CloudFileListEntry extends HookConsumerWidget { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final dataSaving = ref.watch( | ||||||
|  |       appSettingsNotifierProvider.select((s) => s.dataSavingMode), | ||||||
|  |     ); | ||||||
|     final showMature = useState(false); |     final showMature = useState(false); | ||||||
|  |     final showDataSaving = useState(!dataSaving); | ||||||
|  |     final lockedByDS = dataSaving && !showDataSaving.value; | ||||||
|  |     final lockedByMature = file.sensitiveMarks.isNotEmpty && !showMature.value; | ||||||
|  |     final meta = file.fileMeta is Map ? file.fileMeta as Map : const {}; | ||||||
|  |     final hasRatio = | ||||||
|  |         meta.containsKey('ratio') && | ||||||
|  |         (meta['ratio'] is num && (meta['ratio'] as num) != 0); | ||||||
|  |     final ratio = | ||||||
|  |         (meta['ratio'] is num && (meta['ratio'] as num) != 0) | ||||||
|  |             ? (meta['ratio'] as num).toDouble() | ||||||
|  |             : 1.0; | ||||||
|  |  | ||||||
|     var content = Stack( |     final fit = hasRatio ? BoxFit.cover : BoxFit.contain; | ||||||
|       fit: StackFit.expand, |  | ||||||
|       children: [ |     Widget bg = const SizedBox.shrink(); | ||||||
|         if (isImage) |     if (isImage) { | ||||||
|           Positioned.fill( |       if (meta['blur'] is String) { | ||||||
|             child: |         bg = BlurHash(hash: meta['blur'] as String); | ||||||
|                 file.fileMeta?['blur'] is String |       } else if (!lockedByDS && !lockedByMature) { | ||||||
|                     ? BlurHash(hash: file.fileMeta?['blur']) |         bg = ImageFiltered( | ||||||
|                     : ImageFiltered( |  | ||||||
|           imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), |           imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), | ||||||
|                       child: CloudFileWidget(item: file, noBlurhash: true), |           child: CloudFileWidget( | ||||||
|  |             fit: fit, | ||||||
|  |             item: file, | ||||||
|  |             noBlurhash: true, | ||||||
|  |             useInternalGate: false, | ||||||
|           ), |           ), | ||||||
|           ), |         ); | ||||||
|         if (isImage) |       } else { | ||||||
|           CloudFileWidget( |         bg = const ColoredBox(color: Colors.black26); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final bool fullyUnlocked = !lockedByDS && !lockedByMature; | ||||||
|  |     Widget fg = | ||||||
|  |         fullyUnlocked | ||||||
|  |             ? (isImage | ||||||
|  |                 ? CloudFileWidget( | ||||||
|                   item: file, |                   item: file, | ||||||
|                   heroTag: heroTag, |                   heroTag: heroTag, | ||||||
|                   noBlurhash: true, |                   noBlurhash: true, | ||||||
|             fit: BoxFit.contain, |                   fit: fit, | ||||||
|  |                   useInternalGate: false, | ||||||
|                 ) |                 ) | ||||||
|         else |                 : CloudFileWidget( | ||||||
|           CloudFileWidget(item: file, heroTag: heroTag, fit: BoxFit.contain), |                   item: file, | ||||||
|       ], |                   heroTag: heroTag, | ||||||
|     ); |                   fit: fit, | ||||||
|  |                   useInternalGate: false, | ||||||
|  |                 )) | ||||||
|  |             : AspectRatio(aspectRatio: ratio, child: const SizedBox.shrink()); | ||||||
|  |  | ||||||
|     if (file.sensitiveMarks.isNotEmpty) { |     Widget overlays; | ||||||
|       // Show a blurred overlay only when not revealed yet, with a smooth transition |     if (lockedByDS) { | ||||||
|       content = Stack( |       overlays = _DataSavingOverlay(); | ||||||
|         children: [ |     } else if (file.sensitiveMarks.isNotEmpty) { | ||||||
|           content, |       overlays = _SensitiveOverlay( | ||||||
|           // Toggle blur overlay with animation |         file: file, | ||||||
|           Positioned.fill( |         isRevealed: showMature.value, | ||||||
|             child: AnimatedSwitcher( |         onHide: () => showMature.value = false, | ||||||
|               duration: const Duration(milliseconds: 250), |  | ||||||
|               switchInCurve: Curves.easeOut, |  | ||||||
|               switchOutCurve: Curves.easeIn, |  | ||||||
|               layoutBuilder: |  | ||||||
|                   (currentChild, previousChildren) => Stack( |  | ||||||
|                     fit: StackFit.expand, |  | ||||||
|                     children: [ |  | ||||||
|                       ...previousChildren, |  | ||||||
|                       if (currentChild != null) currentChild, |  | ||||||
|                     ], |  | ||||||
|                   ), |  | ||||||
|               child: |  | ||||||
|                   showMature.value |  | ||||||
|                       ? const SizedBox.shrink(key: ValueKey('revealed')) |  | ||||||
|                       : ColoredBox( |  | ||||||
|                         key: const ValueKey('blurred'), |  | ||||||
|                         color: Colors.transparent, |  | ||||||
|                         child: BackdropFilter( |  | ||||||
|                           filter: ImageFilter.blur(sigmaX: 64, sigmaY: 64), |  | ||||||
|                           child: Stack( |  | ||||||
|                             fit: StackFit.expand, |  | ||||||
|                             children: [ |  | ||||||
|                               const ColoredBox(color: Colors.transparent), |  | ||||||
|                               Center( |  | ||||||
|                                 child: Container( |  | ||||||
|                                   margin: const EdgeInsets.all(12), |  | ||||||
|                                   padding: const EdgeInsets.symmetric( |  | ||||||
|                                     horizontal: 12, |  | ||||||
|                                     vertical: 8, |  | ||||||
|                                   ), |  | ||||||
|                                   decoration: BoxDecoration( |  | ||||||
|                                     color: Colors.black54, |  | ||||||
|                                     borderRadius: BorderRadius.circular(12), |  | ||||||
|                                   ), |  | ||||||
|                                   child: ConstrainedBox( |  | ||||||
|                                     constraints: const BoxConstraints( |  | ||||||
|                                       maxWidth: 280, |  | ||||||
|                                     ), |  | ||||||
|                                     child: Column( |  | ||||||
|                                       mainAxisSize: MainAxisSize.min, |  | ||||||
|                                       children: [ |  | ||||||
|                                         const Icon( |  | ||||||
|                                           Icons.warning, |  | ||||||
|                                           color: Colors.white, |  | ||||||
|                                           fill: 1, |  | ||||||
|                                           size: 24, |  | ||||||
|                                         ), |  | ||||||
|                                         const Gap(4), |  | ||||||
|                                         Text( |  | ||||||
|                                           file.sensitiveMarks |  | ||||||
|                                               .map( |  | ||||||
|                                                 (e) => |  | ||||||
|                                                     SensitiveCategory |  | ||||||
|                                                         .values[e] |  | ||||||
|                                                         .i18nKey |  | ||||||
|                                                         .tr(), |  | ||||||
|                                               ) |  | ||||||
|                                               .join(' · '), |  | ||||||
|                                           style: const TextStyle( |  | ||||||
|                                             color: Colors.white, |  | ||||||
|                                             fontWeight: FontWeight.w600, |  | ||||||
|                                           ), |  | ||||||
|                                           textAlign: TextAlign.center, |  | ||||||
|                                         ), |  | ||||||
|                                         Text( |  | ||||||
|                                           'Sensitive Content', |  | ||||||
|                                           style: TextStyle( |  | ||||||
|                                             color: Colors.white, |  | ||||||
|                                             fontSize: 13, |  | ||||||
|                                           ), |  | ||||||
|                                         ), |  | ||||||
|                                         const Gap(4), |  | ||||||
|                                         Text( |  | ||||||
|                                           'Tap to Reveal', |  | ||||||
|                                           style: TextStyle( |  | ||||||
|                                             color: Colors.white, |  | ||||||
|                                             fontSize: 11, |  | ||||||
|                                           ), |  | ||||||
|                                         ), |  | ||||||
|                                       ], |  | ||||||
|                                     ), |  | ||||||
|                                   ).padding(horizontal: 24, vertical: 16), |  | ||||||
|                                 ), |  | ||||||
|                               ), |  | ||||||
|                             ], |  | ||||||
|                           ), |  | ||||||
|                         ), |  | ||||||
|                       ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           // When revealed (no blur), show a small control at top-left to re-enable blur |  | ||||||
|           if (showMature.value) |  | ||||||
|             Positioned( |  | ||||||
|               top: 3, |  | ||||||
|               left: 4, |  | ||||||
|               child: IconButton( |  | ||||||
|                 iconSize: 16, |  | ||||||
|                 constraints: const BoxConstraints(), |  | ||||||
|                 icon: const Icon(Icons.visibility_off, color: Colors.white), |  | ||||||
|                 tooltip: 'Blur content', |  | ||||||
|                 onPressed: () { |  | ||||||
|                   showMature.value = false; |  | ||||||
|                 }, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|         ], |  | ||||||
|       ); |       ); | ||||||
|  |     } else { | ||||||
|  |       overlays = const SizedBox.shrink(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (onTap != null) { |     final content = Stack( | ||||||
|  |       fit: StackFit.expand, | ||||||
|  |       children: [if (isImage) Positioned.fill(child: bg), fg, overlays], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return InkWell( |     return InkWell( | ||||||
|       borderRadius: const BorderRadius.all(Radius.circular(16)), |       borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||||
|       onTap: () { |       onTap: () { | ||||||
|           if (!showMature.value) { |         if (lockedByDS) { | ||||||
|  |           showDataSaving.value = true; | ||||||
|  |         } else if (lockedByMature) { | ||||||
|           showMature.value = true; |           showMature.value = true; | ||||||
|         } else { |         } else { | ||||||
|           onTap?.call(); |           onTap?.call(); | ||||||
| @@ -961,7 +892,125 @@ class _CloudFileListEntry extends HookConsumerWidget { | |||||||
|       child: content, |       child: content, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|     return content; | class _SensitiveOverlay extends StatelessWidget { | ||||||
|  |   final SnCloudFile file; | ||||||
|  |   final VoidCallback? onHide; | ||||||
|  |   final bool isRevealed; | ||||||
|  |  | ||||||
|  |   const _SensitiveOverlay({ | ||||||
|  |     required this.file, | ||||||
|  |     this.onHide, | ||||||
|  |     this.isRevealed = false, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     if (isRevealed) { | ||||||
|  |       return Positioned( | ||||||
|  |         top: 3, | ||||||
|  |         left: 4, | ||||||
|  |         child: IconButton( | ||||||
|  |           iconSize: 16, | ||||||
|  |           constraints: const BoxConstraints(), | ||||||
|  |           icon: const Icon( | ||||||
|  |             Icons.visibility_off, | ||||||
|  |             color: Colors.white, | ||||||
|  |             shadows: [ | ||||||
|  |               Shadow( | ||||||
|  |                 color: Colors.black, | ||||||
|  |                 blurRadius: 5.0, | ||||||
|  |                 offset: Offset(1.0, 1.0), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |           tooltip: 'Blur content', | ||||||
|  |           onPressed: onHide, | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return BackdropFilter( | ||||||
|  |       filter: ImageFilter.blur(sigmaX: 64, sigmaY: 64), | ||||||
|  |       child: Container( | ||||||
|  |         color: Colors.transparent, | ||||||
|  |         child: Center( | ||||||
|  |           child: _OverlayCard( | ||||||
|  |             icon: Icons.warning, | ||||||
|  |             title: file.sensitiveMarks | ||||||
|  |                 .map((e) => SensitiveCategory.values[e].i18nKey.tr()) | ||||||
|  |                 .join(' · '), | ||||||
|  |             subtitle: 'Sensitive Content', | ||||||
|  |             hint: 'Tap to Reveal', | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _DataSavingOverlay extends StatelessWidget { | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return ColoredBox( | ||||||
|  |       color: Colors.black38, | ||||||
|  |       child: Center( | ||||||
|  |         child: _OverlayCard( | ||||||
|  |           icon: Symbols.image, | ||||||
|  |           title: 'Data Saving Mode', | ||||||
|  |           subtitle: '', | ||||||
|  |           hint: 'Tap to Load', | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _OverlayCard extends StatelessWidget { | ||||||
|  |   final IconData icon; | ||||||
|  |   final String title; | ||||||
|  |   final String subtitle; | ||||||
|  |   final String hint; | ||||||
|  |  | ||||||
|  |   const _OverlayCard({ | ||||||
|  |     required this.icon, | ||||||
|  |     required this.title, | ||||||
|  |     required this.subtitle, | ||||||
|  |     required this.hint, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Container( | ||||||
|  |       margin: const EdgeInsets.all(12), | ||||||
|  |       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), | ||||||
|  |       decoration: BoxDecoration( | ||||||
|  |         color: Colors.black54, | ||||||
|  |         borderRadius: BorderRadius.circular(12), | ||||||
|  |       ), | ||||||
|  |       constraints: const BoxConstraints(maxWidth: 280), | ||||||
|  |       child: Column( | ||||||
|  |         mainAxisSize: MainAxisSize.min, | ||||||
|  |         children: [ | ||||||
|  |           Icon(icon, color: Colors.white, size: 24), | ||||||
|  |           const Gap(4), | ||||||
|  |           Text( | ||||||
|  |             title, | ||||||
|  |             style: const TextStyle( | ||||||
|  |               color: Colors.white, | ||||||
|  |               fontWeight: FontWeight.w600, | ||||||
|  |             ), | ||||||
|  |             textAlign: TextAlign.center, | ||||||
|  |           ), | ||||||
|  |           Text( | ||||||
|  |             subtitle, | ||||||
|  |             style: const TextStyle(color: Colors.white, fontSize: 13), | ||||||
|  |           ), | ||||||
|  |           const Gap(4), | ||||||
|  |           Text(hint, style: const TextStyle(color: Colors.white, fontSize: 11)), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ import 'package:island/widgets/content/audio.dart'; | |||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:url_launcher/url_launcher_string.dart'; | import 'package:url_launcher/url_launcher_string.dart'; | ||||||
|  | import 'package:island/widgets/data_saving_gate.dart'; | ||||||
|  |  | ||||||
| import 'image.dart'; | import 'image.dart'; | ||||||
| import 'video.dart'; | import 'video.dart'; | ||||||
| @@ -23,40 +24,51 @@ class CloudFileWidget extends HookConsumerWidget { | |||||||
|   final BoxFit fit; |   final BoxFit fit; | ||||||
|   final String? heroTag; |   final String? heroTag; | ||||||
|   final bool noBlurhash; |   final bool noBlurhash; | ||||||
|  |   final bool useInternalGate; | ||||||
|   const CloudFileWidget({ |   const CloudFileWidget({ | ||||||
|     super.key, |     super.key, | ||||||
|     required this.item, |     required this.item, | ||||||
|     this.fit = BoxFit.cover, |     this.fit = BoxFit.cover, | ||||||
|     this.heroTag, |     this.heroTag, | ||||||
|     this.noBlurhash = false, |     this.noBlurhash = false, | ||||||
|  |     this.useInternalGate = true, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final dataSaving = ref.watch( | ||||||
|  |       appSettingsNotifierProvider.select((s) => s.dataSavingMode), | ||||||
|  |     ); | ||||||
|     final serverUrl = ref.watch(serverUrlProvider); |     final serverUrl = ref.watch(serverUrlProvider); | ||||||
|     final uri = '$serverUrl/drive/files/${item.id}'; |     final uri = '$serverUrl/drive/files/${item.id}'; | ||||||
|  |  | ||||||
|     var ratio = |     final unlocked = useState(false); | ||||||
|         item.fileMeta?['ratio'] is num |  | ||||||
|             ? item.fileMeta!['ratio'].toDouble() |     final meta = item.fileMeta is Map ? (item.fileMeta as Map) : const {}; | ||||||
|             : 1.0; |     final blurHash = noBlurhash ? null : (meta['blur'] as String?); | ||||||
|  |     var ratio = meta['ratio'] is num ? (meta['ratio'] as num).toDouble() : 1.0; | ||||||
|     if (ratio == 0) ratio = 1.0; |     if (ratio == 0) ratio = 1.0; | ||||||
|  |  | ||||||
|  |     Widget cloudImage() => UniversalImage(uri: uri, blurHash: blurHash, fit: fit); | ||||||
|  |     Widget cloudVideo() => CloudVideoWidget(item: item); | ||||||
|  |  | ||||||
|  |     Widget dataPlaceHolder(IconData icon) => _DataSavingPlaceholder( | ||||||
|  |           icon: icon, | ||||||
|  |           onTap: () { | ||||||
|  |             unlocked.value = true; | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|     var content = switch (item.mimeType?.split('/').firstOrNull) { |     var content = switch (item.mimeType?.split('/').firstOrNull) { | ||||||
|       "image" => AspectRatio( |       'image' => AspectRatio( | ||||||
|           aspectRatio: ratio, |           aspectRatio: ratio, | ||||||
|         child: UniversalImage( |           child: (useInternalGate && dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.image) : cloudImage(), | ||||||
|           uri: uri, |  | ||||||
|           blurHash: |  | ||||||
|               noBlurhash |  | ||||||
|                   ? null |  | ||||||
|                   : (item.fileMeta is String ? item.fileMeta!['blur'] : null), |  | ||||||
|         ), |         ), | ||||||
|       ), |       'video' => AspectRatio( | ||||||
|       "video" => AspectRatio( |  | ||||||
|           aspectRatio: ratio, |           aspectRatio: ratio, | ||||||
|         child: CloudVideoWidget(item: item), |           child: (useInternalGate && dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.play_arrow) : cloudVideo(), | ||||||
|         ), |         ), | ||||||
|       "audio" => Center( |       'audio' => Center( | ||||||
|           child: ConstrainedBox( |           child: ConstrainedBox( | ||||||
|             constraints: BoxConstraints( |             constraints: BoxConstraints( | ||||||
|               maxWidth: math.min(360, MediaQuery.of(context).size.width * 0.8), |               maxWidth: math.min(360, MediaQuery.of(context).size.width * 0.8), | ||||||
| @@ -113,6 +125,35 @@ class CloudFileWidget extends HookConsumerWidget { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class _DataSavingPlaceholder extends StatelessWidget { | ||||||
|  |   final IconData icon; | ||||||
|  |   final VoidCallback onTap; | ||||||
|  |   const _DataSavingPlaceholder({required this.icon, required this.onTap}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return GestureDetector( | ||||||
|  |       onTap: onTap, | ||||||
|  |       child: Container( | ||||||
|  |         color: Colors.black26, | ||||||
|  |         alignment: Alignment.center, | ||||||
|  |         child: Column( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|  |           children: [ | ||||||
|  |             Icon(icon, size: 36, | ||||||
|  |               color: Theme.of(context).colorScheme.onSurfaceVariant), | ||||||
|  |             const Gap(8), | ||||||
|  |             Text( | ||||||
|  |               'dataSavingHint'.tr(), | ||||||
|  |               style: Theme.of(context).textTheme.bodySmall, | ||||||
|  |               textAlign: TextAlign.center, | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| class CloudVideoWidget extends HookConsumerWidget { | class CloudVideoWidget extends HookConsumerWidget { | ||||||
|   final SnCloudFile item; |   final SnCloudFile item; | ||||||
|   const CloudVideoWidget({super.key, required this.item}); |   const CloudVideoWidget({super.key, required this.item}); | ||||||
| @@ -314,29 +355,32 @@ class ProfilePictureWidget extends ConsumerWidget { | |||||||
| @override | @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final serverUrl = ref.watch(serverUrlProvider); |     final serverUrl = ref.watch(serverUrlProvider); | ||||||
|     final uri = '$serverUrl/drive/files/${file?.id ?? fileId}'; |     final String? id = file?.id ?? fileId; | ||||||
|  |  | ||||||
|  |     final fallback = Icon( | ||||||
|  |       fallbackIcon ?? Symbols.account_circle, | ||||||
|  |       size: radius, | ||||||
|  |       color: fallbackColor ?? Theme.of(context).colorScheme.onPrimaryContainer, | ||||||
|  |     ).center(); | ||||||
|  |  | ||||||
|     return ClipRRect( |     return ClipRRect( | ||||||
|       borderRadius: |       borderRadius: borderRadius == null | ||||||
|           borderRadius == null |  | ||||||
|           ? BorderRadius.all(Radius.circular(radius)) |           ? BorderRadius.all(Radius.circular(radius)) | ||||||
|           : BorderRadius.all(Radius.circular(borderRadius!)), |           : BorderRadius.all(Radius.circular(borderRadius!)), | ||||||
|       child: Container( |       child: Container( | ||||||
|         width: radius * 2, |         width: radius * 2, | ||||||
|         height: radius * 2, |         height: radius * 2, | ||||||
|         color: Theme.of(context).colorScheme.primaryContainer, |         color: Theme.of(context).colorScheme.primaryContainer, | ||||||
|         child: |         child: id == null | ||||||
|             file != null |             ? fallback | ||||||
|                 ? CloudFileWidget(item: file!, fit: BoxFit.cover) |             : DataSavingGate( | ||||||
|                 : fileId == null |                 bypass: true, | ||||||
|                 ? Icon( |                 placeholder: fallback, | ||||||
|                   fallbackIcon ?? Symbols.account_circle, |                 content: () => UniversalImage( | ||||||
|                   size: radius, |                   uri: '$serverUrl/drive/files/$id', | ||||||
|                   color: |                   fit: BoxFit.cover, | ||||||
|                       fallbackColor ?? |                 ), | ||||||
|                       Theme.of(context).colorScheme.onPrimaryContainer, |               ), | ||||||
|                 ).center() |  | ||||||
|                 : UniversalImage(uri: uri, fit: BoxFit.cover), |  | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								lib/widgets/data_saving_gate.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								lib/widgets/data_saving_gate.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/pods/config.dart'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | typedef WidgetBuilder0 = Widget Function(); | ||||||
|  |  | ||||||
|  | class DataSavingGate extends ConsumerWidget { | ||||||
|  |   final bool bypass; | ||||||
|  |   final WidgetBuilder0 content; | ||||||
|  |   final Widget placeholder; | ||||||
|  |  | ||||||
|  |   const DataSavingGate({ | ||||||
|  |     super.key, | ||||||
|  |     required this.bypass, | ||||||
|  |     required this.content, | ||||||
|  |     required this.placeholder, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final dataSaving = | ||||||
|  |         ref.watch(appSettingsNotifierProvider.select((s) => s.dataSavingMode)); | ||||||
|  |     if (bypass || !dataSaving) return content(); | ||||||
|  |     return placeholder; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -48,7 +48,7 @@ class PostFeaturedList extends HookConsumerWidget { | |||||||
|         'PostFeaturedList: isCollapsed changed to ${isCollapsed.value}', |         'PostFeaturedList: isCollapsed changed to ${isCollapsed.value}', | ||||||
|       ); |       ); | ||||||
|       return null; |       return null; | ||||||
|     }, [isCollapsed.value]); |     }, [isCollapsed]); | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect(() { | ||||||
|       if (featuredPostsAsync.hasValue && featuredPostsAsync.value!.isNotEmpty) { |       if (featuredPostsAsync.hasValue && featuredPostsAsync.value!.isNotEmpty) { | ||||||
| @@ -93,7 +93,7 @@ class PostFeaturedList extends HookConsumerWidget { | |||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|       return null; |       return null; | ||||||
|     }, [featuredPostsAsync.value]); |     }, [featuredPostsAsync]); | ||||||
|  |  | ||||||
|     return ClipRRect( |     return ClipRRect( | ||||||
|       borderRadius: const BorderRadius.all(Radius.circular(8)), |       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|   | |||||||
| @@ -36,9 +36,11 @@ class PostShuffleScreen extends HookConsumerWidget { | |||||||
|               bottom: |               bottom: | ||||||
|                   kBottomControlHeight + MediaQuery.of(context).padding.bottom, |                   kBottomControlHeight + MediaQuery.of(context).padding.bottom, | ||||||
|             ), |             ), | ||||||
|             child: |             child: Builder( | ||||||
|                 (postListState.value?.items.length ?? 0) > 0 |               key: ValueKey(postListState.value?.items.length ?? 0), | ||||||
|                     ? CardSwiper( |               builder: (context) { | ||||||
|  |                 if ((postListState.value?.items.length ?? 0) > 0) { | ||||||
|  |                   return CardSwiper( | ||||||
|                     controller: cardSwiperController, |                     controller: cardSwiperController, | ||||||
|                     cardsCount: postListState.value!.items.length, |                     cardsCount: postListState.value!.items.length, | ||||||
|                     isLoop: false, |                     isLoop: false, | ||||||
| @@ -67,15 +69,17 @@ class PostShuffleScreen extends HookConsumerWidget { | |||||||
|                         ), |                         ), | ||||||
|                       ); |                       ); | ||||||
|                     }, |                     }, | ||||||
|                       onEnd: () { |                     onEnd: () async { | ||||||
|                       if (postListState.value?.hasMore ?? true) { |                       if (postListState.value?.hasMore ?? true) { | ||||||
|                           postListNotifier.fetch( |                         postListNotifier.forceRefresh(); | ||||||
|                             cursor: postListState.value?.nextCursor, |  | ||||||
|                           ); |  | ||||||
|                       } |                       } | ||||||
|                     }, |                     }, | ||||||
|                     ) |                   ); | ||||||
|                     : Center(child: CircularProgressIndicator()), |                 } else { | ||||||
|  |                   return Center(child: CircularProgressIndicator()); | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|           ), |           ), | ||||||
|           Positioned( |           Positioned( | ||||||
|             left: 0, |             left: 0, | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -50,7 +50,7 @@ packages: | |||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.3" |     version: "2.0.3" | ||||||
|   archive: |   archive: | ||||||
|     dependency: transitive |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: archive |       name: archive | ||||||
|       sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" |       sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" | ||||||
| @@ -1789,6 +1789,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.0" |     version: "2.3.0" | ||||||
|  |   pausable_timer: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: pausable_timer | ||||||
|  |       sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.1.0+3" | ||||||
|   petitparser: |   petitparser: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1877,6 +1885,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.0.3" |     version: "6.0.3" | ||||||
|  |   process_run: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: process_run | ||||||
|  |       sha256: "6ec839cdd3e6de4685318e7686cd4abb523c3d3a55af0e8d32a12ae19bc66622" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "1.2.4" | ||||||
|   protobuf: |   protobuf: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -2250,6 +2266,14 @@ packages: | |||||||
|     description: flutter |     description: flutter | ||||||
|     source: sdk |     source: sdk | ||||||
|     version: "0.0.0" |     version: "0.0.0" | ||||||
|  |   slide_countdown: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: slide_countdown | ||||||
|  |       sha256: "363914f96389502467d4dc9c0f26e88f93df3d8e37de2d5ff05b16d981fe973d" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "2.0.2" | ||||||
|   source_gen: |   source_gen: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -2677,7 +2701,7 @@ packages: | |||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.4.0" |     version: "3.4.0" | ||||||
|   wakelock_plus: |   wakelock_plus: | ||||||
|     dependency: transitive |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: wakelock_plus |       name: wakelock_plus | ||||||
|       sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678 |       sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678 | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev | |||||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||||
| # In Windows, build-name is used as the major, minor, and patch parts | # In Windows, build-name is used as the major, minor, and patch parts | ||||||
| # of the product and file versions while build-number is used as the build suffix. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 3.2.0+129 | version: 3.2.0+131 | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: ^3.7.2 |   sdk: ^3.7.2 | ||||||
| @@ -132,6 +132,8 @@ dependencies: | |||||||
|   flutter_typeahead: ^5.2.0 |   flutter_typeahead: ^5.2.0 | ||||||
|   waveform_flutter: ^1.2.0 |   waveform_flutter: ^1.2.0 | ||||||
|   flutter_app_update: ^3.2.2 |   flutter_app_update: ^3.2.2 | ||||||
|  |   archive: ^4.0.7 | ||||||
|  |   process_run: ^1.2.0 | ||||||
|   firebase_crashlytics: ^5.0.1 |   firebase_crashlytics: ^5.0.1 | ||||||
|   firebase_analytics: ^12.0.1 |   firebase_analytics: ^12.0.1 | ||||||
|   material_color_utilities: ^0.11.1 |   material_color_utilities: ^0.11.1 | ||||||
| @@ -141,6 +143,8 @@ dependencies: | |||||||
|   tray_manager: ^0.5.1 |   tray_manager: ^0.5.1 | ||||||
|   flutter_webrtc: ^1.1.0 |   flutter_webrtc: ^1.1.0 | ||||||
|   flutter_local_notifications: ^19.4.1 |   flutter_local_notifications: ^19.4.1 | ||||||
|  |   wakelock_plus: ^1.3.2 | ||||||
|  |   slide_countdown: ^2.0.2 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user