Compare commits

..

27 Commits

Author SHA1 Message Date
6f9d51673b 🚀 Launch 3.2.0+131 2025-09-07 16:33:04 +08:00
f8c6887769 Notable day countdown 2025-09-07 16:30:36 +08:00
cd2a507b7f Account region settings 2025-09-07 16:00:30 +08:00
3cafce00a2 Windows auto update 2025-09-07 15:44:51 +08:00
837f3fbe98 👽 Update the update service to use Solsynth download source 2025-09-07 15:31:36 +08:00
ca7cc5d7ee 👽 Update auth challenge model for remote changes 2025-09-07 15:24:56 +08:00
ef2c14daa2 🐛 Fix image rendering 2025-09-07 14:20:00 +08:00
3a17837cc6 💄 Optimize cloud file rendering 2025-09-07 14:12:51 +08:00
2617a64acf 💄 Optimize post actions 2025-09-07 13:11:18 +08:00
afe1e12a3b 🐛 Some fixes 2025-09-07 13:00:59 +08:00
be80f5ff85 💄 Shadow on reblur button 2025-09-07 02:35:36 +08:00
3281d69eba Apply texas's pathc to reblur content 2025-09-07 02:34:51 +08:00
77b6ce9937 Post actions on post detail page 2025-09-07 02:31:57 +08:00
39275f61b5 🐛 Fix trackpack no longer able to scroll 2025-09-07 02:11:28 +08:00
72193ba8f3 Allow use mosue drag to scroll vertical lists 2025-09-07 02:09:07 +08:00
98dd9b6617 💄 Some changes to improve UX 2025-09-07 01:31:41 +08:00
a22b94a263 Call pod keep alive 2025-09-07 01:20:22 +08:00
9c75eafdb3 Call wakelock 2025-09-07 01:11:40 +08:00
28fda3d0c7 Publisher list on account 2025-09-07 01:05:49 +08:00
187c2ea43e ♻️ Refactor the profile and pub profile 2025-09-07 01:05:49 +08:00
ae7d967461 🐛 Fix some errors 2025-09-07 01:05:49 +08:00
1ce71f1fa1 🐛 Fix post shuffle 2025-09-07 01:05:48 +08:00
9b68808c77 🐛 Fix iOS NSE 2025-09-07 01:05:48 +08:00
Texas0295
99b7bf8199 fixup! data-saving: implement gate with bypass 2025-09-07 00:21:20 +08:00
Texas0295
eb9bb73c31 fixup! data-saving: implement gate with bypass 2025-09-06 19:55:35 +08:00
Texas0295
a8c3830d67 data-saving: implement gate with bypass
- Implement DataSavingGate util (previous commit was only the shell)
- Update ProfilePictureWidget to always load avatars via UniversalImage
  using fileId, bypassing CloudFileWidget and its data-saving check
- Keep larger media under data-saving control
- Add i18n strings for data-saving mode

Signed-off-by: Texas0295 <kimura@texas0295.top>
2025-09-06 14:14:00 +08:00
Texas0295
07a5a19141 settings: add Data Saving Mode toggle (UI & i18n only)
Signed-off-by: Texas0295 <kimura@texas0295.top>
2025-09-06 14:13:59 +08:00
45 changed files with 3269 additions and 1176 deletions

View File

@@ -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"
} }

View File

@@ -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": "距离 {} 还有"
} }

View File

@@ -315,6 +315,8 @@
"settingsRealmCompactView": "緊湊領域視圖", "settingsRealmCompactView": "緊湊領域視圖",
"settingsMixedFeed": "混合動態", "settingsMixedFeed": "混合動態",
"settingsAutoTranslate": "自動翻譯", "settingsAutoTranslate": "自動翻譯",
"settingsDataSavingMode": "低數據模式",
"dataSavingHint": "低數據模式",
"settingsHideBottomNav": "隱藏底部導航", "settingsHideBottomNav": "隱藏底部導航",
"settingsSoundEffects": "音效", "settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "愚人節功能", "settingsAprilFoolFeatures": "愚人節功能",
@@ -825,4 +827,4 @@
"copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。", "copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。",
"expiresIn": "過期時間(秒)", "expiresIn": "過期時間(秒)",
"isOidc": "OIDC 相容" "isOidc": "OIDC 相容"
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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(),

View File

@@ -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({

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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));
});
}
} }

View File

@@ -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(),

View File

@@ -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();
} }
} }

View File

@@ -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') ? null : _$callNotifierHash,
const bool.fromEnvironment('dart.vm.product') dependencies: null,
? null allTransitiveDependencies: null,
: _$callNotifierHash, );
dependencies: 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

View File

@@ -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);

View File

@@ -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

View File

@@ -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)

View File

@@ -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(

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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(() {

View File

@@ -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 {

View File

@@ -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,29 +392,58 @@ class PostDetailScreen extends HookConsumerWidget {
return Stack( return Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
CustomScrollView( ExtendedRefreshIndicator(
slivers: [ onRefresh: () async {
SliverToBoxAdapter( ref.invalidate(postProvider(id));
child: Center( ref.invalidate(postRepliesNotifierProvider(id));
child: ConstrainedBox( },
constraints: BoxConstraints(maxWidth: 600), child: CustomScrollView(
child: PostItem( physics: const AlwaysScrollableScrollPhysics(),
item: post!, slivers: [
isFullPost: true, SliverToBoxAdapter(
isEmbedReply: false, child: Center(
onUpdate: (newItem) { child: ConstrainedBox(
// Update the local state with the new post data constraints: BoxConstraints(maxWidth: 600),
ref child: PostItem(
.read(postStateProvider(id).notifier) item: post!,
.updatePost(newItem); isFullPost: true,
}, isEmbedReply: false,
onUpdate: (newItem) {
// Update the local state with the new post data
ref
.read(postStateProvider(id).notifier)
.updatePost(newItem);
},
),
), ),
), ),
), ),
), SliverToBoxAdapter(
PostRepliesList(postId: id, maxWidth: 600), child: Center(
SliverGap(MediaQuery.of(context).padding.bottom + 80), 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),
SliverGap(MediaQuery.of(context).padding.bottom + 80),
],
),
), ),
if (user.value != null) if (user.value != null)
Positioned( Positioned(
@@ -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)),
), ),
), ),
); );

View File

@@ -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,

View File

@@ -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

View File

@@ -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(() {

View File

@@ -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':
appWindow.show(); if (appWindow.isVisible) {
appWindow.restore();
} else {
appWindow.show();
}
break; break;
case 'exit_app': case 'exit_app':
appWindow.close(); appWindow.close();

View File

@@ -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,

View 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);
});
}

View File

@@ -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,

View File

@@ -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});

View File

@@ -63,7 +63,11 @@ class AppWrapper extends HookConsumerWidget with TrayListener {
} }
void _trayIconPrimaryAction() { void _trayIconPrimaryAction() {
appWindow.show(); if (appWindow.isVisible) {
appWindow.restore();
} else {
appWindow.show();
}
} }
void _trayIconSecondaryAction() { void _trayIconSecondaryAction() {

View File

@@ -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,98 +100,147 @@ 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( Column(
borderRadius: BorderRadius.circular(8), crossAxisAlignment: CrossAxisAlignment.start,
child: Container( children: [
color: Theme.of(context).colorScheme.secondaryContainer, Row(
width: 56, spacing: 8,
height: 56, mainAxisSize: MainAxisSize.min,
child: crossAxisAlignment: CrossAxisAlignment.center,
Column( children: [
mainAxisSize: MainAxisSize.min, Icon(
crossAxisAlignment: CrossAxisAlignment.center, switch (DateTime.now().weekday) {
children: [ 6 || 7 => Symbols.weekend,
Text(DateFormat('EEE').format(DateTime.now())) _ => isAdult ? Symbols.work : Symbols.school,
.fontSize(16) },
.bold() fill: 1,
.textColor( size: 16,
Theme.of(context).colorScheme.onSecondaryContainer, ).padding(right: 2),
Text(DateFormat('EEE').format(DateTime.now()))
.fontSize(16)
.bold()
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
Text(DateFormat('MM/dd').format(DateTime.now()))
.fontSize(12)
.textColor(
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(),
),
),
],
),
],
).padding(horizontal: 16, top: 8),
const Divider(height: 1),
Row(
children: [
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: todayResult.when(
data: (result) {
if (result == null) return _CheckInNoneWidget();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'checkInResultLevel${result.level}',
).tr().fontSize(15).bold(),
Wrap(
children:
result.tips
.map((e) {
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),
],
)
.toList()
..removeLast(),
), ),
Text(DateFormat('MM/dd').format(DateTime.now())) ],
.fontSize(12) );
.textColor( },
Theme.of(context).colorScheme.onSecondaryContainer, loading: () => _CheckInNoneWidget(),
), error:
], (err, stack) => Column(
).center(), crossAxisAlignment: CrossAxisAlignment.start,
), children: [
), Text('error').tr().fontSize(15).bold(),
Expanded( Text(err.toString()).fontSize(11),
child: AnimatedSwitcher( ],
duration: const Duration(milliseconds: 300), ),
child: todayResult.when( ),
data: (result) { ),
if (result == null) return _CheckInNoneWidget(); ),
return Column( IconButton.outlined(
crossAxisAlignment: CrossAxisAlignment.stretch, onPressed: () {
children: [ if (todayResult.valueOrNull == null) {
Text( checkIn();
'checkInResultLevel${result.level}', } else {
).tr().fontSize(15).bold(), context.pushNamed(
Text( 'accountCalendar',
result.tips pathParameters: {'name': 'me'},
.map( );
(e) => '${e.isPositive ? '' : ''} ${e.title}', }
)
.join(' · '),
).fontSize(11),
],
);
}, },
loading: () => _CheckInNoneWidget(), icon: AnimatedSwitcher(
error: duration: const Duration(milliseconds: 300),
(err, stack) => Column( child: todayResult.when(
crossAxisAlignment: CrossAxisAlignment.start, data:
children: [ (result) => Icon(
Text('error').tr().fontSize(15).bold(), result == null
Text(err.toString()).fontSize(11), ? Symbols.local_fire_department
], : Symbols.event,
), key: ValueKey(result != null),
),
loading: () => const Icon(Symbols.refresh),
error: (_, _) => const Icon(Symbols.error),
),
),
), ),
), ],
), ).padding(horizontal: 16, bottom: 12, top: 4),
IconButton.outlined(
onPressed: () {
if (todayResult.valueOrNull == null) {
checkIn();
} else {
context.pushNamed(
'accountCalendar',
pathParameters: {'name': 'me'},
);
}
},
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: todayResult.when(
data:
(result) => Icon(
result == null
? Symbols.local_fire_department
: Symbols.event,
key: ValueKey(result != null),
),
loading: () => const Icon(Symbols.refresh),
error: (_, _) => const Icon(Symbols.error),
),
),
),
], ],
).padding(horizontal: 16, vertical: 12), ),
); );
} }
} }

View File

@@ -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

View File

@@ -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) {

View File

@@ -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,164 +804,213 @@ 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(
child: CloudFileWidget(item: file, noBlurhash: true), fit: fit,
),
),
if (isImage)
CloudFileWidget(
item: file, item: file,
heroTag: heroTag,
noBlurhash: true, noBlurhash: true,
fit: BoxFit.contain, useInternalGate: false,
) ),
else );
CloudFileWidget(item: file, heroTag: heroTag, fit: BoxFit.contain), } else {
], bg = const ColoredBox(color: Colors.black26);
}
}
final bool fullyUnlocked = !lockedByDS && !lockedByMature;
Widget fg =
fullyUnlocked
? (isImage
? CloudFileWidget(
item: file,
heroTag: heroTag,
noBlurhash: true,
fit: fit,
useInternalGate: false,
)
: CloudFileWidget(
item: file,
heroTag: heroTag,
fit: fit,
useInternalGate: false,
))
: AspectRatio(aspectRatio: ratio, child: const SizedBox.shrink());
Widget overlays;
if (lockedByDS) {
overlays = _DataSavingOverlay();
} else if (file.sensitiveMarks.isNotEmpty) {
overlays = _SensitiveOverlay(
file: file,
isRevealed: showMature.value,
onHide: () => showMature.value = false,
);
} else {
overlays = const SizedBox.shrink();
}
final content = Stack(
fit: StackFit.expand,
children: [if (isImage) Positioned.fill(child: bg), fg, overlays],
); );
if (file.sensitiveMarks.isNotEmpty) { return InkWell(
// Show a blurred overlay only when not revealed yet, with a smooth transition borderRadius: const BorderRadius.all(Radius.circular(16)),
content = Stack( onTap: () {
children: [ if (lockedByDS) {
content, showDataSaving.value = true;
// Toggle blur overlay with animation } else if (lockedByMature) {
Positioned.fill( showMature.value = true;
child: AnimatedSwitcher( } else {
duration: const Duration(milliseconds: 250), onTap?.call();
switchInCurve: Curves.easeOut, }
switchOutCurve: Curves.easeIn, },
layoutBuilder: child: content,
(currentChild, previousChildren) => Stack( );
fit: StackFit.expand, }
children: [ }
...previousChildren,
if (currentChild != null) currentChild, class _SensitiveOverlay extends StatelessWidget {
], final SnCloudFile file;
), final VoidCallback? onHide;
child: final bool isRevealed;
showMature.value
? const SizedBox.shrink(key: ValueKey('revealed')) const _SensitiveOverlay({
: ColoredBox( required this.file,
key: const ValueKey('blurred'), this.onHide,
color: Colors.transparent, this.isRevealed = false,
child: BackdropFilter( });
filter: ImageFilter.blur(sigmaX: 64, sigmaY: 64),
child: Stack( @override
fit: StackFit.expand, Widget build(BuildContext context) {
children: [ if (isRevealed) {
const ColoredBox(color: Colors.transparent), return Positioned(
Center( top: 3,
child: Container( left: 4,
margin: const EdgeInsets.all(12), child: IconButton(
padding: const EdgeInsets.symmetric( iconSize: 16,
horizontal: 12, constraints: const BoxConstraints(),
vertical: 8, icon: const Icon(
), Icons.visibility_off,
decoration: BoxDecoration( color: Colors.white,
color: Colors.black54, shadows: [
borderRadius: BorderRadius.circular(12), Shadow(
), color: Colors.black,
child: ConstrainedBox( blurRadius: 5.0,
constraints: const BoxConstraints( offset: Offset(1.0, 1.0),
maxWidth: 280, ),
), ],
child: Column( ),
mainAxisSize: MainAxisSize.min, tooltip: 'Blur content',
children: [ onPressed: onHide,
const Icon( ),
Icons.warning, );
color: Colors.white, }
fill: 1,
size: 24, return BackdropFilter(
), filter: ImageFilter.blur(sigmaX: 64, sigmaY: 64),
const Gap(4), child: Container(
Text( color: Colors.transparent,
file.sensitiveMarks child: Center(
.map( child: _OverlayCard(
(e) => icon: Icons.warning,
SensitiveCategory title: file.sensitiveMarks
.values[e] .map((e) => SensitiveCategory.values[e].i18nKey.tr())
.i18nKey .join(' · '),
.tr(), subtitle: 'Sensitive Content',
) hint: 'Tap to Reveal',
.join(' · '), ),
style: const TextStyle( ),
color: Colors.white, ),
fontWeight: FontWeight.w600, );
), }
textAlign: TextAlign.center, }
),
Text( class _DataSavingOverlay extends StatelessWidget {
'Sensitive Content', @override
style: TextStyle( Widget build(BuildContext context) {
color: Colors.white, return ColoredBox(
fontSize: 13, color: Colors.black38,
), child: Center(
), child: _OverlayCard(
const Gap(4), icon: Symbols.image,
Text( title: 'Data Saving Mode',
'Tap to Reveal', subtitle: '',
style: TextStyle( hint: 'Tap to Load',
color: Colors.white, ),
fontSize: 11, ),
), );
), }
], }
),
).padding(horizontal: 24, vertical: 16), class _OverlayCard extends StatelessWidget {
), final IconData icon;
), final String title;
], final String subtitle;
), final String hint;
),
), const _OverlayCard({
), required this.icon,
), required this.title,
// When revealed (no blur), show a small control at top-left to re-enable blur required this.subtitle,
if (showMature.value) required this.hint,
Positioned( });
top: 3,
left: 4, @override
child: IconButton( Widget build(BuildContext context) {
iconSize: 16, return Container(
constraints: const BoxConstraints(), margin: const EdgeInsets.all(12),
icon: const Icon(Icons.visibility_off, color: Colors.white), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
tooltip: 'Blur content', decoration: BoxDecoration(
onPressed: () { color: Colors.black54,
showMature.value = false; 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),
if (onTap != null) { Text(
return InkWell( title,
borderRadius: const BorderRadius.all(Radius.circular(16)), style: const TextStyle(
onTap: () { color: Colors.white,
if (!showMature.value) { fontWeight: FontWeight.w600,
showMature.value = true; ),
} else { textAlign: TextAlign.center,
onTap?.call(); ),
} Text(
}, subtitle,
child: content, style: const TextStyle(color: Colors.white, fontSize: 13),
); ),
} const Gap(4),
Text(hint, style: const TextStyle(color: Colors.white, fontSize: 11)),
return content; ],
),
);
} }
} }

View File

@@ -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,86 +24,97 @@ 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: (useInternalGate && dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.play_arrow) : cloudVideo(),
child: CloudVideoWidget(item: item), ),
), '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), ),
child: UniversalAudio(uri: uri, filename: item.name),
), ),
child: UniversalAudio(uri: uri, filename: item.name),
), ),
),
_ => Column( _ => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
Symbols.insert_drive_file, Symbols.insert_drive_file,
size: 48, size: 48,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const Gap(8),
Text(
item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), const Gap(8),
Text( Text(
formatFileSize(item.size), item.name,
style: TextStyle( maxLines: 1,
fontSize: 12, overflow: TextOverflow.ellipsis,
color: Theme.of(context).colorScheme.onSurfaceVariant, style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
), ),
), Text(
const Gap(8), formatFileSize(item.size),
TextButton.icon( style: TextStyle(
onPressed: () { fontSize: 12,
launchUrlString( color: Theme.of(context).colorScheme.onSurfaceVariant,
'https://fs.solian.app/files/${item.id}', ),
mode: LaunchMode.externalApplication, ),
); const Gap(8),
}, TextButton.icon(
icon: const Icon(Symbols.launch), onPressed: () {
label: Text('openInBrowser').tr(), launchUrlString(
), 'https://fs.solian.app/files/${item.id}',
], mode: LaunchMode.externalApplication,
).padding(all: 8), );
},
icon: const Icon(Symbols.launch),
label: Text('openInBrowser').tr(),
),
],
).padding(all: 8),
}; };
if (heroTag != null) { if (heroTag != null) {
@@ -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});
@@ -311,32 +352,35 @@ class ProfilePictureWidget extends ConsumerWidget {
this.fallbackColor, this.fallbackColor,
}); });
@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),
), ),
); );
} }

View 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;
}
}

View File

@@ -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)),

View File

@@ -36,46 +36,50 @@ 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) {
controller: cardSwiperController, if ((postListState.value?.items.length ?? 0) > 0) {
cardsCount: postListState.value!.items.length, return CardSwiper(
isLoop: false, controller: cardSwiperController,
cardBuilder: ( cardsCount: postListState.value!.items.length,
context, isLoop: false,
index, cardBuilder: (
horizontalOffsetPercentage, context,
verticalOffsetPercentage, index,
) { horizontalOffsetPercentage,
return Center( verticalOffsetPercentage,
child: ConstrainedBox( ) {
constraints: BoxConstraints(maxWidth: 540), return Center(
child: SingleChildScrollView( child: ConstrainedBox(
child: Card( constraints: BoxConstraints(maxWidth: 540),
margin: EdgeInsets.zero, child: SingleChildScrollView(
child: ClipRRect( child: Card(
borderRadius: const BorderRadius.all( margin: EdgeInsets.zero,
Radius.circular(8), child: ClipRRect(
), borderRadius: const BorderRadius.all(
child: PostActionableItem( Radius.circular(8),
item: postListState.value!.items[index], ),
), child: PostActionableItem(
item: postListState.value!.items[index],
), ),
), ),
), ),
), ),
); ),
}, );
onEnd: () { },
if (postListState.value?.hasMore ?? true) { onEnd: () async {
postListNotifier.fetch( if (postListState.value?.hasMore ?? true) {
cursor: postListState.value?.nextCursor, postListNotifier.forceRefresh();
); }
} },
}, );
) } else {
: Center(child: CircularProgressIndicator()), return Center(child: CircularProgressIndicator());
}
},
),
), ),
Positioned( Positioned(
left: 0, left: 0,

View File

@@ -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

View File

@@ -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: