Compare commits

..

57 Commits

Author SHA1 Message Date
1cc34240da Post embed view rendering 2025-09-08 02:42:49 +08:00
013f7f02bc Post embed view 2025-09-08 02:15:22 +08:00
4e79e4100f Public contact method 2025-09-08 00:40:32 +08:00
feda1f067f 🐛 Post detail initial expand poll 2025-09-07 23:57:14 +08:00
fe0e192a43 💄 Optimize error alert 2025-09-07 23:56:58 +08:00
93df294142 Poll collapse 2025-09-07 23:00:29 +08:00
78d65c39f3 🐛 Fix poll yes or no stats 2025-09-07 22:48:22 +08:00
18b0dbd797 🐛 Fix post submit 2025-09-07 22:20:49 +08:00
80cc8cbb40 🐛 Fix android build 2025-09-07 18:30:35 +08:00
646e95a9fc 💄 Optimize check in 2025-09-07 18:30:28 +08:00
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
ecc100ac45 Extended refresh indicator (keyboard based) 2025-09-06 13:52:20 +08:00
573b76d3ff 🍱 Update media offline placeholder
🐛 Fix image picker
2025-09-06 13:23:09 +08:00
f7dad5e419 Cache udid 2025-09-06 13:09:27 +08:00
9f2f1c0848 System notification for desktop and android 2025-09-06 12:59:23 +08:00
580d9fd979 🐛 Trying to make the macOS app do not quit after all windows closed 2025-09-06 11:53:22 +08:00
3b375abc09 Tray 2025-09-04 22:10:00 +08:00
c527b5e67c 🐛 Fix explore pagination 2025-09-04 00:55:37 +08:00
e9f09bbe54 Explore shuffle post 2025-09-04 00:52:02 +08:00
3aece9316c Able to temporary disable background image 2025-09-04 00:25:44 +08:00
a61c889c6c ⬆️ Upgrade flutter deps 2025-09-03 00:31:47 +08:00
0dd3221a56 🐛 Fix oidc on web close #175 2025-09-02 00:36:06 +08:00
66918521f8 More search filter 2025-09-02 00:01:36 +08:00
bb1846e462 🐛 Fix post creator style broke 2025-09-01 23:17:54 +08:00
a976a6eaf4 💄 Optimize the post item reaction made effect 2025-09-01 23:10:24 +08:00
4252f66fd3 💄 Optimize some designs 2025-09-01 23:05:11 +08:00
f2d780b48f 💄 Optimize of account profile card 2025-09-01 21:37:59 +08:00
300541f9bb ♻️ Replaced picker 2025-09-01 01:00:52 +08:00
43787bb813 💄 Optimize attachment previewer in editing 2025-09-01 00:45:35 +08:00
3417c51a3b Preview for SND files 2025-09-01 00:34:55 +08:00
f98e603e82 🐛 Dozens of bug fixes 2025-09-01 00:07:19 +08:00
105 changed files with 6192 additions and 2009 deletions

View File

@@ -62,4 +62,3 @@ If you want to build the release version, use the flutter build command. Learn m
```bash
flutter build <platform>
```

View File

@@ -24,6 +24,8 @@ android {
ndkVersion = "29.0.13113456"
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
@@ -63,6 +65,8 @@ android {
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
implementation("com.google.android.material:material:1.12.0")
implementation("com.github.bumptech.glide:glide:4.16.0")
implementation("com.squareup.okhttp3:okhttp:5.1.0")

View File

@@ -133,6 +133,25 @@
"other": "{} replies"
},
"forward": "Forward",
"award": "Award",
"awardPost": "Award Post",
"awardMessage": "Message",
"awardMessageHint": "Enter your award message...",
"awardAttitude": "Attitude",
"awardAttitudePositive": "Positive",
"awardAttitudeNegative": "Negative",
"awardAmount": "Amount",
"awardAmountHint": "Enter amount...",
"awardAmountRequired": "Amount is required",
"awardAmountInvalid": "Please enter a valid amount",
"awardMessageTooLong": "Message is too long (max 4096 characters)",
"awardSuccess": "Award sent successfully!",
"awardSubmit": "Award",
"awardPostPreview": "Post Preview",
"awardNoContent": "No content available",
"awardByPublisher": "By {}",
"awardBenefits": "Award Benefits",
"awardBenefitsDescription": "Awarding this post increases its value and visibility. Higher valued posts have a better chance of being featured and highlighted in the community.",
"repliedTo": "Replied to",
"forwarded": "Forwarded",
"hasAttachments": {
@@ -195,6 +214,7 @@
"checkInResultLevel2": "A Normal Day",
"checkInResultLevel3": "Good Luck",
"checkInResultLevel4": "Best Luck",
"checkInResultLevel5": "Happy Birthday 🥳",
"checkInActivityTitle": "{} checked in on {} and got a {}",
"eventCalander": "Event Calander",
"eventCalanderEmpty": "No events on that day.",
@@ -228,6 +248,8 @@
"settings": "Settings",
"language": "Language",
"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",
"languageFollowSystem": "Follow System",
"postsCreatedCount": "Posts",
@@ -338,6 +360,7 @@
"notifications": "Notifications",
"posts": "Posts",
"settingsBackgroundImage": "Background Image",
"settingsBackgroundImageEnable": "Show Background Image",
"settingsBackgroundImageClear": "Clear Background Image",
"settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image",
"messageNone": "No content to display",
@@ -348,6 +371,8 @@
"chatBreakNone": "None",
"settingsRealmCompactView": "Compact Realm View",
"settingsMixedFeed": "Mixed Feed",
"settingsDataSavingMode": "Data Saving Mode",
"dataSavingHint": "Data Saving Mode",
"settingsAutoTranslate": "Auto Translate",
"settingsHideBottomNav": "Hide Bottom Navigation",
"settingsSoundEffects": "Sound Effects",
@@ -492,6 +517,10 @@
"contactMethodSetPrimary": "Set as Primary",
"contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications",
"contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.",
"contactMethodMakePublic": "Make Public",
"contactMethodMakePrivate": "Make Private",
"contactMethodPublic": "Public",
"contactMethodPrivate": "Private",
"chatNotifyLevel": "Notify Level",
"chatNotifyLevelDescription": "Decide how many notifications you will receive.",
"chatNotifyLevelAll": "All",
@@ -634,8 +663,9 @@
"chatJoin": "Join the Chat",
"realmJoin": "Join the Realm",
"realmJoinSuccess": "Successfully joined the realm.",
"discoverRealms": "Discover realms",
"discoverPublishers": "Discover publishers",
"discoverRealms": "Realms",
"discoverPublishers": "Publishers",
"discoverShuffledPost": "Random Posts",
"search": "Search",
"publisherMembers": "Collaborators",
"developerHub": "Developer Hub",
@@ -693,7 +723,7 @@
"publisherFeatureDevelopDescription": "Unlock development abilities for your publisher, including custom apps, API keys, and more.",
"publisherFeatureDevelopHint": "Currently, this feature is under active development, you need send a request to unlock this feature.",
"learnMore": "Learn More",
"discoverWebArticles": "Articles from external sites",
"discoverWebArticles": "Web Feed Articles",
"webArticlesStand": "Article Stand",
"about": "About",
"membershipCancel": "Cancel Membership",
@@ -951,7 +981,6 @@
"chatBreak15m": "15m",
"chatBreak30m": "30m",
"chatBreakCustomMinutes": "Custom (minutes)",
"chatBreakEnterMinutes": "Enter minutes",
"errorGeneric": "Error: {}",
"searchMessages": "Search Messages",
"messagesCount": "{} messages",
@@ -960,5 +989,30 @@
"searchMessagesHint": "Search messages...",
"searchLinks": "Links",
"searchAttachments": "Attachments",
"noMessagesFound": "No messages found"
}
"noMessagesFound": "No messages found",
"openInBrowser": "Open in Browser",
"highlightPost": "Highlight Post",
"filters": "Filters",
"apply": "Apply",
"pubName": "Pub Name",
"realm": "Realm",
"shuffle": "Shuffle",
"pinned": "Pinned",
"noResultsFound": "No results found",
"toggleFilters": "Toggle filters",
"notableDayNext": "{} is in",
"expandPoll": "Expand Poll",
"collapsePoll": "Collapse Poll",
"embedView": "Embed View",
"embedUri": "Embed URI",
"aspectRatio": "Aspect Ratio",
"renderer": "Renderer",
"addEmbed": "Add Embed",
"editEmbed": "Edit Embed",
"deleteEmbed": "Delete Embed",
"deleteEmbedConfirm": "Are you sure you want to delete this embed?",
"currentEmbed": "Current Embed",
"noEmbed": "No embed yet",
"save": "Save",
"webView": "Web View"
}

View File

@@ -158,11 +158,12 @@
"checkIn": "签到",
"checkInNone": "尚未签到",
"checkInNoneHint": "通过签到获取您的财富提示和每日奖励。",
"checkInResultLevel0": "最差运气",
"checkInResultLevel1": "坏运气",
"checkInResultLevel2": "一个普通的日常",
"checkInResultLevel3": "好运",
"checkInResultLevel4": "最佳运气",
"checkInResultLevel0": "大凶",
"checkInResultLevel1": "",
"checkInResultLevel2": "中平",
"checkInResultLevel3": "",
"checkInResultLevel4": "大吉",
"checkInResultLevel5": "生日快乐 🥳",
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
"eventCalander": "活动日历",
"eventCalanderEmpty": "该日无活动。",
@@ -304,6 +305,7 @@
"notifications": "通知",
"posts": "帖子",
"settingsBackgroundImage": "背景图片",
"settingsBackgroundImageEnable": "显示背景图片",
"settingsBackgroundImageClear": "清除背景图片",
"settingsBackgroundGenerateColor": "从背景图像生成主题色",
"messageNone": "没有内容可显示",
@@ -314,6 +316,8 @@
"chatBreakNone": "无",
"settingsRealmCompactView": "紧凑领域视图",
"settingsMixedFeed": "混合动态",
"settingsDataSavingMode": "流量节省模式",
"dataSavingHint": "流量节省模式",
"settingsAutoTranslate": "自动翻译",
"settingsHideBottomNav": "隐藏底部导航",
"settingsSoundEffects": "音效",
@@ -857,5 +861,8 @@
"expiresIn": "过期时间(秒)",
"isOidc": "OIDC 兼容",
"statusPresent": "至今",
"accountAutomated": "机器人"
"accountAutomated": "机器人",
"openInBrowser": "在浏览器中打开",
"highlightPost": "精选帖子",
"notableDayNext": "距离 {} 还有"
}

View File

@@ -303,7 +303,8 @@
"notifications": "通知",
"posts": "帖子",
"settingsBackgroundImage": "背景圖片",
"settingsBackgroundImageClear": "清除背景圖片",
"settingsBackgroundImageEnable": "顯示背景圖片",
"settingsBackgroundImageClear": "清除背景圖片",
"settingsBackgroundGenerateColor": "從背景圖像生成主題色",
"messageNone": "沒有內容可顯示",
"unreadMessages": {
@@ -314,6 +315,8 @@
"settingsRealmCompactView": "緊湊領域視圖",
"settingsMixedFeed": "混合動態",
"settingsAutoTranslate": "自動翻譯",
"settingsDataSavingMode": "低數據模式",
"dataSavingHint": "低數據模式",
"settingsHideBottomNav": "隱藏底部導航",
"settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "愚人節功能",
@@ -824,4 +827,4 @@
"copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。",
"expiresIn": "過期時間(秒)",
"isOidc": "OIDC 相容"
}
}

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" fill="none">
<path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"
d="M54 147h86" />
<path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="10"
d="M57 111s-2-4.5-2-10m22 22s-4 7-11 4m9-22s-2-4.5-2-10" />
<path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"
d="M54 147a32 32 0 0 1-11.999-61.665A39 39 0 0 1 81 46m59 101a30 30 0 0 0 29.933-28" />
<circle cx="132" cy="75" r="4" stroke="#fff" stroke-linecap="round" stroke-linejoin="round"
stroke-width="8" />
<path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="10"
d="M112.5 41.217C100.843 47.961 93 60.564 93 75c0 6.375 1.53 12.393 4.242 17.707m69.513-35.419A38.84 38.84 0 0 1 171 75c0 14.433-7.84 27.034-19.493 33.779m-.793-43.317A20.9 20.9 0 0 1 153 75c0 7.77-4.221 14.556-10.495 18.188m-21.003-36.38C115.224 60.44 111 67.226 111 75a20.9 20.9 0 0 0 2.284 9.533" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>13.0</string>
</dict>
</plist>

View File

@@ -42,83 +42,83 @@ PODS:
- Flutter
- file_saver (0.0.1):
- Flutter
- Firebase/CoreOnly (12.0.0):
- FirebaseCore (~> 12.0.0)
- Firebase/Crashlytics (12.0.0):
- Firebase/CoreOnly (12.2.0):
- FirebaseCore (~> 12.2.0)
- Firebase/Crashlytics (12.2.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 12.0.0)
- Firebase/Messaging (12.0.0):
- FirebaseCrashlytics (~> 12.2.0)
- Firebase/Messaging (12.2.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.0.0)
- firebase_analytics (12.0.0):
- FirebaseMessaging (~> 12.2.0)
- firebase_analytics (12.0.1):
- firebase_core
- FirebaseAnalytics (= 12.0.0)
- FirebaseAnalytics (= 12.2.0)
- Flutter
- firebase_core (4.0.0):
- Firebase/CoreOnly (= 12.0.0)
- firebase_core (4.1.0):
- Firebase/CoreOnly (= 12.2.0)
- Flutter
- firebase_crashlytics (5.0.0):
- Firebase/Crashlytics (= 12.0.0)
- firebase_crashlytics (5.0.1):
- Firebase/Crashlytics (= 12.2.0)
- firebase_core
- Flutter
- firebase_messaging (16.0.0):
- Firebase/Messaging (= 12.0.0)
- firebase_messaging (16.0.1):
- Firebase/Messaging (= 12.2.0)
- firebase_core
- Flutter
- FirebaseAnalytics (12.0.0):
- FirebaseAnalytics/Default (= 12.0.0)
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- FirebaseAnalytics (12.2.0):
- FirebaseAnalytics/Default (= 12.2.0)
- FirebaseCore (~> 12.2.0)
- FirebaseInstallations (~> 12.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- GoogleAppMeasurement/Default (= 12.0.0)
- FirebaseAnalytics/Default (12.2.0):
- FirebaseCore (~> 12.2.0)
- FirebaseInstallations (~> 12.2.0)
- GoogleAppMeasurement/Default (= 12.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseCore (12.0.0):
- FirebaseCoreInternal (~> 12.0.0)
- FirebaseCore (12.2.0):
- FirebaseCoreInternal (~> 12.2.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseCoreInternal (12.0.0):
- FirebaseCoreExtension (12.2.0):
- FirebaseCore (~> 12.2.0)
- FirebaseCoreInternal (12.2.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseCrashlytics (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- FirebaseRemoteConfigInterop (~> 12.0.0)
- FirebaseSessions (~> 12.0.0)
- FirebaseCrashlytics (12.2.0):
- FirebaseCore (~> 12.2.0)
- FirebaseInstallations (~> 12.2.0)
- FirebaseRemoteConfigInterop (~> 12.2.0)
- FirebaseSessions (~> 12.2.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (12.2.0):
- FirebaseCore (~> 12.2.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- FirebaseMessaging (12.2.0):
- FirebaseCore (~> 12.2.0)
- FirebaseInstallations (~> 12.2.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- FirebaseRemoteConfigInterop (12.0.0)
- FirebaseSessions (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseCoreExtension (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- FirebaseRemoteConfigInterop (12.2.0)
- FirebaseSessions (12.2.0):
- FirebaseCore (~> 12.2.0)
- FirebaseCoreExtension (~> 12.2.0)
- FirebaseInstallations (~> 12.2.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
@@ -136,6 +136,8 @@ PODS:
- OrderedSet (~> 6.0.3)
- flutter_keyboard_visibility (0.0.1):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (2.4.3):
- Flutter
- flutter_platform_alert (0.0.1):
@@ -147,33 +149,33 @@ PODS:
- flutter_udid (0.0.1):
- Flutter
- SAMKeychain
- flutter_webrtc (1.0.0):
- flutter_webrtc (1.1.0):
- Flutter
- WebRTC-SDK (= 137.7151.02)
- WebRTC-SDK (= 137.7151.03)
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleAdsOnDeviceConversion (2.1.0):
- GoogleAdsOnDeviceConversion (2.3.0):
- GoogleUtilities/Logger (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Core (12.0.0):
- GoogleAppMeasurement/Core (12.2.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (12.0.0):
- GoogleAdsOnDeviceConversion (= 2.1.0)
- GoogleAppMeasurement/Core (= 12.0.0)
- GoogleAppMeasurement/IdentitySupport (= 12.0.0)
- GoogleAppMeasurement/Default (12.2.0):
- GoogleAdsOnDeviceConversion (= 2.3.0)
- GoogleAppMeasurement/Core (= 12.2.0)
- GoogleAppMeasurement/IdentitySupport (= 12.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (12.0.0):
- GoogleAppMeasurement/Core (= 12.0.0)
- GoogleAppMeasurement/IdentitySupport (12.2.0):
- GoogleAppMeasurement/Core (= 12.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
@@ -217,7 +219,7 @@ PODS:
- livekit_client (2.5.0):
- Flutter
- flutter_webrtc
- WebRTC-SDK (= 137.7151.02)
- WebRTC-SDK (= 137.7151.03)
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
@@ -250,9 +252,9 @@ PODS:
- record_ios (1.1.0):
- Flutter
- SAMKeychain (1.5.3)
- SDWebImage (5.21.1):
- SDWebImage/Core (= 5.21.1)
- SDWebImage/Core (5.21.1)
- SDWebImage (5.21.2):
- SDWebImage/Core (= 5.21.2)
- SDWebImage/Core (5.21.2)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -297,7 +299,7 @@ PODS:
- Flutter
- wakelock_plus (0.0.1):
- Flutter
- WebRTC-SDK (137.7151.02)
- WebRTC-SDK (137.7151.03)
DEPENDENCIES:
- Alamofire
@@ -314,6 +316,7 @@ DEPENDENCIES:
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/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_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
@@ -402,6 +405,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_platform_alert:
@@ -470,39 +475,40 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Firebase: 800d487043c0557d9faed71477a38d9aafb08a41
firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d
firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4
firebase_crashlytics: 2c6c1a17900a38081d938330e9f48e60ec5b255d
firebase_messaging: d17feef781edc84ebefe62624fb384358ad96361
FirebaseAnalytics: 6d790cd1b159b4eb61a99948df0934ce505a34f7
FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a
FirebaseCoreExtension: 639afb3de6abd611952be78a794c54a47fa0f361
FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55
FirebaseCrashlytics: db75aa0cab8d00f68406fa247c32fe17ade884d7
FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988
FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde
FirebaseRemoteConfigInterop: bfa0ea72ba3dc5af739777296424e46bd6f42613
FirebaseSessions: 4e784acda213108aafef536535cdfc03504acc42
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1
firebase_analytics: 111ff65791a430356bd6c7e4d7339537fc6a15ae
firebase_core: 3ff52146406557dddd01d570e807e203ec7e1302
firebase_crashlytics: 3637078b718a52dc9fb4d64e37c969e86b87ff6f
firebase_messaging: 3dcc998dd98e1e54af75d0cccae8606eba43553c
FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8
FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd
FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5
FirebaseCoreInternal: 56ea29f3dad2894f81b060f706f9d53509b6ed3b
FirebaseCrashlytics: f83cbf176d5c637ade108c0aacf1ccbd5ec499bf
FirebaseInstallations: 3e884b01feabdf67582a80f3250425a00979b4ed
FirebaseMessaging: 43ec73bbfedd0c385a849bb91593ab4ad4b9e48e
FirebaseRemoteConfigInterop: 0896fd52ab72586a355c8f389ff85aaa9e5375e1
FirebaseSessions: f4692789e770bec66ce17d772c0e9561c4f11737
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_webrtc: 6f7da106613d52ade777d5b4875a43f48c28b457
flutter_webrtc: b0b2e04411747142962164a1cfa43a1af9a0afac
gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleAdsOnDeviceConversion: 2be6297a4f048459e0ae17fad9bfd2844e10cf64
GoogleAppMeasurement: 8f6ab04ad6ae493b53fcf56bd26323fb2f1384f3
GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe
GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c
livekit_client: e3b79b99405428aac439b6b76a254cd9a11dbbfb
livekit_client: f810c81bbbc229a84f60b09e66603ac4e93f7599
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
@@ -518,7 +524,7 @@ SPEC CHECKSUMS:
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
@@ -530,7 +536,7 @@ SPEC CHECKSUMS:
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
WebRTC-SDK: d20de357dcbf7c9696b124b39f3ff62125107e4b
WebRTC-SDK: 69d4e56b0b4b27d788e87bab9b9a1326ed05b1e3
PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f

View File

@@ -853,7 +853,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -897,6 +897,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -915,6 +916,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -931,6 +933,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1078,7 +1081,7 @@
INFOPLIST_FILE = SolianShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1121,7 +1124,7 @@
INFOPLIST_FILE = SolianShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1161,7 +1164,7 @@
INFOPLIST_FILE = SolianShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1348,7 +1351,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -1399,7 +1402,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;

View File

@@ -47,6 +47,7 @@ class NotificationService: UNNotificationServiceExtension {
private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws {
switch content.userInfo["type"] as? String {
case "messages.new":
content.categoryIdentifier = "REPLYABLE_MESSAGE"
try handleMessagingNotification(request: request, content: content)
default:
try handleDefaultNotification(content: content)
@@ -60,8 +61,6 @@ class NotificationService: UNNotificationServiceExtension {
let pfpIdentifier = meta["pfp"] as? String
content.categoryIdentifier = "REPLYABLE_MESSAGE"
let metaCopy = meta as? [String: Any] ?? [:]
let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil

View File

@@ -30,7 +30,6 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
@@ -52,7 +51,6 @@ void main() async {
}
try {
await langdetect.initLangDetect();
await EasyLocalization.ensureInitialized();
if (kIsWeb || !Platform.isLinux) {
@@ -225,6 +223,7 @@ class IslandApp extends HookConsumerWidget {
if (user.value != null) {
final apiClient = ref.read(apiClientProvider);
subscribePushNotification(apiClient);
initializeLocalNotifications();
final wsNotifier = ref.read(websocketStateProvider.notifier);
wsNotifier.connect();
}
@@ -241,6 +240,7 @@ class IslandApp extends HookConsumerWidget {
themeMode: ThemeMode.system,
routerConfig: router,
supportedLocales: context.supportedLocales,
scrollBehavior: AppScrollBehavior(),
localizationsDelegates: [
...context.localizationDelegates,
CroppyLocalizations.delegate,

View File

@@ -13,11 +13,13 @@ sealed class SnAccount with _$SnAccount {
required String name,
required String nick,
required String language,
@Default("") String region,
required bool isSuperuser,
required String? automatedId,
required SnAccountProfile profile,
required SnWalletSubscriptionRef? perkSubscription,
@Default([]) List<SnAccountBadge> badges,
@Default([]) List<SnContactMethod> contacts,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
@@ -134,6 +136,7 @@ sealed class SnContactMethod with _$SnContactMethod {
required int type,
required DateTime? verifiedAt,
required bool isPrimary,
required bool isPublic,
required String content,
required String accountId,
required DateTime createdAt,

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
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; List<SnContactMethod> get contacts; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $SnAccountCopyWith<SnAccount> get copyWith => _$SnAccountCopyWithImpl<SnAccount>
@override
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)&&const DeepCollectionEquality().equals(other.contacts, contacts)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,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),const DeepCollectionEquality().hash(contacts),createdAt,updatedAt,deletedAt);
@override
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, contacts: $contacts, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -48,7 +48,7 @@ abstract mixin class $SnAccountCopyWith<$Res> {
factory $SnAccountCopyWith(SnAccount value, $Res Function(SnAccount) _then) = _$SnAccountCopyWithImpl;
@useResult
$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, List<SnContactMethod> contacts, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -65,18 +65,20 @@ class _$SnAccountCopyWithImpl<$Res>
/// Create a copy of SnAccount
/// 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? contacts = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,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,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 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 SnAccountProfile,perkSubscription: freezed == perkSubscription ? _self.perkSubscription : perkSubscription // ignore: cast_nullable_to_non_nullable
as SnWalletSubscriptionRef?,badges: null == badges ? _self.badges : badges // ignore: cast_nullable_to_non_nullable
as List<SnAccountBadge>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as List<SnAccountBadge>,contacts: null == contacts ? _self.contacts : contacts // ignore: cast_nullable_to_non_nullable
as List<SnContactMethod>,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
@@ -182,10 +184,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, List<SnContactMethod> contacts, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
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.contacts,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
@@ -203,10 +205,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, List<SnContactMethod> contacts, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
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.contacts,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -220,10 +222,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, List<SnContactMethod> contacts, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
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.contacts,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
@@ -235,13 +237,14 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,
@JsonSerializable()
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 [], final List<SnContactMethod> contacts = const [], required this.createdAt, required this.updatedAt, required this.deletedAt}): _badges = badges,_contacts = contacts;
factory _SnAccount.fromJson(Map<String, dynamic> json) => _$SnAccountFromJson(json);
@override final String id;
@override final String name;
@override final String nick;
@override final String language;
@override@JsonKey() final String region;
@override final bool isSuperuser;
@override final String? automatedId;
@override final SnAccountProfile profile;
@@ -253,6 +256,13 @@ class _SnAccount implements SnAccount {
return EqualUnmodifiableListView(_badges);
}
final List<SnContactMethod> _contacts;
@override@JsonKey() List<SnContactMethod> get contacts {
if (_contacts is EqualUnmodifiableListView) return _contacts;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_contacts);
}
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
@@ -270,16 +280,16 @@ Map<String, dynamic> toJson() {
@override
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)&&const DeepCollectionEquality().equals(other._contacts, _contacts)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,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),const DeepCollectionEquality().hash(_contacts),createdAt,updatedAt,deletedAt);
@override
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, contacts: $contacts, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -290,7 +300,7 @@ abstract mixin class _$SnAccountCopyWith<$Res> implements $SnAccountCopyWith<$Re
factory _$SnAccountCopyWith(_SnAccount value, $Res Function(_SnAccount) _then) = __$SnAccountCopyWithImpl;
@override @useResult
$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, List<SnContactMethod> contacts, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -307,18 +317,20 @@ class __$SnAccountCopyWithImpl<$Res>
/// Create a copy of SnAccount
/// 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? contacts = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccount(
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,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,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 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 SnAccountProfile,perkSubscription: freezed == perkSubscription ? _self.perkSubscription : perkSubscription // ignore: cast_nullable_to_non_nullable
as SnWalletSubscriptionRef?,badges: null == badges ? _self._badges : badges // ignore: cast_nullable_to_non_nullable
as List<SnAccountBadge>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as List<SnAccountBadge>,contacts: null == contacts ? _self._contacts : contacts // ignore: cast_nullable_to_non_nullable
as List<SnContactMethod>,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
@@ -1624,7 +1636,7 @@ as DateTime?,
/// @nodoc
mixin _$SnContactMethod {
String get id; int get type; DateTime? get verifiedAt; bool get isPrimary; String get content; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; int get type; DateTime? get verifiedAt; bool get isPrimary; bool get isPublic; String get content; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnContactMethod
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -1637,16 +1649,16 @@ $SnContactMethodCopyWith<SnContactMethod> get copyWith => _$SnContactMethodCopyW
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnContactMethod&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.verifiedAt, verifiedAt) || other.verifiedAt == verifiedAt)&&(identical(other.isPrimary, isPrimary) || other.isPrimary == isPrimary)&&(identical(other.content, content) || other.content == content)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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 SnContactMethod&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.verifiedAt, verifiedAt) || other.verifiedAt == verifiedAt)&&(identical(other.isPrimary, isPrimary) || other.isPrimary == isPrimary)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.content, content) || other.content == content)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,verifiedAt,isPrimary,content,accountId,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,type,verifiedAt,isPrimary,isPublic,content,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnContactMethod(id: $id, type: $type, verifiedAt: $verifiedAt, isPrimary: $isPrimary, content: $content, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnContactMethod(id: $id, type: $type, verifiedAt: $verifiedAt, isPrimary: $isPrimary, isPublic: $isPublic, content: $content, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -1657,7 +1669,7 @@ abstract mixin class $SnContactMethodCopyWith<$Res> {
factory $SnContactMethodCopyWith(SnContactMethod value, $Res Function(SnContactMethod) _then) = _$SnContactMethodCopyWithImpl;
@useResult
$Res call({
String id, int type, DateTime? verifiedAt, bool isPrimary, String content, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, int type, DateTime? verifiedAt, bool isPrimary, bool isPublic, String content, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -1674,12 +1686,13 @@ class _$SnContactMethodCopyWithImpl<$Res>
/// Create a copy of SnContactMethod
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? verifiedAt = freezed,Object? isPrimary = null,Object? content = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? verifiedAt = freezed,Object? isPrimary = null,Object? isPublic = null,Object? content = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,verifiedAt: freezed == verifiedAt ? _self.verifiedAt : verifiedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isPrimary: null == isPrimary ? _self.isPrimary : isPrimary // ignore: cast_nullable_to_non_nullable
as bool,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable
as bool,content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String,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
@@ -1767,10 +1780,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, int type, DateTime? verifiedAt, bool isPrimary, String content, 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, int type, DateTime? verifiedAt, bool isPrimary, bool isPublic, String content, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnContactMethod() when $default != null:
return $default(_that.id,_that.type,_that.verifiedAt,_that.isPrimary,_that.content,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.type,_that.verifiedAt,_that.isPrimary,_that.isPublic,_that.content,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
@@ -1788,10 +1801,10 @@ return $default(_that.id,_that.type,_that.verifiedAt,_that.isPrimary,_that.conte
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, int type, DateTime? verifiedAt, bool isPrimary, String content, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, int type, DateTime? verifiedAt, bool isPrimary, bool isPublic, String content, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnContactMethod():
return $default(_that.id,_that.type,_that.verifiedAt,_that.isPrimary,_that.content,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
return $default(_that.id,_that.type,_that.verifiedAt,_that.isPrimary,_that.isPublic,_that.content,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -1805,10 +1818,10 @@ return $default(_that.id,_that.type,_that.verifiedAt,_that.isPrimary,_that.conte
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, int type, DateTime? verifiedAt, bool isPrimary, String content, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, int type, DateTime? verifiedAt, bool isPrimary, bool isPublic, String content, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnContactMethod() when $default != null:
return $default(_that.id,_that.type,_that.verifiedAt,_that.isPrimary,_that.content,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.type,_that.verifiedAt,_that.isPrimary,_that.isPublic,_that.content,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
@@ -1820,13 +1833,14 @@ return $default(_that.id,_that.type,_that.verifiedAt,_that.isPrimary,_that.conte
@JsonSerializable()
class _SnContactMethod implements SnContactMethod {
const _SnContactMethod({required this.id, required this.type, required this.verifiedAt, required this.isPrimary, required this.content, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
const _SnContactMethod({required this.id, required this.type, required this.verifiedAt, required this.isPrimary, required this.isPublic, required this.content, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnContactMethod.fromJson(Map<String, dynamic> json) => _$SnContactMethodFromJson(json);
@override final String id;
@override final int type;
@override final DateTime? verifiedAt;
@override final bool isPrimary;
@override final bool isPublic;
@override final String content;
@override final String accountId;
@override final DateTime createdAt;
@@ -1846,16 +1860,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnContactMethod&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.verifiedAt, verifiedAt) || other.verifiedAt == verifiedAt)&&(identical(other.isPrimary, isPrimary) || other.isPrimary == isPrimary)&&(identical(other.content, content) || other.content == content)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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 _SnContactMethod&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.verifiedAt, verifiedAt) || other.verifiedAt == verifiedAt)&&(identical(other.isPrimary, isPrimary) || other.isPrimary == isPrimary)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.content, content) || other.content == content)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,verifiedAt,isPrimary,content,accountId,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,type,verifiedAt,isPrimary,isPublic,content,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnContactMethod(id: $id, type: $type, verifiedAt: $verifiedAt, isPrimary: $isPrimary, content: $content, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnContactMethod(id: $id, type: $type, verifiedAt: $verifiedAt, isPrimary: $isPrimary, isPublic: $isPublic, content: $content, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -1866,7 +1880,7 @@ abstract mixin class _$SnContactMethodCopyWith<$Res> implements $SnContactMethod
factory _$SnContactMethodCopyWith(_SnContactMethod value, $Res Function(_SnContactMethod) _then) = __$SnContactMethodCopyWithImpl;
@override @useResult
$Res call({
String id, int type, DateTime? verifiedAt, bool isPrimary, String content, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, int type, DateTime? verifiedAt, bool isPrimary, bool isPublic, String content, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -1883,12 +1897,13 @@ class __$SnContactMethodCopyWithImpl<$Res>
/// Create a copy of SnContactMethod
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? verifiedAt = freezed,Object? isPrimary = null,Object? content = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? verifiedAt = freezed,Object? isPrimary = null,Object? isPublic = null,Object? content = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnContactMethod(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,verifiedAt: freezed == verifiedAt ? _self.verifiedAt : verifiedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isPrimary: null == isPrimary ? _self.isPrimary : isPrimary // ignore: cast_nullable_to_non_nullable
as bool,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable
as bool,content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String,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

View File

@@ -11,6 +11,7 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
name: json['name'] as String,
nick: json['nick'] as String,
language: json['language'] as String,
region: json['region'] as String? ?? "",
isSuperuser: json['is_superuser'] as bool,
automatedId: json['automated_id'] as String?,
profile: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
@@ -25,6 +26,11 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
?.map((e) => SnAccountBadge.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
contacts:
(json['contacts'] as List<dynamic>?)
?.map((e) => SnContactMethod.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
@@ -39,11 +45,13 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
'name': instance.name,
'nick': instance.nick,
'language': instance.language,
'region': instance.region,
'is_superuser': instance.isSuperuser,
'automated_id': instance.automatedId,
'profile': instance.profile.toJson(),
'perk_subscription': instance.perkSubscription?.toJson(),
'badges': instance.badges.map((e) => e.toJson()).toList(),
'contacts': instance.contacts.map((e) => e.toJson()).toList(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
@@ -227,6 +235,7 @@ _SnContactMethod _$SnContactMethodFromJson(Map<String, dynamic> json) =>
? null
: DateTime.parse(json['verified_at'] as String),
isPrimary: json['is_primary'] as bool,
isPublic: json['is_public'] as bool,
content: json['content'] as String,
accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
@@ -243,6 +252,7 @@ Map<String, dynamic> _$SnContactMethodToJson(_SnContactMethod instance) =>
'type': instance.type,
'verified_at': instance.verifiedAt?.toIso8601String(),
'is_primary': instance.isPrimary,
'is_public': instance.isPublic,
'content': instance.content,
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),

View File

@@ -4,6 +4,20 @@ import 'package:island/models/account.dart';
part 'activity.freezed.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
sealed class SnActivity with _$SnActivity {
const factory SnActivity({

View File

@@ -12,6 +12,281 @@ part of 'activity.dart';
// dart format off
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
mixin _$SnActivity {

View File

@@ -6,6 +6,27 @@ part of 'activity.dart';
// 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(
id: json['id'] as String,
type: json['type'] as String,

View File

@@ -11,6 +11,20 @@ sealed class AppToken with _$AppToken {
_$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
sealed class SnAuthChallenge with _$SnAuthChallenge {
const factory SnAuthChallenge({
@@ -26,7 +40,7 @@ sealed class SnAuthChallenge with _$SnAuthChallenge {
required String ipAddress,
required String userAgent,
required String? nonce,
required String? location,
required GeoIpLocation? location,
required String accountId,
required DateTime createdAt,
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
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
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -305,11 +574,11 @@ abstract mixin class $SnAuthChallengeCopyWith<$Res> {
factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl;
@useResult
$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
@@ -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,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?,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 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?,
));
}
/// 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) {
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 _:
@@ -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) {
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);}
@@ -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) {
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 _:
@@ -509,7 +790,7 @@ class _SnAuthChallenge implements SnAuthChallenge {
@override final String ipAddress;
@override final String userAgent;
@override final String? nonce;
@override final String? location;
@override final GeoIpLocation? location;
@override final String accountId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@@ -548,11 +829,11 @@ abstract mixin class _$SnAuthChallengeCopyWith<$Res> implements $SnAuthChallenge
factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl;
@override @useResult
$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
@@ -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,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?,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 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
@@ -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,
};
_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(
id: json['id'] as String,
@@ -30,7 +48,12 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
ipAddress: json['ip_address'] as String,
userAgent: json['user_agent'] 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,
createdAt: DateTime.parse(json['created_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,
'user_agent': instance.userAgent,
'nonce': instance.nonce,
'location': instance.location,
'location': instance.location?.toJson(),
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),

View File

@@ -22,6 +22,7 @@ sealed class SnPost with _$SnPost {
String? slug,
@Default(0) int type,
Map<String, dynamic>? meta,
SnPostEmbedView? embedView,
@Default(0) int viewsUnique,
@Default(0) int viewsTotal,
@Default(0) int upvotes,
@@ -105,3 +106,20 @@ const Map<String, ReactInfo> kReactionTemplates = {
'pray': ReactInfo(icon: '🙏', attitude: 0),
'heart': ReactInfo(icon: '❤️', attitude: 0),
};
enum PostEmbedViewRenderer {
@JsonValue(0)
webView,
}
@freezed
sealed class SnPostEmbedView with _$SnPostEmbedView {
const factory SnPostEmbedView({
required String uri,
double? aspectRatio,
@Default(PostEmbedViewRenderer.webView) PostEmbedViewRenderer renderer,
}) = _SnPostEmbedView;
factory SnPostEmbedView.fromJson(Map<String, dynamic> json) =>
_$SnPostEmbedViewFromJson(json);
}

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPost {
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; String? get slug; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; int? get pinMode; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; String? get realmId; SnRealm? get realm; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<SnPostTag> get tags; List<SnPostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated;
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; String? get slug; int get type; Map<String, dynamic>? get meta; SnPostEmbedView? get embedView; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; int? get pinMode; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; String? get realmId; SnRealm? get realm; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<SnPostTag> get tags; List<SnPostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated;
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $SnPostCopyWith<SnPost> get copyWith => _$SnPostCopyWithImpl<SnPost>(this as SnP
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactionsMade, reactionsMade)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.embedView, embedView) || other.embedView == embedView)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactionsMade, reactionsMade)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactionsMade),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]);
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(meta),embedView,viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactionsMade),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]);
@override
String toString() {
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, embedView: $embedView, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
}
@@ -48,11 +48,11 @@ abstract mixin class $SnPostCopyWith<$Res> {
factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl;
@useResult
$Res call({
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, SnPostEmbedView? embedView, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
});
$SnPostCopyWith<$Res>? get threadedPost;$SnPostCopyWith<$Res>? get repliedPost;$SnPostCopyWith<$Res>? get forwardedPost;$SnRealmCopyWith<$Res>? get realm;$SnPublisherCopyWith<$Res> get publisher;
$SnPostEmbedViewCopyWith<$Res>? get embedView;$SnPostCopyWith<$Res>? get threadedPost;$SnPostCopyWith<$Res>? get repliedPost;$SnPostCopyWith<$Res>? get forwardedPost;$SnRealmCopyWith<$Res>? get realm;$SnPublisherCopyWith<$Res> get publisher;
}
/// @nodoc
@@ -65,7 +65,7 @@ class _$SnPostCopyWithImpl<$Res>
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? embedView = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
@@ -78,7 +78,8 @@ as int,content: freezed == content ? _self.content : content // ignore: cast_nul
as String?,slug: freezed == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,viewsUnique: null == viewsUnique ? _self.viewsUnique : viewsUnique // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,embedView: freezed == embedView ? _self.embedView : embedView // ignore: cast_nullable_to_non_nullable
as SnPostEmbedView?,viewsUnique: null == viewsUnique ? _self.viewsUnique : viewsUnique // ignore: cast_nullable_to_non_nullable
as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: cast_nullable_to_non_nullable
as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable
as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable
@@ -111,6 +112,18 @@ as bool,
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostEmbedViewCopyWith<$Res>? get embedView {
if (_self.embedView == null) {
return null;
}
return $SnPostEmbedViewCopyWith<$Res>(_self.embedView!, (value) {
return _then(_self.copyWith(embedView: value));
});
}/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostCopyWith<$Res>? get threadedPost {
if (_self.threadedPost == null) {
return null;
@@ -243,10 +256,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, SnPostEmbedView? embedView, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPost() when $default != null:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.embedView,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
return orElse();
}
@@ -264,10 +277,10 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, SnPostEmbedView? embedView, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated) $default,) {final _that = this;
switch (_that) {
case _SnPost():
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);}
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.embedView,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -281,10 +294,10 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, SnPostEmbedView? embedView, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,) {final _that = this;
switch (_that) {
case _SnPost() when $default != null:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.embedView,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
return null;
}
@@ -296,7 +309,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
@JsonSerializable()
class _SnPost implements SnPost {
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.slug, this.type = 0, final Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.pinMode, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, this.realmId, this.realm, final List<SnCloudFile> attachments = const [], required this.publisher, final Map<String, int> reactionsCount = const {}, final Map<String, bool> reactionsMade = const {}, final List<dynamic> reactions = const [], final List<SnPostTag> tags = const [], final List<SnPostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.slug, this.type = 0, final Map<String, dynamic>? meta, this.embedView, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.pinMode, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, this.realmId, this.realm, final List<SnCloudFile> attachments = const [], required this.publisher, final Map<String, int> reactionsCount = const {}, final Map<String, bool> reactionsMade = const {}, final List<dynamic> reactions = const [], final List<SnPostTag> tags = const [], final List<SnPostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
@override final String id;
@@ -318,6 +331,7 @@ class _SnPost implements SnPost {
return EqualUnmodifiableMapView(value);
}
@override final SnPostEmbedView? embedView;
@override@JsonKey() final int viewsUnique;
@override@JsonKey() final int viewsTotal;
@override@JsonKey() final int upvotes;
@@ -400,16 +414,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactionsMade, _reactionsMade)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.embedView, embedView) || other.embedView == embedView)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactionsMade, _reactionsMade)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactionsMade),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]);
int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(_meta),embedView,viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactionsMade),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]);
@override
String toString() {
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, embedView: $embedView, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)';
}
@@ -420,11 +434,11 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl;
@override @useResult
$Res call({
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, SnPostEmbedView? embedView, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
});
@override $SnPostCopyWith<$Res>? get threadedPost;@override $SnPostCopyWith<$Res>? get repliedPost;@override $SnPostCopyWith<$Res>? get forwardedPost;@override $SnRealmCopyWith<$Res>? get realm;@override $SnPublisherCopyWith<$Res> get publisher;
@override $SnPostEmbedViewCopyWith<$Res>? get embedView;@override $SnPostCopyWith<$Res>? get threadedPost;@override $SnPostCopyWith<$Res>? get repliedPost;@override $SnPostCopyWith<$Res>? get forwardedPost;@override $SnRealmCopyWith<$Res>? get realm;@override $SnPublisherCopyWith<$Res> get publisher;
}
/// @nodoc
@@ -437,7 +451,7 @@ class __$SnPostCopyWithImpl<$Res>
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? embedView = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
return _then(_SnPost(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
@@ -450,7 +464,8 @@ as int,content: freezed == content ? _self.content : content // ignore: cast_nul
as String?,slug: freezed == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,viewsUnique: null == viewsUnique ? _self.viewsUnique : viewsUnique // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,embedView: freezed == embedView ? _self.embedView : embedView // ignore: cast_nullable_to_non_nullable
as SnPostEmbedView?,viewsUnique: null == viewsUnique ? _self.viewsUnique : viewsUnique // ignore: cast_nullable_to_non_nullable
as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: cast_nullable_to_non_nullable
as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable
as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable
@@ -484,6 +499,18 @@ as bool,
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostEmbedViewCopyWith<$Res>? get embedView {
if (_self.embedView == null) {
return null;
}
return $SnPostEmbedViewCopyWith<$Res>(_self.embedView!, (value) {
return _then(_self.copyWith(embedView: value));
});
}/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostCopyWith<$Res>? get threadedPost {
if (_self.threadedPost == null) {
return null;
@@ -1324,6 +1351,269 @@ as int,
}
}
/// @nodoc
mixin _$SnPostEmbedView {
String get uri; double? get aspectRatio; PostEmbedViewRenderer get renderer;
/// Create a copy of SnPostEmbedView
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnPostEmbedViewCopyWith<SnPostEmbedView> get copyWith => _$SnPostEmbedViewCopyWithImpl<SnPostEmbedView>(this as SnPostEmbedView, _$identity);
/// Serializes this SnPostEmbedView to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostEmbedView&&(identical(other.uri, uri) || other.uri == uri)&&(identical(other.aspectRatio, aspectRatio) || other.aspectRatio == aspectRatio)&&(identical(other.renderer, renderer) || other.renderer == renderer));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,uri,aspectRatio,renderer);
@override
String toString() {
return 'SnPostEmbedView(uri: $uri, aspectRatio: $aspectRatio, renderer: $renderer)';
}
}
/// @nodoc
abstract mixin class $SnPostEmbedViewCopyWith<$Res> {
factory $SnPostEmbedViewCopyWith(SnPostEmbedView value, $Res Function(SnPostEmbedView) _then) = _$SnPostEmbedViewCopyWithImpl;
@useResult
$Res call({
String uri, double? aspectRatio, PostEmbedViewRenderer renderer
});
}
/// @nodoc
class _$SnPostEmbedViewCopyWithImpl<$Res>
implements $SnPostEmbedViewCopyWith<$Res> {
_$SnPostEmbedViewCopyWithImpl(this._self, this._then);
final SnPostEmbedView _self;
final $Res Function(SnPostEmbedView) _then;
/// Create a copy of SnPostEmbedView
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? uri = null,Object? aspectRatio = freezed,Object? renderer = null,}) {
return _then(_self.copyWith(
uri: null == uri ? _self.uri : uri // ignore: cast_nullable_to_non_nullable
as String,aspectRatio: freezed == aspectRatio ? _self.aspectRatio : aspectRatio // ignore: cast_nullable_to_non_nullable
as double?,renderer: null == renderer ? _self.renderer : renderer // ignore: cast_nullable_to_non_nullable
as PostEmbedViewRenderer,
));
}
}
/// Adds pattern-matching-related methods to [SnPostEmbedView].
extension SnPostEmbedViewPatterns on SnPostEmbedView {
/// 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( _SnPostEmbedView value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnPostEmbedView() 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( _SnPostEmbedView value) $default,){
final _that = this;
switch (_that) {
case _SnPostEmbedView():
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( _SnPostEmbedView value)? $default,){
final _that = this;
switch (_that) {
case _SnPostEmbedView() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String uri, double? aspectRatio, PostEmbedViewRenderer renderer)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPostEmbedView() when $default != null:
return $default(_that.uri,_that.aspectRatio,_that.renderer);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String uri, double? aspectRatio, PostEmbedViewRenderer renderer) $default,) {final _that = this;
switch (_that) {
case _SnPostEmbedView():
return $default(_that.uri,_that.aspectRatio,_that.renderer);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String uri, double? aspectRatio, PostEmbedViewRenderer renderer)? $default,) {final _that = this;
switch (_that) {
case _SnPostEmbedView() when $default != null:
return $default(_that.uri,_that.aspectRatio,_that.renderer);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnPostEmbedView implements SnPostEmbedView {
const _SnPostEmbedView({required this.uri, this.aspectRatio, this.renderer = PostEmbedViewRenderer.webView});
factory _SnPostEmbedView.fromJson(Map<String, dynamic> json) => _$SnPostEmbedViewFromJson(json);
@override final String uri;
@override final double? aspectRatio;
@override@JsonKey() final PostEmbedViewRenderer renderer;
/// Create a copy of SnPostEmbedView
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnPostEmbedViewCopyWith<_SnPostEmbedView> get copyWith => __$SnPostEmbedViewCopyWithImpl<_SnPostEmbedView>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnPostEmbedViewToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostEmbedView&&(identical(other.uri, uri) || other.uri == uri)&&(identical(other.aspectRatio, aspectRatio) || other.aspectRatio == aspectRatio)&&(identical(other.renderer, renderer) || other.renderer == renderer));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,uri,aspectRatio,renderer);
@override
String toString() {
return 'SnPostEmbedView(uri: $uri, aspectRatio: $aspectRatio, renderer: $renderer)';
}
}
/// @nodoc
abstract mixin class _$SnPostEmbedViewCopyWith<$Res> implements $SnPostEmbedViewCopyWith<$Res> {
factory _$SnPostEmbedViewCopyWith(_SnPostEmbedView value, $Res Function(_SnPostEmbedView) _then) = __$SnPostEmbedViewCopyWithImpl;
@override @useResult
$Res call({
String uri, double? aspectRatio, PostEmbedViewRenderer renderer
});
}
/// @nodoc
class __$SnPostEmbedViewCopyWithImpl<$Res>
implements _$SnPostEmbedViewCopyWith<$Res> {
__$SnPostEmbedViewCopyWithImpl(this._self, this._then);
final _SnPostEmbedView _self;
final $Res Function(_SnPostEmbedView) _then;
/// Create a copy of SnPostEmbedView
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? uri = null,Object? aspectRatio = freezed,Object? renderer = null,}) {
return _then(_SnPostEmbedView(
uri: null == uri ? _self.uri : uri // ignore: cast_nullable_to_non_nullable
as String,aspectRatio: freezed == aspectRatio ? _self.aspectRatio : aspectRatio // ignore: cast_nullable_to_non_nullable
as double?,renderer: null == renderer ? _self.renderer : renderer // ignore: cast_nullable_to_non_nullable
as PostEmbedViewRenderer,
));
}
}
// dart format on

View File

@@ -24,6 +24,12 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
slug: json['slug'] as String?,
type: (json['type'] as num?)?.toInt() ?? 0,
meta: json['meta'] as Map<String, dynamic>?,
embedView:
json['embed_view'] == null
? null
: SnPostEmbedView.fromJson(
json['embed_view'] as Map<String, dynamic>,
),
viewsUnique: (json['views_unique'] as num?)?.toInt() ?? 0,
viewsTotal: (json['views_total'] as num?)?.toInt() ?? 0,
upvotes: (json['upvotes'] as num?)?.toInt() ?? 0,
@@ -105,6 +111,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'slug': instance.slug,
'type': instance.type,
'meta': instance.meta,
'embed_view': instance.embedView?.toJson(),
'views_unique': instance.viewsUnique,
'views_total': instance.viewsTotal,
'upvotes': instance.upvotes,
@@ -166,3 +173,24 @@ Map<String, dynamic> _$SnSubscriptionStatusToJson(
'publisher_id': instance.publisherId,
'publisher_name': instance.publisherName,
};
_SnPostEmbedView _$SnPostEmbedViewFromJson(Map<String, dynamic> json) =>
_SnPostEmbedView(
uri: json['uri'] as String,
aspectRatio: (json['aspect_ratio'] as num?)?.toDouble(),
renderer:
$enumDecodeNullable(
_$PostEmbedViewRendererEnumMap,
json['renderer'],
) ??
PostEmbedViewRenderer.webView,
);
Map<String, dynamic> _$SnPostEmbedViewToJson(_SnPostEmbedView instance) =>
<String, dynamic>{
'uri': instance.uri,
'aspect_ratio': instance.aspectRatio,
'renderer': _$PostEmbedViewRendererEnumMap[instance.renderer]!,
};
const _$PostEmbedViewRendererEnumMap = {PostEmbedViewRenderer.webView: 0};

View File

@@ -1,7 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/post.dart';
import 'package:island/services/text.dart';
import 'package:island/utils/text.dart';
part 'post_category.freezed.dart';
part 'post_category.g.dart';

View File

@@ -9,6 +9,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:island/pods/network.dart';
import 'package:island/models/chat.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
part 'call.g.dart';
part 'call.freezed.dart';
@@ -54,7 +55,7 @@ sealed class CallParticipantLive with _$CallParticipantLive {
bool get hasAudio => remoteParticipant.hasAudio;
}
@riverpod
@Riverpod(keepAlive: true)
class CallNotifier extends _$CallNotifier {
Room? _room;
LocalParticipant? _localParticipant;
@@ -277,14 +278,27 @@ class CallNotifier extends _$CallNotifier {
// Listen for connection updates
_room!.addListener(() {
final wasConnected = state.isConnected;
final isNowConnected =
_room!.connectionState == ConnectionState.connected;
state = state.copyWith(
isConnected: _room!.connectionState == ConnectionState.connected,
isConnected: isNowConnected,
isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(),
isCameraEnabled: _localParticipant!.isCameraEnabled(),
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);
// Enable wakelock when call connects
WakelockPlus.enable();
} else {
state = state.copyWith(error: 'Failed to join room');
}
@@ -344,6 +358,8 @@ class CallNotifier extends _$CallNotifier {
isCameraEnabled: false,
isScreenSharing: false,
);
// Disable wakelock when call disconnects
WakelockPlus.disable();
}
}
@@ -381,5 +397,7 @@ class CallNotifier extends _$CallNotifier {
_durationTimer?.cancel();
_roomId = null;
participantsVolumes = {};
// Disable wakelock when disposing
WakelockPlus.disable();
}
}

View File

@@ -6,22 +6,19 @@ part of 'call.dart';
// RiverpodGenerator
// **************************************************************************
String _$callNotifierHash() => r'18fb807f067eecd3ea42631c1426c3e5f1fb4280';
String _$callNotifierHash() => r'eb9bd41b97e9b5e9d54007c8327edb6567458846';
/// See also [CallNotifier].
@ProviderFor(CallNotifier)
final callNotifierProvider =
AutoDisposeNotifierProvider<CallNotifier, CallState>.internal(
CallNotifier.new,
name: r'callNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$callNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
final callNotifierProvider = NotifierProvider<CallNotifier, CallState>.internal(
CallNotifier.new,
name: r'callNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$callNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$CallNotifier = AutoDisposeNotifier<CallState>;
typedef _$CallNotifier = Notifier<CallState>;
// 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

View File

@@ -15,10 +15,12 @@ const kNetworkServerStoreKey = 'app_server_url';
const kAppbarTransparentStoreKey = 'app_bar_transparent';
const kAppBackgroundStoreKey = 'app_has_background';
const kAppShowBackgroundImage = 'app_show_background_image';
const kAppColorSchemeStoreKey = 'app_color_scheme';
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const kAppCustomFonts = 'app_custom_fonts';
const kAppAutoTranslate = 'app_auto_translate';
const kAppDataSavingMode = 'app_data_saving_mode';
const kAppSoundEffects = 'app_sound_effects';
const kAppAprilFoolFeatures = 'app_april_fool_features';
const kAppWindowSize = 'app_window_size';
@@ -54,10 +56,12 @@ final serverUrlProvider = Provider<String>((ref) {
sealed class AppSettings with _$AppSettings {
const factory AppSettings({
required bool autoTranslate,
required bool dataSavingMode,
required bool soundEffects,
required bool aprilFoolFeatures,
required bool enterToSend,
required bool appBarTransparent,
required bool showBackgroundImage,
required String? customFonts,
required int? appColorScheme, // The color stored via the int type
required Size? windowSize, // The window size for desktop platforms
@@ -71,10 +75,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
final prefs = ref.watch(sharedPreferencesProvider);
return AppSettings(
autoTranslate: prefs.getBool(kAppAutoTranslate) ?? false,
dataSavingMode: prefs.getBool(kAppDataSavingMode) ?? false,
soundEffects: prefs.getBool(kAppSoundEffects) ?? true,
aprilFoolFeatures: prefs.getBool(kAppAprilFoolFeatures) ?? true,
enterToSend: prefs.getBool(kAppEnterToSend) ?? true,
appBarTransparent: prefs.getBool(kAppbarTransparentStoreKey) ?? false,
showBackgroundImage: prefs.getBool(kAppShowBackgroundImage) ?? true,
customFonts: prefs.getString(kAppCustomFonts),
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
windowSize: _getWindowSizeFromPrefs(prefs),
@@ -104,6 +110,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
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) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppSoundEffects, value);
@@ -129,6 +141,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
ref.read(themeProvider.notifier).reloadTheme();
}
void setShowBackgroundImage(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppShowBackgroundImage, value);
state = state.copyWith(showBackgroundImage: value);
}
void setCustomFonts(String? value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setString(kAppCustomFonts, value ?? '');

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AppSettings {
bool get autoTranslate; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; 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;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@@ -26,16 +26,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
@override
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.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
int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,customFonts,appColorScheme,windowSize);
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize);
@override
String toString() {
return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, 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;
@useResult
$Res call({
bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, 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,13 +63,15 @@ class _$AppSettingsCopyWithImpl<$Res>
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = 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(
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,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,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent : appBarTransparent // ignore: cast_nullable_to_non_nullable
as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundImage : showBackgroundImage // ignore: cast_nullable_to_non_nullable
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
@@ -155,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, 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) {
case _AppSettings() when $default != null:
return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_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();
}
@@ -176,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, 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) {
case _AppSettings():
return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_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`
///
@@ -193,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, 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) {
case _AppSettings() when $default != null:
return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_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;
}
@@ -208,14 +210,16 @@ return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_
class _AppSettings implements AppSettings {
const _AppSettings({required this.autoTranslate, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, 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 dataSavingMode;
@override final bool soundEffects;
@override final bool aprilFoolFeatures;
@override final bool enterToSend;
@override final bool appBarTransparent;
@override final bool showBackgroundImage;
@override final String? customFonts;
@override final int? appColorScheme;
// The color stored via the int type
@@ -231,16 +235,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.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.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
int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,customFonts,appColorScheme,windowSize);
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize);
@override
String toString() {
return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, 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)';
}
@@ -251,7 +255,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
@override @useResult
$Res call({
bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, 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
});
@@ -268,13 +272,15 @@ class __$AppSettingsCopyWithImpl<$Res>
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = 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(
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,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,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent : appBarTransparent // ignore: cast_nullable_to_non_nullable
as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundImage : showBackgroundImage // ignore: cast_nullable_to_non_nullable
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable

View File

@@ -7,7 +7,7 @@ part of 'config.dart';
// **************************************************************************
String _$appSettingsNotifierHash() =>
r'c4f40a3bc4311c6360c2b5e44f8df5e5d7c1bd75';
r'cd18bff2614a94e3523634e6c577cefad0367eba';
/// See also [AppSettingsNotifier].
@ProviderFor(AppSettingsNotifier)

View File

@@ -1,11 +1,9 @@
import 'dart:convert';
import 'dart:developer';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/network.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
part 'translate.freezed.dart';
part 'translate.g.dart';
@@ -29,10 +27,17 @@ Future<String> translateString(Ref ref, TranslateQuery query) async {
@riverpod
String? detectStringLanguage(Ref ref, String text) {
try {
return langdetect.detectLangs(text).firstOrNull?.lang;
} catch (err) {
log('[Language] Unable to detect text\'s language: $text');
return null;
bool isChinese(String text) {
final chineseRegex = RegExp(r'[\u4e00-\u9fff]');
return chineseRegex.hasMatch(text);
}
bool isEnglish(String text) {
final englishRegex = RegExp(r'[a-zA-Z]');
return englishRegex.hasMatch(text) && !isChinese(text);
}
if (isChinese(text)) return "zh";
if (isEnglish(text)) return "en";
return null;
}

View File

@@ -149,7 +149,7 @@ class _TranslateStringProviderElement
}
String _$detectStringLanguageHash() =>
r'697b68464b3d00927cc43ccc1ba8ba93f2a470ed';
r'24fbf52edbbffcc8dc4f09f7206f82d69728e703';
/// See also [detectStringLanguage].
@ProviderFor(detectStringLanguage)

View File

@@ -68,6 +68,7 @@ class AccountScreen extends HookConsumerWidget {
body: SingleChildScrollView(
padding: getTabbedPadding(context),
child: Column(
spacing: 4,
children: <Widget>[
Card(
child: Column(
@@ -112,20 +113,22 @@ class AccountScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
AccountName(
account: user.value!,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
Flexible(
child: AccountName(
account: user.value!,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
Text('@${user.value!.name}'),
Flexible(child: Text('@${user.value!.name}')),
],
),
Text(
(user.value!.profile.bio.isNotEmpty)
? user.value!.profile.bio
: 'No description yet.',
: 'descriptionNone'.tr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -158,8 +161,16 @@ class AccountScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Symbols.draw, size: 28).padding(bottom: 8),
Text('creatorHub').tr().fontSize(16).bold(),
Text('creatorHubDescription').tr(),
Text(
'creatorHub',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).tr().fontSize(16).bold(),
Text(
'creatorHubDescription',
maxLines: 2,
overflow: TextOverflow.ellipsis,
).tr(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {
@@ -176,8 +187,16 @@ class AccountScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Symbols.code, size: 28).padding(bottom: 8),
Text('developerPortal').tr().fontSize(16).bold(),
Text('developerPortalDescription').tr(),
Text(
'developerPortal',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).tr().fontSize(16).bold(),
Text(
'developerPortalDescription',
maxLines: 2,
overflow: TextOverflow.ellipsis,
).tr(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {

View File

@@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/auth.dart';
import 'package:island/models/account.dart';
@@ -14,7 +15,6 @@ import 'package:island/screens/account/me/settings_connections.dart';
import 'package:island/screens/account/me/settings_contacts.dart';
import 'package:island/screens/auth/captcha.dart';
import 'package:island/screens/auth/login.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_devices.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
@@ -57,7 +57,6 @@ class AccountSettingsScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final isDesktop =
!kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
final isWide = isWideScreen(context);
Future<void> requestAccountDeletion() async {
final confirm = await showConfirmAlert(
@@ -440,51 +439,19 @@ class AccountSettingsScreen extends HookConsumerWidget {
// Create a responsive layout based on screen width
Widget buildSettingsList() {
if (isWide) {
// Two-column layout for wide screens
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'accountSecurityTitle',
children: securitySettings,
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'accountDangerZoneTitle',
children: dangerZoneSettings,
),
],
),
),
],
);
} else {
// Single column layout for narrow screens
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'accountSecurityTitle',
children: securitySettings,
),
_SettingsSection(
title: 'accountDangerZoneTitle',
children: dangerZoneSettings,
),
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'accountSecurityTitle',
children: securitySettings,
),
_SettingsSection(
title: 'accountDangerZoneTitle',
children: dangerZoneSettings,
),
],
);
}
return AppScaffold(
@@ -513,6 +480,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
);
},
),
const Gap(8),
]
: null,
),

View File

@@ -21,6 +21,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
const kServerSupportedLanguages = {'en-US': 'en-us', 'zh-CN': 'zh-hans'};
const kServerSupportedRegions = ['US', 'JP', 'CN'];
class UpdateProfileScreen extends HookConsumerWidget {
const UpdateProfileScreen({super.key});
@@ -97,6 +98,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
final usernameController = useTextEditingController(text: user.value!.name);
final nicknameController = useTextEditingController(text: user.value!.nick);
final language = useState(user.value!.language);
final region = useState(user.value!.region);
final links = useState<List<ProfileLink>>(user.value!.profile.links);
void updateBasicInfo() async {
@@ -111,6 +113,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
'name': usernameController.text,
'nick': nicknameController.text,
'language': language.value,
'region': region.value,
},
);
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(
alignment: Alignment.centerRight,
child: TextButton.icon(

View File

@@ -1,14 +1,16 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/auth.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/account/me/account_settings.dart';
import 'package:island/screens/auth/oidc.native.dart';
import 'package:island/services/text.dart';
import 'package:island/utils/text.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
@@ -16,6 +18,7 @@ import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
// Helper function to get provider icon and localized name
Widget getProviderIcon(String provider, {double size = 24, Color? color}) {
@@ -165,9 +168,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
scopes: [AppleIDAuthorizationScopes.email],
webAuthenticationOptions: WebAuthenticationOptions(
clientId: 'dev.solsynth.solarpass',
redirectUri: Uri.parse(
'https://id.solian.app/auth/callback/apple',
),
redirectUri: Uri.parse('https://id.solian.app/auth/callback'),
),
);
@@ -195,17 +196,25 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
case 'github':
case 'discord':
case 'afdian':
await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
builder:
(context) => OidcScreen(
provider: selectedProvider.value.toLowerCase(),
title:
'Connect with ${selectedProvider.value.capitalizeEachWord()}',
),
),
);
if (context.mounted) Navigator.pop(context, true);
if (kIsWeb) {
final serverUrl = ref.watch(serverUrlProvider);
final accessToken = ref.watch(tokenProvider);
launchUrlString(
'$serverUrl/id/auth/login/${selectedProvider.value}?tk=${accessToken!.token}',
);
} else {
await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
builder:
(context) => OidcScreen(
provider: selectedProvider.value.toLowerCase(),
title:
'Connect with ${selectedProvider.value.capitalizeEachWord()}',
),
),
);
if (context.mounted) Navigator.pop(context, true);
}
break;
default:
showSnackBar('accountConnectionAddError'.tr());

View File

@@ -62,6 +62,32 @@ class ContactMethodSheet extends HookConsumerWidget {
}
}
Future<void> makeContactMethodPublic() async {
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.post('/id/accounts/me/contacts/${contact.id}/public');
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> makeContactMethodPrivate() async {
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.delete('/id/accounts/me/contacts/${contact.id}/public');
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
return SheetScaffold(
titleText: 'contactMethod'.tr(),
child: Column(
@@ -111,6 +137,27 @@ class ContactMethodSheet extends HookConsumerWidget {
backgroundColor: Theme.of(context).colorScheme.tertiary,
),
),
if (contact.isPublic)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Badge(
label: Text('contactMethodPublic'.tr()),
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
),
if (!contact.isPublic)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Badge(
label: Text('contactMethodPrivate'.tr()),
textColor: Theme.of(context).colorScheme.onSurface,
backgroundColor:
Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
),
],
),
],
@@ -130,6 +177,20 @@ class ContactMethodSheet extends HookConsumerWidget {
onTap: setContactMethodAsPrimary,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
if (contact.verifiedAt != null && !contact.isPublic)
ListTile(
leading: const Icon(Symbols.public),
title: Text('contactMethodMakePublic').tr(),
onTap: makeContactMethodPublic,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
if (contact.verifiedAt != null && contact.isPublic)
ListTile(
leading: const Icon(Symbols.visibility_off),
title: Text('contactMethodMakePrivate').tr(),
onTap: makeContactMethodPrivate,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
ListTile(
leading: const Icon(Symbols.delete),
title: Text('contactMethodDelete').tr(),

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 _$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: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
@@ -99,7 +100,10 @@ class RelationshipListTile extends StatelessWidget {
return ListTile(
contentPadding: const EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(fileId: account.profile.picture?.id),
leading: AccountPfcGestureDetector(
uname: account.name,
child: ProfilePictureWidget(fileId: account.profile.picture?.id),
),
title: Row(
spacing: 6,
children: [

View File

@@ -700,45 +700,48 @@ class _LoginLookupScreen extends HookConsumerWidget {
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: isBusy.value ? null : (_) => performNewTicket(),
).padding(horizontal: 7),
Row(
spacing: 6,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("loginOr").tr().fontSize(11).opacity(0.85),
const Gap(8),
Spacer(),
IconButton.filledTonal(
onPressed: () => withOidc('github'),
padding: EdgeInsets.zero,
icon: getProviderIcon(
"github",
size: 16,
color: Theme.of(context).colorScheme.onPrimaryContainer,
if (!kIsWeb)
Row(
spacing: 6,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("loginOr").tr().fontSize(11).opacity(0.85),
const Gap(8),
Spacer(),
IconButton.filledTonal(
onPressed: () => withOidc('github'),
padding: EdgeInsets.zero,
icon: getProviderIcon(
"github",
size: 16,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
tooltip: 'GitHub',
),
tooltip: 'GitHub',
),
IconButton.filledTonal(
onPressed: () => withOidc('google'),
padding: EdgeInsets.zero,
icon: getProviderIcon(
"google",
size: 16,
color: Theme.of(context).colorScheme.onPrimaryContainer,
IconButton.filledTonal(
onPressed: () => withOidc('google'),
padding: EdgeInsets.zero,
icon: getProviderIcon(
"google",
size: 16,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
tooltip: 'Google',
),
tooltip: 'Google',
),
IconButton.filledTonal(
onPressed: withApple,
padding: EdgeInsets.zero,
icon: getProviderIcon(
"apple",
size: 16,
color: Theme.of(context).colorScheme.onPrimaryContainer,
IconButton.filledTonal(
onPressed: withApple,
padding: EdgeInsets.zero,
icon: getProviderIcon(
"apple",
size: 16,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
tooltip: 'Apple Account',
),
tooltip: 'Apple Account',
),
],
).padding(horizontal: 8, vertical: 8),
],
).padding(horizontal: 8, vertical: 8)
else
const Gap(12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

View File

@@ -4,6 +4,7 @@ import "dart:developer" as developer;
import "dart:io";
import "package:dio/dio.dart";
import "package:easy_localization/easy_localization.dart";
import "package:file_picker/file_picker.dart";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import "package:go_router/go_router.dart";
@@ -1251,26 +1252,32 @@ class ChatRoomScreen extends HookConsumerWidget {
}, [id]);
Future<void> pickPhotoMedia() async {
final result = await ref
.watch(imagePickerProvider)
.pickMultiImage(requestFullMetadata: true);
if (result.isEmpty) return;
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: true,
allowCompression: false,
);
if (result == null || result.count == 0) return;
attachments.value = [
...attachments.value,
...result.map(
(e) => UniversalFile(data: e, type: UniversalFileType.image),
...result.files.map(
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image),
),
];
}
Future<void> pickVideoMedia() async {
final result = await ref
.watch(imagePickerProvider)
.pickVideo(source: ImageSource.gallery);
if (result == null) return;
final result = await FilePicker.platform.pickFiles(
type: FileType.video,
allowMultiple: true,
allowCompression: false,
);
if (result == null || result.count == 0) return;
attachments.value = [
...attachments.value,
UniversalFile(data: result, type: UniversalFileType.video),
...result.files.map(
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.video),
),
];
}
@@ -1387,6 +1394,8 @@ class ChatRoomScreen extends HookConsumerWidget {
],
);
const messageKeyPrefix = 'message-';
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
SuperListView.builder(
listController: listController,
@@ -1396,7 +1405,9 @@ class ChatRoomScreen extends HookConsumerWidget {
itemCount: messageList.length,
findChildIndexCallback: (key) {
final valueKey = key as ValueKey;
final messageId = valueKey.value as String;
final messageId = (valueKey.value as String).substring(
messageKeyPrefix.length,
);
return messageList.indexWhere((m) => m.id == messageId);
},
extentEstimation: (_, _) => 40,
@@ -1413,10 +1424,13 @@ class ChatRoomScreen extends HookConsumerWidget {
.abs() >
3;
final key = ValueKey('$messageKeyPrefix${message.id}');
return chatIdentity.when(
skipError: true,
data:
(identity) => MessageItem(
key: key,
message: message,
isCurrentUser: identity?.id == message.senderId,
onAction: (action) {
@@ -1459,6 +1473,7 @@ class ChatRoomScreen extends HookConsumerWidget {
),
loading:
() => MessageItem(
key: key,
message: message,
isCurrentUser: false,
onAction: null,
@@ -1466,7 +1481,7 @@ class ChatRoomScreen extends HookConsumerWidget {
showAvatar: false,
onJump: (_) {},
),
error: (_, _) => const SizedBox.shrink(),
error: (_, _) => SizedBox.shrink(key: key),
);
},
);

View File

@@ -153,7 +153,9 @@ class ChatDetailScreen extends HookConsumerWidget {
),
ListTile(
title: const Text('chatBreak5m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak5m'.tr()]),
subtitle: const Text(
'chatBreakHour',
).tr(args: ['chatBreak5m'.tr()]),
leading: const Icon(Symbols.circle),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 5)));
@@ -165,7 +167,9 @@ class ChatDetailScreen extends HookConsumerWidget {
),
ListTile(
title: const Text('chatBreak10m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak10m'.tr()]),
subtitle: const Text(
'chatBreakHour',
).tr(args: ['chatBreak10m'.tr()]),
leading: const Icon(Symbols.circle),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 10)));
@@ -177,7 +181,9 @@ class ChatDetailScreen extends HookConsumerWidget {
),
ListTile(
title: const Text('chatBreak15m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak15m'.tr()]),
subtitle: const Text(
'chatBreakHour',
).tr(args: ['chatBreak15m'.tr()]),
leading: const Icon(Symbols.timer_3),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 15)));
@@ -189,7 +195,9 @@ class ChatDetailScreen extends HookConsumerWidget {
),
ListTile(
title: const Text('chatBreak30m').tr(),
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak30m'.tr()]),
subtitle: const Text(
'chatBreakHour',
).tr(args: ['chatBreak30m'.tr()]),
leading: const Icon(Symbols.timer),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 30)));
@@ -247,7 +255,10 @@ class ChatDetailScreen extends HookConsumerWidget {
return AppScaffold(
body: roomState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))),
error:
(error, _) => Center(
child: Text('errorGeneric'.tr(args: [error.toString()])),
),
data:
(currentRoom) => CustomScrollView(
slivers: [
@@ -375,12 +386,26 @@ class ChatDetailScreen extends HookConsumerWidget {
trailing: const Icon(Symbols.chevron_right),
title: const Text('searchMessages').tr(),
subtitle: totalMessages.when(
data: (count) => Text('messagesCount'.tr(args: [count.toString()])),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('errorGeneric'.tr(args: [err.toString()])),
data:
(count) => Text(
'messagesCount'.tr(
args: [count.toString()],
),
),
loading:
() => const CircularProgressIndicator(),
error:
(err, stack) => Text(
'errorGeneric'.tr(
args: [err.toString()],
),
),
),
onTap: () {
context.pushNamed('searchMessages', pathParameters: {'id': id});
context.pushNamed(
'searchMessages',
pathParameters: {'id': id},
);
},
),
],
@@ -702,7 +727,11 @@ class _ChatMemberListSheet extends HookConsumerWidget {
children: [
Flexible(child: Text(member.account.nick)),
if (member.status != null)
AccountStatusLabel(status: member.status!),
AccountStatusLabel(
status: member.status!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (member.joinedAt == null)
const Icon(Symbols.pending_actions, size: 20),
],
@@ -716,7 +745,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
? 'permissionModerator'
: 'permissionMember',
).tr(),
Text('dotSeparator').bold().padding(horizontal: 6),
Text('·').bold().padding(horizontal: 6),
Expanded(child: Text("@${member.account.name}")),
],
),

View File

@@ -7,7 +7,7 @@ part of 'room_detail.dart';
// **************************************************************************
String _$totalMessagesCountHash() =>
r'a15c03461f25c2d4d39c0926509bf626ae2550a6';
r'd55f1507aba2acdce5e468c1c2e15dba7640c571';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -11,7 +11,7 @@ import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/services/responsive.dart';
import 'package:island/services/text.dart';
import 'package:island/utils/text.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';

View File

@@ -9,6 +9,7 @@ import 'package:island/widgets/poll/poll_feedback.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
part 'poll_list.g.dart';
@@ -86,7 +87,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
onPressed: () => _createPoll(context),
child: const Icon(Icons.add),
),
body: RefreshIndicator(
body: ExtendedRefreshIndicator(
onRefresh: () => ref.refresh(pollListNotifierProvider(pubName).future),
child: CustomScrollView(
slivers: [

View File

@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:island/pods/webfeed.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/empty_state.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
import 'package:material_symbols_icons/symbols.dart';
class WebFeedListScreen extends ConsumerWidget {
@@ -20,7 +21,10 @@ class WebFeedListScreen extends ConsumerWidget {
floatingActionButton: FloatingActionButton(
child: const Icon(Symbols.add),
onPressed: () {
context.pushNamed('creatorFeedNew', pathParameters: {'name': pubName});
context.pushNamed(
'creatorFeedNew',
pathParameters: {'name': pubName},
);
},
),
body: feedsAsync.when(
@@ -32,7 +36,7 @@ class WebFeedListScreen extends ConsumerWidget {
description: 'Add a new web feed to get started',
);
}
return RefreshIndicator(
return ExtendedRefreshIndicator(
onRefresh: () => ref.refresh(webFeedListProvider(pubName).future),
child: ListView.builder(
padding: EdgeInsets.only(top: 8),
@@ -62,7 +66,10 @@ class WebFeedListScreen extends ConsumerWidget {
),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context.pushNamed('creatorFeedEdit', pathParameters: {'name': pubName, 'feedId': feed.id});
context.pushNamed(
'creatorFeedEdit',
pathParameters: {'name': pubName, 'feedId': feed.id},
);
},
),
);

View File

@@ -9,6 +9,7 @@ import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
part 'bots.g.dart';
@@ -60,7 +61,7 @@ class BotsScreen extends HookConsumerWidget {
),
);
}
return RefreshIndicator(
return ExtendedRefreshIndicator(
onRefresh:
() => ref.refresh(botsProvider(publisherName, projectId).future),
child: ListView.builder(

View File

@@ -72,7 +72,7 @@ class MarketplaceWebFeedsScreen extends HookConsumerWidget {
searchController.clear();
}
return null;
}, [query.value]);
}, [query]);
// Clean up timer on dispose
useEffect(() {

View File

@@ -27,6 +27,7 @@ import 'package:island/pods/network.dart';
import 'package:island/widgets/realm/realm_card.dart';
import 'package:island/widgets/publisher/publisher_card.dart';
import 'package:island/widgets/web_article_card.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
import 'package:styled_widget/styled_widget.dart';
part 'explore.g.dart';
@@ -368,7 +369,7 @@ class ExploreScreen extends HookConsumerWidget {
final isWide = isWideScreen(context);
return RefreshIndicator(
return ExtendedRefreshIndicator(
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
child: PagingHelperView(
provider: activityListNotifierProvider(filter),
@@ -399,6 +400,69 @@ class _DiscoveryActivityItem extends StatelessWidget {
final items = data['items'] as List;
final type = items.firstOrNull?['type'] ?? 'unknown';
var flexWeights = isWideScreen(context) ? <int>[3, 2, 1] : <int>[4, 1];
if (type == 'post') flexWeights = <int>[3, 2];
final height = type == 'post' ? 280.0 : 180.0;
final contentWidget = switch (type) {
'post' => ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: items.length,
separatorBuilder: (context, index) => const Gap(12),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
itemBuilder: (context, index) {
final item = items[index];
return Container(
width: 320,
decoration: BoxDecoration(
border: Border.all(
width: 1 / MediaQuery.of(context).devicePixelRatio,
color: Theme.of(context).dividerColor.withOpacity(0.5),
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: SingleChildScrollView(
child: PostActionableItem(
item: SnPost.fromJson(item['data']),
isCompact: true,
),
),
),
);
},
),
_ => CarouselView.weighted(
flexWeights: flexWeights,
consumeMaxWeight: false,
enableSplash: false,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
itemSnapping: false,
children: [
for (final item in items)
switch (type) {
'realm' => RealmCard(
realm: SnRealm.fromJson(item['data']),
maxWidth: 280,
),
'publisher' => PublisherCard(
publisher: SnPublisher.fromJson(item['data']),
maxWidth: 280,
),
'article' => WebArticleCard(
article: SnWebArticle.fromJson(item['data']),
maxWidth: 280,
),
_ => const Placeholder(),
},
],
),
};
return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
@@ -407,13 +471,20 @@ class _DiscoveryActivityItem extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.explore, size: 19),
Icon(switch (type) {
'realm' => Symbols.public,
'publisher' => Symbols.account_circle,
'article' => Symbols.auto_stories,
'post' => Symbols.shuffle,
_ => Symbols.explore,
}, size: 19),
const Gap(8),
Text(
(switch (type) {
'realm' => 'discoverRealms',
'publisher' => 'discoverPublishers',
'article' => 'discoverWebArticles',
'post' => 'discoverShuffledPost',
_ => 'unknown',
}).tr(),
style: Theme.of(context).textTheme.titleMedium,
@@ -421,37 +492,8 @@ class _DiscoveryActivityItem extends StatelessWidget {
],
).padding(horizontal: 20, top: 8, bottom: 4),
SizedBox(
height: 180,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: CarouselView.weighted(
flexWeights:
isWideScreen(context) ? <int>[3, 2, 1] : <int>[4, 1],
consumeMaxWeight: false,
enableSplash: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
children: [
for (final item in items)
switch (type) {
'realm' => RealmCard(
realm: SnRealm.fromJson(item['data']),
maxWidth: 280,
),
'publisher' => PublisherCard(
publisher: SnPublisher.fromJson(item['data']),
maxWidth: 280,
),
'article' => WebArticleCard(
article: SnWebArticle.fromJson(item['data']),
maxWidth: 280,
),
_ => Placeholder(),
},
],
),
),
height: height,
child: contentWidget,
).padding(bottom: 8, horizontal: 8),
],
),
@@ -569,7 +611,8 @@ class ActivityListNotifier extends _$ActivityListNotifier
if (cursor != null) 'cursor': cursor,
'take': take,
if (filter != null) 'filter': filter,
if (kDebugMode) 'debugInclude': 'realms,publishers,articles',
if (kDebugMode)
'debugInclude': 'realms,publishers,articles,shuffledPosts',
};
final response = await client.get(
@@ -584,12 +627,13 @@ class ActivityListNotifier extends _$ActivityListNotifier
final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
final nextCursor =
items
.map((x) => x.createdAt)
.lastOrNull
?.toUtc()
.toIso8601String()
.toString();
items.isNotEmpty
? items
.map((x) => x.createdAt)
.reduce((a, b) => a.isBefore(b) ? a : b)
.toUtc()
.toIso8601String()
: null;
return CursorPagingData(
items: items,

View File

@@ -7,7 +7,7 @@ part of 'explore.dart';
// **************************************************************************
String _$activityListNotifierHash() =>
r'b75fd5c08d5f84ca433e16b7387d317ea72b91c9';
r'167021cada54da7c8d8437eef1ffb387a92ea2e3';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -1,15 +1,26 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.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/extended_refresh_indicator.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_replies.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: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 {
final String id;
const PostDetailScreen({super.key, required this.id});
@@ -66,29 +392,58 @@ class PostDetailScreen extends HookConsumerWidget {
return Stack(
fit: StackFit.expand,
children: [
CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 600),
child: PostItem(
item: post!,
isFullPost: true,
isEmbedReply: false,
onUpdate: (newItem) {
// Update the local state with the new post data
ref
.read(postStateProvider(id).notifier)
.updatePost(newItem);
},
ExtendedRefreshIndicator(
onRefresh: () async {
ref.invalidate(postProvider(id));
ref.invalidate(postRepliesNotifierProvider(id));
},
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 600),
child: PostItem(
item: post!,
isFullPost: true,
isEmbedReply: false,
onUpdate: (newItem) {
// Update the local state with the new post data
ref
.read(postStateProvider(id).notifier)
.updatePost(newItem);
},
),
),
),
),
),
PostRepliesList(postId: id, maxWidth: 600),
SliverGap(MediaQuery.of(context).padding.bottom + 80),
],
SliverToBoxAdapter(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 600),
child: PostActionButtons(
post: post,
renderingPadding: const EdgeInsets.symmetric(
horizontal: 8,
),
onRefresh: () {
ref.invalidate(postProvider(id));
ref.invalidate(postRepliesNotifierProvider(id));
},
onUpdate: (newItem) {
ref
.read(postStateProvider(id).notifier)
.updatePost(newItem);
},
),
),
),
),
PostRepliesList(postId: id, maxWidth: 600),
SliverGap(MediaQuery.of(context).padding.bottom + 80),
],
),
),
if (user.value != null)
Positioned(
@@ -126,7 +481,7 @@ class PostDetailScreen extends HookConsumerWidget {
error:
(e, _) => ResponseErrorWidget(
error: e,
onRetry: () => ref.invalidate(postStateProvider(id)),
onRetry: () => ref.invalidate(postProvider(id)),
),
),
);

View File

@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
@@ -7,6 +9,7 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/response.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
final postSearchNotifierProvider = StateNotifierProvider.autoDispose<
PostSearchNotifier,
@@ -18,6 +21,13 @@ class PostSearchNotifier
final AutoDisposeRef ref;
static const int _pageSize = 20;
String _currentQuery = '';
String? _pubName;
String? _realm;
int? _type;
List<String>? _categories;
List<String>? _tags;
bool _shuffle = false;
bool? _pinned;
bool _isLoading = false;
PostSearchNotifier(this.ref) : super(const AsyncValue.loading()) {
@@ -26,11 +36,38 @@ class PostSearchNotifier
);
}
Future<void> search(String query) async {
Future<void> search(
String query, {
String? pubName,
String? realm,
int? type,
List<String>? categories,
List<String>? tags,
bool shuffle = false,
bool? pinned,
}) async {
if (_isLoading) return;
_currentQuery = query.trim();
if (_currentQuery.isEmpty) {
_pubName = pubName;
_realm = realm;
_type = type;
_categories = categories;
_tags = tags;
_shuffle = shuffle;
_pinned = pinned;
// Allow search even with empty query if any filters are applied
final hasFilters =
pubName != null ||
realm != null ||
type != null ||
categories != null ||
tags != null ||
shuffle ||
pinned != null;
if (_currentQuery.isEmpty && !hasFilters) {
state = AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
);
@@ -57,6 +94,13 @@ class PostSearchNotifier
'offset': offset,
'take': _pageSize,
'vector': false,
if (_pubName != null) 'pub': _pubName,
if (_realm != null) 'realm': _realm,
if (_type != null) 'type': _type,
if (_tags != null) 'tags': _tags,
if (_categories != null) 'categories': _categories,
if (_shuffle) 'shuffle': true,
if (_pinned != null) 'pinned': _pinned,
},
);
@@ -80,100 +124,267 @@ class PostSearchNotifier
}
}
class PostSearchScreen extends ConsumerStatefulWidget {
class PostSearchScreen extends HookConsumerWidget {
const PostSearchScreen({super.key});
@override
ConsumerState<PostSearchScreen> createState() => _PostSearchScreenState();
}
Widget build(BuildContext context, WidgetRef ref) {
final searchController = useTextEditingController();
final debounce = useMemoized(() => Duration(milliseconds: 500));
final debounceTimer = useRef<Timer?>(null);
final showFilters = useState(false);
final pubNameController = useTextEditingController();
final realmController = useTextEditingController();
final typeValue = useState<int?>(null);
final selectedCategories = useState<List<String>>([]);
final selectedTags = useState<List<String>>([]);
final shuffleValue = useState(false);
final pinnedValue = useState<bool?>(null);
class _PostSearchScreenState extends ConsumerState<PostSearchScreen> {
final _searchController = TextEditingController();
final _debounce = Duration(milliseconds: 500);
Timer? _debounceTimer;
useEffect(() {
return () {
searchController.dispose();
pubNameController.dispose();
realmController.dispose();
debounceTimer.value?.cancel();
};
}, []);
@override
void dispose() {
_searchController.dispose();
_debounceTimer?.cancel();
super.dispose();
}
void onSearchChanged(String query) {
if (debounceTimer.value?.isActive ?? false) debounceTimer.value!.cancel();
void _onSearchChanged(String query) {
if (_debounceTimer?.isActive ?? false) _debounceTimer!.cancel();
debounceTimer.value = Timer(debounce, () {
ref.read(postSearchNotifierProvider.notifier).search(query);
});
}
_debounceTimer = Timer(_debounce, () {
ref.read(postSearchNotifierProvider.notifier).search(query);
});
}
void onSearchWithFilters(String query) {
if (debounceTimer.value?.isActive ?? false) debounceTimer.value!.cancel();
debounceTimer.value = Timer(debounce, () {
ref
.read(postSearchNotifierProvider.notifier)
.search(
query,
pubName:
pubNameController.text.isNotEmpty
? pubNameController.text
: null,
realm:
realmController.text.isNotEmpty ? realmController.text : null,
type: typeValue.value,
categories:
selectedCategories.value.isNotEmpty
? selectedCategories.value
: null,
tags: selectedTags.value.isNotEmpty ? selectedTags.value : null,
shuffle: shuffleValue.value,
pinned: pinnedValue.value,
);
});
}
void toggleFilters() {
showFilters.value = !showFilters.value;
}
void applyFilters() {
onSearchWithFilters(searchController.text);
}
void clearFilters() {
pubNameController.clear();
realmController.clear();
typeValue.value = null;
selectedCategories.value = [];
selectedTags.value = [];
shuffleValue.value = false;
pinnedValue.value = null;
onSearchChanged(searchController.text);
}
Widget buildFilterPanel() {
return Card(
margin: EdgeInsets.symmetric(vertical: 8, horizontal: 8),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'filters'.tr(),
style: Theme.of(context).textTheme.titleMedium,
).padding(left: 4),
Row(
children: [
TextButton(
onPressed: applyFilters,
child: Text('apply'.tr()),
),
TextButton(
onPressed: clearFilters,
child: Text('clear'.tr()),
),
],
),
],
),
SizedBox(height: 16),
TextField(
controller: pubNameController,
decoration: InputDecoration(
labelText: 'pubName'.tr(),
border: OutlineInputBorder(),
),
onChanged:
(value) => onSearchWithFilters(searchController.text),
),
SizedBox(height: 8),
TextField(
controller: realmController,
decoration: InputDecoration(
labelText: 'realm'.tr(),
border: OutlineInputBorder(),
),
onChanged:
(value) => onSearchWithFilters(searchController.text),
),
SizedBox(height: 8),
Row(
children: [
Checkbox(
value: shuffleValue.value,
onChanged: (value) {
shuffleValue.value = value ?? false;
onSearchWithFilters(searchController.text);
},
),
Text('shuffle'.tr()),
],
),
Row(
children: [
Checkbox(
value: pinnedValue.value ?? false,
onChanged: (value) {
pinnedValue.value = value;
onSearchWithFilters(searchController.text);
},
),
Text('pinned'.tr()),
],
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search posts...',
border: InputBorder.none,
hintStyle: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
title: Row(
children: [
Expanded(
child: TextField(
controller: searchController,
decoration: InputDecoration(
hintText: 'search'.tr(),
border: InputBorder.none,
hintStyle: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
onChanged: onSearchChanged,
onSubmitted: (value) {
onSearchWithFilters(value);
},
autofocus: true,
),
),
),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
onChanged: _onSearchChanged,
onSubmitted: (value) {
ref.read(postSearchNotifierProvider.notifier).search(value);
},
autofocus: true,
IconButton(
icon: Icon(
showFilters.value
? Icons.filter_alt
: Icons.filter_alt_outlined,
),
onPressed: toggleFilters,
tooltip: 'toggleFilters'.tr(),
),
],
),
),
body: Consumer(
builder: (context, ref, child) {
final searchState = ref.watch(postSearchNotifierProvider);
return searchState.when(
data: (data) {
if (data.items.isEmpty && _searchController.text.isNotEmpty) {
return const Center(child: Text('No results found'));
}
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.items.length + (data.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= data.items.length) {
ref
.read(postSearchNotifierProvider.notifier)
.fetch(cursor: data.nextCursor);
return const Center(child: CircularProgressIndicator());
return CustomScrollView(
slivers: [
if (showFilters.value)
SliverToBoxAdapter(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: buildFilterPanel(),
),
),
),
searchState.when(
data: (data) {
if (data.items.isEmpty && searchController.text.isNotEmpty) {
return SliverFillRemaining(
child: Center(child: Text('noResultsFound'.tr())),
);
}
final post = data.items[index];
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 600),
child: Card(
margin: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
if (index >= data.items.length) {
ref
.read(postSearchNotifierProvider.notifier)
.fetch(cursor: data.nextCursor);
return Center(child: CircularProgressIndicator());
}
final post = data.items[index];
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 600),
child: Card(
margin: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
child: PostActionableItem(
item: post,
borderRadius: 8,
),
),
),
child: PostActionableItem(item: post, borderRadius: 8),
),
),
);
}, childCount: data.items.length + (data.hasMore ? 1 : 0)),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(postSearchNotifierProvider),
),
loading:
() => SliverFillRemaining(
child: Center(child: CircularProgressIndicator()),
),
error:
(error, stack) => SliverFillRemaining(
child: ResponseErrorWidget(
error: error,
onRetry:
() => ref.invalidate(postSearchNotifierProvider),
),
),
),
],
);
},
),

View File

@@ -27,6 +27,224 @@ import 'package:styled_widget/styled_widget.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
Future<SnPublisher> publisher(Ref ref, String uname) async {
final apiClient = ref.watch(apiClientProvider);
@@ -132,170 +350,6 @@ class PublisherProfileScreen extends HookConsumerWidget {
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(
data:
(data) => AppScaffold(
@@ -351,7 +405,9 @@ class PublisherProfileScreen extends HookConsumerWidget {
SliverGap(16),
SliverPostList(pubName: name, pinned: true),
SliverToBoxAdapter(
child: publisherCategoryTabWidget(),
child: _PublisherCategoryTabWidget(
categoryTabController: categoryTabController,
),
),
SliverPostList(
key: ValueKey(categoryTab.value),
@@ -377,10 +433,19 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
publisherBasisWidget(data).padding(bottom: 8),
publisherBadgesWidget(data),
publisherVerificationWidget(data),
publisherBioWidget(data),
_PublisherBasisWidget(
data: data,
subStatus: subStatus,
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(
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(
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),
SliverToBoxAdapter(child: publisherCategoryTabWidget()),
SliverToBoxAdapter(
child: _PublisherCategoryTabWidget(
categoryTabController: categoryTabController,
),
),
SliverPostList(
key: ValueKey(categoryTab.value),
pubName: name,

View File

@@ -668,7 +668,13 @@ class _RealmMemberListSheet extends HookConsumerWidget {
title: Row(
spacing: 6,
children: [
Flexible(child: Text(member.account!.nick)),
Flexible(
child: Text(
member.account!.nick,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (member.status != null)
AccountStatusLabel(status: member.status!),
if (member.joinedAt == null)

View File

@@ -23,6 +23,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/realm/realm_list_tile.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
part 'realms.g.dart';
@@ -90,7 +91,7 @@ class RealmListScreen extends HookConsumerWidget {
},
),
floatingActionButtonLocation: TabbedFabLocation(context),
body: RefreshIndicator(
body: ExtendedRefreshIndicator(
child: realms.when(
data:
(value) => Column(

View File

@@ -219,6 +219,33 @@ class SettingsScreen extends HookConsumerWidget {
},
),
// Background image enabled
if (!kIsWeb && docBasepath.value != null)
FutureBuilder<bool>(
future:
File('${docBasepath.value}/$kAppBackgroundImagePath').exists(),
builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) {
return const SizedBox.shrink();
}
return ListTile(
minLeadingWidth: 48,
title: Text('settingsBackgroundImageEnable').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.image),
trailing: Switch(
value: settings.showBackgroundImage,
onChanged: (value) {
ref
.read(appSettingsNotifierProvider.notifier)
.setShowBackgroundImage(value);
},
),
);
},
),
// Clear background image option
if (!kIsWeb && docBasepath.value != null)
FutureBuilder<bool>(
@@ -423,66 +450,25 @@ 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
final desktopSettings =
!isDesktop
? <Widget>[]
: <Widget>[
ListTile(
minLeadingWidth: 48,
title: Text('settingsKeyboardShortcuts').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.keyboard),
onTap: () {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('settingsKeyboardShortcuts').tr(),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ShortcutRow(
shortcut: 'Ctrl+F',
description:
'settingsKeyboardShortcutSearch'.tr(),
),
_ShortcutRow(
shortcut: 'Ctrl+,',
description:
'settingsKeyboardShortcutSettings'.tr(),
),
_ShortcutRow(
shortcut: 'Ctrl+N',
description:
'settingsKeyboardShortcutNewMessage'.tr(),
),
_ShortcutRow(
shortcut: 'Esc',
description:
'settingsKeyboardShortcutCloseDialog'
.tr(),
),
// Add more shortcuts as needed
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('close').tr(),
),
],
),
);
},
trailing: const Icon(Symbols.chevron_right),
),
];
// But nothing for now
final desktopSettings = !isDesktop ? <Widget>[] : <Widget>[];
// Create a responsive layout based on screen width
Widget buildSettingsList() {
@@ -553,34 +539,7 @@ class SettingsScreen extends HookConsumerWidget {
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: Text('settings').tr(),
actions:
isDesktop
? [
IconButton(
icon: const Icon(Symbols.help_outline),
onPressed: () {
// Show help dialog
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('settingsHelp').tr(),
content: Text('settingsHelpContent').tr(),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('close').tr(),
),
],
),
);
},
),
]
: null,
),
appBar: AppBar(title: Text('settings').tr()),
body: Focus(
autofocus: true,
onKeyEvent: (node, event) {
@@ -630,35 +589,3 @@ class _SettingsSection extends StatelessWidget {
);
}
}
// Helper widget for displaying keyboard shortcuts
class _ShortcutRow extends StatelessWidget {
final String shortcut;
final String description;
const _ShortcutRow({required this.shortcut, required this.description});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
),
),
child: Text(shortcut, style: TextStyle(fontFamily: 'monospace')),
),
SizedBox(width: 16),
Text(description),
],
),
);
}
}

View File

@@ -77,7 +77,7 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
searchController.clear();
}
return null;
}, [query.value]);
}, [query]);
// Clean up timer on dispose
useEffect(() {

View File

@@ -0,0 +1,62 @@
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/foundation.dart';
import 'package:tray_manager/tray_manager.dart';
class TrayService {
TrayService._();
static final TrayService _instance = TrayService._();
static TrayService get instance => _instance;
bool _checkPlatformAvalability() {
if (kIsWeb) return false;
if (Platform.isAndroid || Platform.isIOS) return false;
return true;
}
Future<void> initialize(TrayListener listener) async {
if (!_checkPlatformAvalability()) return;
await trayManager.setIcon(
Platform.isWindows
? 'assets/icons/icon.ico'
: 'assets/icons/icon-outline.svg',
);
final menu = Menu(
items: [
MenuItem(key: 'show_window', label: 'Show Window'),
MenuItem.separator(),
MenuItem(key: 'exit_app', label: 'Exit App'),
],
);
await trayManager.setContextMenu(menu);
trayManager.addListener(listener);
}
Future<void> dispose(TrayListener listener) async {
if (!_checkPlatformAvalability()) return;
trayManager.removeListener(listener);
await trayManager.destroy();
}
void handleAction(MenuItem item) {
switch (item.key) {
case 'show_window':
if (appWindow.isVisible) {
appWindow.restore();
} else {
appWindow.show();
}
break;
case 'exit_app':
appWindow.close();
break;
}
}
}

View File

@@ -6,6 +6,7 @@ import 'package:dio/dio.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:island/main.dart';
@@ -16,54 +17,159 @@ import 'package:island/widgets/app_notification.dart';
import 'package:top_snackbar_flutter/top_snack_bar.dart';
import 'package:url_launcher/url_launcher_string.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
void _onAppLifecycleChanged(AppLifecycleState state) {
_appLifecycleState = state;
}
Future<void> initializeLocalNotifications() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings initializationSettingsIOS =
DarwinInitializationSettings();
const DarwinInitializationSettings initializationSettingsMacOS =
DarwinInitializationSettings();
const LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(defaultActionName: 'Open notification');
const WindowsInitializationSettings initializationSettingsWindows =
WindowsInitializationSettings(
appName: 'Island',
appUserModelId: 'dev.solsynth.solian',
guid: 'dev.solsynth.solian',
);
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
macOS: initializationSettingsMacOS,
linux: initializationSettingsLinux,
windows: initializationSettingsWindows,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) async {
final payload = response.payload;
if (payload != null) {
if (payload.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(payload);
} else {
// External URLs
launchUrlString(payload);
}
}
},
);
WidgetsBinding.instance.addObserver(
LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
);
}
class LifecycleEventHandler extends WidgetsBindingObserver {
final void Function(AppLifecycleState) onAppLifecycleChanged;
LifecycleEventHandler({required this.onAppLifecycleChanged});
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
onAppLifecycleChanged(state);
}
}
StreamSubscription<WebSocketPacket> setupNotificationListener(
BuildContext context,
WidgetRef ref,
) {
final ws = ref.watch(websocketProvider);
return ws.dataStream.listen((pkt) {
return ws.dataStream.listen((pkt) async {
if (pkt.type == "notifications.new") {
final notification = SnNotification.fromJson(pkt.data!);
showTopSnackBar(
globalOverlay.currentState!,
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: NotificationCard(notification: notification),
if (_appLifecycleState == AppLifecycleState.resumed) {
// App is focused, show in-app notification
log(
'[Notification] Showing in-app notification: ${notification.title}',
);
showTopSnackBar(
globalOverlay.currentState!,
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: NotificationCard(notification: notification),
),
),
),
onTap: () {
if (notification.meta['action_uri'] != null) {
var uri = notification.meta['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(
notification.meta['action_uri'],
);
} else {
// External URLs
launchUrlString(uri);
onTap: () {
if (notification.meta['action_uri'] != null) {
var uri = notification.meta['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(
notification.meta['action_uri'],
);
} else {
// External URLs
launchUrlString(uri);
}
}
}
},
onDismissed: () {},
dismissType: DismissType.onSwipe,
displayDuration: const Duration(seconds: 5),
snackBarPosition: SnackBarPosition.top,
padding: EdgeInsets.only(
left: 16,
right: 16,
top:
(!kIsWeb &&
(Platform.isMacOS ||
Platform.isWindows ||
Platform.isLinux))
? 28
// ignore: use_build_context_synchronously
: MediaQuery.of(context).padding.top + 16,
bottom: 16,
),
);
},
onDismissed: () {},
dismissType: DismissType.onSwipe,
displayDuration: const Duration(seconds: 5),
snackBarPosition: SnackBarPosition.top,
padding: EdgeInsets.only(
left: 16,
right: 16,
top:
(!kIsWeb &&
(Platform.isMacOS ||
Platform.isWindows ||
Platform.isLinux))
? 28
// ignore: use_build_context_synchronously
: MediaQuery.of(context).padding.top + 16,
bottom: 16,
),
);
} else {
// App is in background, show system notification (only on supported platforms)
if (!kIsWeb && !Platform.isIOS) {
log(
'[Notification] Showing system notification: ${notification.title}',
);
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'channel_id',
'channel_name',
channelDescription: 'channel_description',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
);
const NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
);
await flutterLocalNotificationsPlugin.show(
0,
notification.title,
notification.content,
notificationDetails,
payload: notification.meta['action_uri'] as String?,
);
} else {
log(
'[Notification] Skipping system notification for unsupported platform: ${notification.title}',
);
}
}
}
});
}
@@ -72,7 +178,7 @@ Future<void> subscribePushNotification(
Dio apiClient, {
bool detailedErrors = false,
}) async {
if (Platform.isLinux) {
if (!kIsWeb && Platform.isLinux) {
return;
}
await FirebaseMessaging.instance.requestPermission(

View File

@@ -1,5 +1,11 @@
import 'package:flutter_udid/flutter_udid.dart';
String? _cachedUdid;
Future<String> getUdid() async {
return await FlutterUdid.consistentUdid;
if (_cachedUdid != null) {
return _cachedUdid!;
}
_cachedUdid = await FlutterUdid.consistentUdid;
return _cachedUdid!;
}

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.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:material_symbols_icons/symbols.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:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -180,9 +184,13 @@ class UpdateService {
useRootNavigator: true,
builder: (ctx) {
String? androidUpdateUrl;
String? windowsUpdateUrl;
if (Platform.isAndroid) {
androidUpdateUrl = _getAndroidUpdateUrl(release.assets);
}
if (Platform.isWindows) {
windowsUpdateUrl = _getWindowsUpdateUrl();
}
return _UpdateSheet(
release: release,
onOpen: () async {
@@ -192,6 +200,7 @@ class UpdateService {
}
},
androidUpdateUrl: androidUpdateUrl,
windowsUpdateUrl: windowsUpdateUrl,
useProxy: useProxy, // Pass the useProxy flag
);
},
@@ -211,15 +220,270 @@ class UpdateService {
// Prioritize arm64, then armeabi, then x86_64
if (arm64 != null) {
return arm64.browserDownloadUrl;
return 'https://fs.solsynth.dev/d/official/solian/${arm64.name}';
} else if (armeabi != null) {
return armeabi.browserDownloadUrl;
return 'https://fs.solsynth.dev/d/official/solian/${armeabi.name}';
} else if (x86_64 != null) {
return x86_64.browserDownloadUrl;
return 'https://fs.solsynth.dev/d/official/solian/${x86_64.name}';
}
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.
/// Public so other screens (e.g., About) can manually trigger update checks.
Future<GithubReleaseInfo?> fetchLatestRelease() async {
@@ -277,10 +541,12 @@ class _UpdateSheet extends StatefulWidget {
required this.release,
required this.onOpen,
this.androidUpdateUrl,
this.windowsUpdateUrl,
this.useProxy = false,
});
final String? androidUpdateUrl;
final String? windowsUpdateUrl;
final bool useProxy;
final GithubReleaseInfo release;
final VoidCallback onOpen;
@@ -299,8 +565,11 @@ class _UpdateSheetState extends State<_UpdateSheet> {
}
Future<void> _installUpdate(String url) async {
final downloadUrl =
_useProxy ? 'https://ghfast.top/${Uri.encodeComponent(url)}' : url;
String downloadUrl = url;
if (_useProxy) {
final fileName = url.split('/').last;
downloadUrl = 'https://fs.solsynth.dev/d/rainyun02/solian/$fileName';
}
UpdateModel model = UpdateModel(
downloadUrl,
@@ -350,7 +619,7 @@ class _UpdateSheetState extends State<_UpdateSheet> {
),
if (!kIsWeb && Platform.isAndroid)
SwitchListTile(
title: const Text('Use GitHub Proxy for Download'),
title: const Text('Use secondary source for download'),
value: _useProxy,
onChanged: (value) {
setState(() {
@@ -376,6 +645,25 @@ class _UpdateSheetState extends State<_UpdateSheet> {
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(
child: FilledButton.icon(
onPressed: widget.onOpen,

9
lib/utils/format.dart Normal file
View File

@@ -0,0 +1,9 @@
String formatFileSize(int bytes) {
if (bytes <= 0) return '0 B';
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
}

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

@@ -12,6 +12,7 @@ import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
part 'account_devices.g.dart';
@@ -177,7 +178,7 @@ class AccountSessionSheet extends HookConsumerWidget {
titleText: 'authSessions'.tr(),
child: authDevices.when(
data:
(data) => RefreshIndicator(
(data) => ExtendedRefreshIndicator(
onRefresh:
() => Future.sync(() => ref.invalidate(authDevicesProvider)),
child: ListView.builder(

View File

@@ -37,7 +37,14 @@ class AccountName extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
Flexible(child: Text(account.nick, style: nameStyle)),
Flexible(
child: Text(
account.nick,
style: nameStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (account.perkSubscription != null)
StellarMembershipMark(membership: account.perkSubscription!),
if (account.profile.verification != null)
@@ -162,7 +169,7 @@ class VerificationStatusCard extends StatelessWidget {
size: 32,
color: kVerificationMarkColors[mark.type],
fill: 1,
),
).alignment(Alignment.centerLeft),
const Gap(8),
Text(mark.title ?? 'No title').bold(),
Text(mark.description ?? 'descriptionNone'.tr()),

View File

@@ -111,26 +111,39 @@ class AccountProfileCard extends HookConsumerWidget {
],
),
if (data.profile.timeZone.isNotEmpty && !kIsWeb)
Row(
spacing: 6,
children: [
Icon(
Symbols.alarm,
size: 17,
fill: 1,
).padding(right: 2),
Text(
getTzInfo(
data.profile.timeZone,
).$2.formatCustomGlobal('HH:mm'),
).fontSize(12),
Text(
getTzInfo(
data.profile.timeZone,
).$1.formatOffsetLocal(),
).fontSize(12),
],
).padding(top: 2),
() {
try {
final tzInfo = getTzInfo(data.profile.timeZone);
return Row(
spacing: 6,
children: [
Icon(
Symbols.alarm,
size: 17,
fill: 1,
).padding(right: 2),
Text(
tzInfo.$2.formatCustomGlobal('HH:mm'),
).fontSize(12),
Text(
tzInfo.$1.formatOffsetLocal(),
).fontSize(12),
],
).padding(top: 2);
} catch (e) {
return Row(
spacing: 6,
children: [
Icon(
Symbols.alarm,
size: 17,
fill: 1,
).padding(right: 2),
Text('timezoneNotFound'.tr()).fontSize(12),
],
).padding(top: 2);
}
}(),
if (data.badges.isNotEmpty)
BadgeList(badges: data.badges).padding(top: 12),
LevelingProgressCard(

View File

@@ -46,6 +46,10 @@ class EventDetailsWidget extends StatelessWidget {
size: 12,
fill: 1,
).padding(top: 4, right: 4),
Icon(
tip.isPositive ? Symbols.thumb_up : Symbols.thumb_down,
size: 14,
).padding(top: 2.5),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -60,7 +60,9 @@ class AccountStatusCreationWidget extends HookConsumerWidget {
spacing: 4,
children: [
Icon(Symbols.keyboard_arrow_up),
Text('statusCreateHint').tr(),
Expanded(
child: Text('statusCreateHint', maxLines: 1).tr(),
),
],
),
).opacity(0.85),

View File

@@ -17,8 +17,8 @@ class NotificationCard extends HookConsumerWidget {
return Card(
elevation: 4,
margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -1,8 +1,10 @@
import 'dart:io';
import 'dart:ui';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -14,6 +16,15 @@ import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:path_provider/path_provider.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 {
final Widget child;
const WindowScaffold({super.key, required this.child});
@@ -153,7 +164,7 @@ class _WindowSizeObserver extends WidgetsBindingObserver {
final rootScaffoldKey = GlobalKey<ScaffoldState>();
class AppScaffold extends StatelessWidget {
class AppScaffold extends HookConsumerWidget {
final Widget? body;
final PreferredSizeWidget? bottomNavigationBar;
final PreferredSizeWidget? bottomSheet;
@@ -186,7 +197,14 @@ class AppScaffold extends StatelessWidget {
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final focusNode = useFocusNode();
useEffect(() {
focusNode.requestFocus();
return null;
}, []);
final appBarHeight = appBar?.preferredSize.height ?? 0;
final safeTop = MediaQuery.of(context).padding.top;
@@ -201,29 +219,59 @@ class AppScaffold extends StatelessWidget {
],
);
return Scaffold(
extendBody: extendBody ?? true,
extendBodyBehindAppBar: true,
backgroundColor:
noBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor,
body:
noBackground ? content : AppBackground(isRoot: true, child: content),
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
drawer: drawer,
endDrawer: endDrawer,
floatingActionButton: floatingActionButton,
floatingActionButtonAnimator: floatingActionButtonAnimator,
floatingActionButtonLocation: floatingActionButtonLocation,
onDrawerChanged: onDrawerChanged,
onEndDrawerChanged: onEndDrawerChanged,
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.escape): const PopIntent(),
},
child: Actions(
actions: <Type, Action<Intent>>{PopIntent: PopAction(context)},
child: Focus(
focusNode: focusNode,
child: Scaffold(
extendBody: extendBody ?? true,
extendBodyBehindAppBar: true,
backgroundColor:
noBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor,
body:
noBackground
? content
: AppBackground(isRoot: true, child: content),
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
drawer: drawer,
endDrawer: endDrawer,
floatingActionButton: floatingActionButton,
floatingActionButtonAnimator: floatingActionButtonAnimator,
floatingActionButtonLocation: floatingActionButtonLocation,
onDrawerChanged: onDrawerChanged,
onEndDrawerChanged: onEndDrawerChanged,
),
),
),
);
}
}
class PopIntent extends Intent {
const PopIntent();
}
class PopAction extends Action<PopIntent> {
final BuildContext context;
PopAction(this.context);
@override
void invoke(PopIntent intent) {
if (context.canPop()) {
context.pop();
}
}
}
class PageBackButton extends StatelessWidget {
final Color? color;
final List<Shadow>? shadows;
@@ -271,11 +319,12 @@ class AppBackground extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final imageFileAsync = ref.watch(backgroundImageFileProvider);
final settings = ref.watch(appSettingsNotifierProvider);
if (isRoot || !isWideScreen(context)) {
return imageFileAsync.when(
data: (file) {
if (file != null) {
if (file != null && settings.showBackgroundImage) {
return Container(
color: Theme.of(context).colorScheme.surface,
child: Container(

View File

@@ -1,15 +1,18 @@
import 'dart:async';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/screens/tray_manager.dart';
import 'package:island/services/notify.dart';
import 'package:island/services/sharing_intent.dart';
import 'package:island/services/update_service.dart';
import 'package:island/widgets/content/network_status_sheet.dart';
import 'package:island/widgets/tour/tour.dart';
import 'package:tray_manager/tray_manager.dart';
class AppWrapper extends HookConsumerWidget {
class AppWrapper extends HookConsumerWidget with TrayListener {
final Widget child;
const AppWrapper({super.key, required this.child});
@@ -20,10 +23,16 @@ class AppWrapper extends HookConsumerWidget {
Future(() {
if (context.mounted) ntySubs = setupNotificationListener(context, ref);
});
final sharingService = SharingIntentService();
sharingService.initialize(context);
UpdateService().checkForUpdates(context);
TrayService.instance.initialize(this);
return () {
TrayService.instance.dispose(this);
sharingService.dispose();
ntySubs?.cancel();
};
@@ -52,4 +61,31 @@ class AppWrapper extends HookConsumerWidget {
return TourTriggerWidget(key: UniqueKey(), child: child);
}
void _trayIconPrimaryAction() {
if (appWindow.isVisible) {
appWindow.restore();
} else {
appWindow.show();
}
}
void _trayIconSecondaryAction() {
trayManager.popUpContextMenu();
}
@override
void onTrayIconMouseUp() {
_trayIconPrimaryAction();
}
@override
void onTrayIconRightMouseDown() {
_trayIconSecondaryAction();
}
@override
void onTrayMenuItemClick(MenuItem menuItem) {
TrayService.instance.handleAction(menuItem);
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.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:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:slide_countdown/slide_countdown.dart';
import 'package:styled_widget/styled_widget.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 {
final EdgeInsets? margin;
final VoidCallback? onChecked;
@@ -42,6 +55,22 @@ class CheckInWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
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 {
final client = ref.read(apiClientProvider);
@@ -72,67 +101,127 @@ class CheckInWidget extends HookConsumerWidget {
margin:
margin ?? EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 16,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.secondaryContainer,
width: 56,
height: 56,
child:
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
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,
),
],
).center(),
),
),
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(),
Text(
result.tips
.map(
(e) => '${e.isPositive ? '' : ''} ${e.title}',
)
.join(' · '),
).fontSize(11),
],
);
},
loading: () => _CheckInNoneWidget(),
error:
(err, stack) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('error').tr().fontSize(15).bold(),
Text(err.toString()).fontSize(11),
],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 6,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
switch (DateTime.now().weekday) {
6 || 7 => Symbols.weekend,
_ => isAdult ? Symbols.work : Symbols.school,
},
fill: 1,
size: 16,
).padding(right: 2),
Text(DateFormat('EEE').format(DateTime.now()))
.fontSize(16)
.bold()
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
Text(DateFormat('MM/dd').format(DateTime.now()))
.fontSize(16)
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: todayResult.when(
data: (result) {
return Text(
result == null
? 'checkInNone'
: 'checkInResultLevel${result.level}',
textAlign: TextAlign.start,
).tr().fontSize(15).bold();
},
loading:
() =>
Text('checkInNone').tr().fontSize(15).bold(),
error:
(err, stack) =>
Text('error').tr().fontSize(15).bold(),
),
).alignment(Alignment.centerLeft),
),
),
],
),
Row(
spacing: 5,
children: [
Text('notableDayNext')
.tr(args: [nextNotableDay.value?.localName ?? 'idk'])
.fontSize(12),
if (nextNotableDay.value != null)
SlideCountdown(
decoration: const BoxDecoration(),
style: const TextStyle(fontSize: 12),
separatorStyle: const TextStyle(fontSize: 12),
padding: EdgeInsets.zero,
duration: nextNotableDay.value?.date.difference(
DateTime.now(),
),
),
],
),
const Gap(6),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: todayResult.when(
data: (result) {
if (result == null) {
return Text('checkInNoneHint').tr().fontSize(11);
}
return Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
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(),
);
},
loading: () => Text('checkInNoneHint').tr().fontSize(11),
error:
(err, stack) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('error').tr().fontSize(15).bold(),
Text(err.toString()).fontSize(11),
],
),
),
).alignment(Alignment.centerLeft),
],
),
),
IconButton.outlined(
@@ -167,21 +256,6 @@ class CheckInWidget extends HookConsumerWidget {
}
}
class _CheckInNoneWidget extends StatelessWidget {
const _CheckInNoneWidget();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('checkInNone').tr().fontSize(15).bold(),
Text('checkInNoneHint').tr().fontSize(11),
],
);
}
}
class CheckInActivityWidget extends StatelessWidget {
final SnActivity item;
const CheckInActivityWidget({super.key, required this.item});

View File

@@ -26,5 +26,24 @@ final checkInResultTodayProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
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: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -6,17 +6,21 @@ import 'package:flutter_platform_alert/flutter_platform_alert.dart';
String _parseRemoteError(DioException err) {
log('${err.requestOptions.method} ${err.requestOptions.uri} ${err.message}');
if (err.response?.data is String) return err.response?.data;
if (err.response?.data?['errors'] != null) {
String? message;
if (err.response?.data is String) {
message = err.response?.data;
} else if (err.response?.data?['errors'] != null) {
final errors = err.response?.data['errors'] as Map<String, dynamic>;
return errors.values
message = errors.values
.map(
(ele) =>
(ele as List<dynamic>).map((ele) => ele.toString()).join('\n'),
)
.join('\n');
}
return err.message ?? err.toString();
if (message == null || message.isEmpty) message = err.response?.statusMessage;
message ??= err.message;
return message ?? err.toString();
}
void showErrorAlert(dynamic err) async {

View File

@@ -10,6 +10,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/file.dart';
import 'package:island/utils/format.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
@@ -284,6 +285,13 @@ class AttachmentPreview extends HookConsumerWidget {
Builder(
key: ValueKey(item.hashCode),
builder: (context) {
final fallbackIcon = switch (item.type) {
UniversalFileType.video => Symbols.video_file,
UniversalFileType.audio => Symbols.audio_file,
UniversalFileType.image => Symbols.image,
_ => Symbols.insert_drive_file,
};
if (item.isOnCloud) {
return CloudFileWidget(item: item.data);
} else if (item.data is XFile) {
@@ -309,9 +317,23 @@ class AttachmentPreview extends HookConsumerWidget {
: Image.file(File(file.path));
default:
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Symbols.document_scanner),
Text(file.name),
Icon(fallbackIcon),
const Gap(6),
Text(file.name, textAlign: TextAlign.center),
FutureBuilder(
future: file.length(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final size = snapshot.data as int;
return Text(
formatFileSize(size),
).fontSize(11);
}
return const SizedBox.shrink();
},
),
],
);
}
@@ -321,7 +343,14 @@ class AttachmentPreview extends HookConsumerWidget {
return Image.memory(item.data);
default:
return Column(
children: [const Icon(Symbols.document_scanner)],
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(fallbackIcon),
const Gap(6),
Text(
formatFileSize(item.data.length),
).fontSize(11),
],
);
}
}

View File

@@ -5,11 +5,11 @@ import 'dart:ui';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:gal/gal.dart';
@@ -17,6 +17,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/utils/format.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sensitive.dart';
@@ -359,16 +360,6 @@ class CloudFileZoomIn extends HookConsumerWidget {
}
}
String formatFileSize(int bytes) {
if (bytes <= 0) return '0 B';
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
}
void showInfoSheet() {
final theme = Theme.of(context);
final exifData = item.fileMeta?['exif'] as Map<String, dynamic>? ?? {};
@@ -813,164 +804,213 @@ class _CloudFileListEntry extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final dataSaving = ref.watch(
appSettingsNotifierProvider.select((s) => s.dataSavingMode),
);
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(
fit: StackFit.expand,
children: [
if (isImage)
Positioned.fill(
child:
file.fileMeta?['blur'] is String
? BlurHash(hash: file.fileMeta?['blur'])
: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: CloudFileWidget(item: file, noBlurhash: true),
),
),
if (isImage)
CloudFileWidget(
final fit = hasRatio ? BoxFit.cover : BoxFit.contain;
Widget bg = const SizedBox.shrink();
if (isImage) {
if (meta['blur'] is String) {
bg = BlurHash(hash: meta['blur'] as String);
} else if (!lockedByDS && !lockedByMature) {
bg = ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: CloudFileWidget(
fit: fit,
item: file,
heroTag: heroTag,
noBlurhash: true,
fit: BoxFit.contain,
)
else
CloudFileWidget(item: file, heroTag: heroTag, fit: BoxFit.contain),
],
useInternalGate: false,
),
);
} 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) {
// Show a blurred overlay only when not revealed yet, with a smooth transition
content = Stack(
children: [
content,
// Toggle blur overlay with animation
Positioned.fill(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
layoutBuilder:
(currentChild, previousChildren) => Stack(
fit: StackFit.expand,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
),
child:
showMature.value
? const SizedBox.shrink(key: ValueKey('revealed'))
: ColoredBox(
key: const ValueKey('blurred'),
color: Colors.transparent,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 64, sigmaY: 64),
child: Stack(
fit: StackFit.expand,
children: [
const ColoredBox(color: Colors.transparent),
Center(
child: Container(
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(12),
),
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 280,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.warning,
color: Colors.white,
fill: 1,
size: 24,
),
const Gap(4),
Text(
file.sensitiveMarks
.map(
(e) =>
SensitiveCategory
.values[e]
.i18nKey
.tr(),
)
.join(' · '),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
Text(
'Sensitive Content',
style: TextStyle(
color: Colors.white,
fontSize: 13,
),
),
const Gap(4),
Text(
'Tap to Reveal',
style: TextStyle(
color: Colors.white,
fontSize: 11,
),
),
],
),
).padding(horizontal: 24, vertical: 16),
),
),
],
),
),
),
),
),
// When revealed (no blur), show a small control at top-left to re-enable blur
if (showMature.value)
Positioned(
top: 3,
left: 4,
child: IconButton(
iconSize: 16,
constraints: const BoxConstraints(),
icon: const Icon(Icons.visibility_off, color: Colors.white),
tooltip: 'Blur content',
onPressed: () {
showMature.value = false;
},
),
),
],
);
}
if (onTap != null) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: () {
if (!showMature.value) {
showMature.value = true;
} else {
onTap?.call();
}
},
child: content,
);
}
return content;
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: () {
if (lockedByDS) {
showDataSaving.value = true;
} else if (lockedByMature) {
showMature.value = true;
} else {
onTap?.call();
}
},
child: content,
);
}
}
class _SensitiveOverlay extends StatelessWidget {
final SnCloudFile file;
final VoidCallback? onHide;
final bool isRevealed;
const _SensitiveOverlay({
required this.file,
this.onHide,
this.isRevealed = false,
});
@override
Widget build(BuildContext context) {
if (isRevealed) {
return Positioned(
top: 3,
left: 4,
child: IconButton(
iconSize: 16,
constraints: const BoxConstraints(),
icon: const Icon(
Icons.visibility_off,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
tooltip: 'Blur content',
onPressed: onHide,
),
);
}
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 64, sigmaY: 64),
child: Container(
color: Colors.transparent,
child: Center(
child: _OverlayCard(
icon: Icons.warning,
title: file.sensitiveMarks
.map((e) => SensitiveCategory.values[e].i18nKey.tr())
.join(' · '),
subtitle: 'Sensitive Content',
hint: 'Tap to Reveal',
),
),
),
);
}
}
class _DataSavingOverlay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ColoredBox(
color: Colors.black38,
child: Center(
child: _OverlayCard(
icon: Symbols.image,
title: 'Data Saving Mode',
subtitle: '',
hint: 'Tap to Load',
),
),
);
}
}
class _OverlayCard extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final String hint;
const _OverlayCard({
required this.icon,
required this.title,
required this.subtitle,
required this.hint,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(12),
),
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: Colors.white, size: 24),
const Gap(4),
Text(
title,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
Text(
subtitle,
style: const TextStyle(color: Colors.white, fontSize: 13),
),
const Gap(4),
Text(hint, style: const TextStyle(color: Colors.white, fontSize: 11)),
],
),
);
}
}

View File

@@ -1,15 +1,20 @@
import 'dart:math' as math;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/services/time.dart';
import 'package:island/utils/format.dart';
import 'package:island/widgets/content/audio.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:island/widgets/data_saving_gate.dart';
import 'image.dart';
import 'video.dart';
@@ -19,48 +24,97 @@ class CloudFileWidget extends HookConsumerWidget {
final BoxFit fit;
final String? heroTag;
final bool noBlurhash;
final bool useInternalGate;
const CloudFileWidget({
super.key,
required this.item,
this.fit = BoxFit.cover,
this.heroTag,
this.noBlurhash = false,
this.useInternalGate = true,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final dataSaving = ref.watch(
appSettingsNotifierProvider.select((s) => s.dataSavingMode),
);
final serverUrl = ref.watch(serverUrlProvider);
final uri = '$serverUrl/drive/files/${item.id}';
var ratio =
item.fileMeta?['ratio'] is num
? item.fileMeta!['ratio'].toDouble()
: 1.0;
final unlocked = useState(false);
final meta = item.fileMeta is Map ? (item.fileMeta as Map) : const {};
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;
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) {
"image" => AspectRatio(
aspectRatio: ratio,
child: UniversalImage(
uri: uri,
blurHash:
noBlurhash
? null
: (item.fileMeta is String ? item.fileMeta!['blur'] : null),
'image' => AspectRatio(
aspectRatio: ratio,
child: (useInternalGate && dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.image) : cloudImage(),
),
),
"video" => AspectRatio(
aspectRatio: ratio,
child: CloudVideoWidget(item: item),
),
"audio" => Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: math.min(360, MediaQuery.of(context).size.width * 0.8),
'video' => AspectRatio(
aspectRatio: ratio,
child: (useInternalGate && dataSaving && !unlocked.value) ? dataPlaceHolder(Symbols.play_arrow) : cloudVideo(),
),
'audio' => Center(
child: ConstrainedBox(
constraints: BoxConstraints(
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),
),
),
_ => Text('Unable render for ${item.mimeType}'),
_ => Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Symbols.insert_drive_file,
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,
),
),
Text(
formatFileSize(item.size),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const Gap(8),
TextButton.icon(
onPressed: () {
launchUrlString(
'https://fs.solian.app/files/${item.id}',
mode: LaunchMode.externalApplication,
);
},
icon: const Icon(Symbols.launch),
label: Text('openInBrowser').tr(),
),
],
).padding(all: 8),
};
if (heroTag != null) {
@@ -71,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 {
final SnCloudFile item;
const CloudVideoWidget({super.key, required this.item});
@@ -269,32 +352,35 @@ class ProfilePictureWidget extends ConsumerWidget {
this.fallbackColor,
});
@override
@override
Widget build(BuildContext context, WidgetRef ref) {
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(
borderRadius:
borderRadius == null
? BorderRadius.all(Radius.circular(radius))
: BorderRadius.all(Radius.circular(borderRadius!)),
borderRadius: borderRadius == null
? BorderRadius.all(Radius.circular(radius))
: BorderRadius.all(Radius.circular(borderRadius!)),
child: Container(
width: radius * 2,
height: radius * 2,
color: Theme.of(context).colorScheme.primaryContainer,
child:
file != null
? CloudFileWidget(item: file!, fit: BoxFit.cover)
: fileId == null
? Icon(
fallbackIcon ?? Symbols.account_circle,
size: radius,
color:
fallbackColor ??
Theme.of(context).colorScheme.onPrimaryContainer,
).center()
: UniversalImage(uri: uri, fit: BoxFit.cover),
child: id == null
? fallback
: DataSavingGate(
bypass: true,
placeholder: fallback,
content: () => UniversalImage(
uri: '$serverUrl/drive/files/$id',
fit: BoxFit.cover,
),
),
),
);
}

View File

@@ -52,12 +52,10 @@ class UniversalImage extends StatelessWidget {
},
errorWidget: (context, url, error) {
return Image.asset(
'assets/images/media-offline.png',
'assets/images/media-offline.jpg',
fit: BoxFit.cover,
key: Key('image-broke-$uri'),
);
// return const Center(
// child: Icon(Icons.broken_image, color: Colors.white, size: 16),
// );
},
),
],

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

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class RefreshIntent extends Intent {
const RefreshIntent();
}
class ExtendedRefreshIndicator extends StatefulWidget {
final Widget child;
final RefreshCallback onRefresh;
const ExtendedRefreshIndicator({
super.key,
required this.child,
required this.onRefresh,
});
@override
State<ExtendedRefreshIndicator> createState() =>
_ExtendedRefreshIndicatorState();
}
class _ExtendedRefreshIndicatorState extends State<ExtendedRefreshIndicator> {
late final FocusNode _focusNode;
@override
void initState() {
super.initState();
_focusNode = FocusNode();
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyR):
const RefreshIntent(),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.keyR):
const RefreshIntent(),
LogicalKeySet(LogicalKeyboardKey.f5): const RefreshIntent(),
},
child: Actions(
actions: <Type, Action<Intent>>{
RefreshIntent: CallbackAction<RefreshIntent>(
onInvoke: (RefreshIntent intent) => widget.onRefresh(),
),
},
child: Focus(
focusNode: _focusNode,
child: RefreshIndicator(
onRefresh: widget.onRefresh,
child: widget.child,
),
),
),
);
}
}

View File

@@ -44,13 +44,13 @@ class PollStatsWidget extends StatelessWidget {
// yes/no: map {true: count, false: count}
if (raw is Map) {
final int yes =
(raw[true] is int)
? raw[true] as int
: int.tryParse('${raw[true]}') ?? 0;
(raw['true'] is int)
? raw['true'] as int
: int.tryParse('${raw['true']}') ?? 0;
final int no =
(raw[false] is int)
? raw[false] as int
: int.tryParse('${raw[false]}') ?? 0;
(raw['false'] is int)
? raw['false'] as int
: int.tryParse('${raw['false']}') ?? 0;
final total = (yes + no).clamp(0, 1 << 31);
final yesPct = total == 0 ? 0.0 : yes / total;
final noPct = total == 0 ? 0.0 : no / total;

View File

@@ -17,6 +17,7 @@ class PollSubmit extends ConsumerStatefulWidget {
this.onCancel,
this.showProgress = true,
this.isReadonly = false,
this.isInitiallyExpanded = false,
});
final SnPollWithStats poll;
@@ -36,6 +37,9 @@ class PollSubmit extends ConsumerStatefulWidget {
final bool isReadonly;
/// Whether the poll should start expanded instead of collapsed.
final bool isInitiallyExpanded;
@override
ConsumerState<PollSubmit> createState() => _PollSubmitState();
}
@@ -45,6 +49,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
int _index = 0;
bool _submitting = false;
bool _isModifying = false; // New state to track if user is modifying answers
bool _isCollapsed = true; // New state to track collapse/expand
/// Collected answers, keyed by questionId
late Map<String, dynamic> _answers;
@@ -65,6 +70,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
_questions = [...widget.poll.questions]
..sort((a, b) => a.order.compareTo(b.order));
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
// Set initial collapse state based on the parameter
_isCollapsed = !widget.isInitiallyExpanded;
if (!widget.isReadonly) {
_loadCurrentIntoLocalState();
// If initial answers are provided, set _isModifying to false initially
@@ -405,6 +412,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
});
},
multiSelectionEnabled: false,
emptySelectionAllowed: true,
),
),
],
@@ -652,39 +660,179 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
);
}
Widget _buildCollapsedView(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.poll.title != null)
Text(
widget.poll.title!,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (widget.poll.description != null)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
widget.poll.description!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).textTheme.bodySmall?.color?.withOpacity(0.7),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
)
else
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
'${_questions.length} question${_questions.length == 1 ? '' : 's'}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).textTheme.bodySmall?.color?.withOpacity(0.7),
),
),
),
],
),
),
IconButton(
icon: Icon(
_isCollapsed ? Icons.expand_more : Icons.expand_less,
size: 20,
),
onPressed: () {
setState(() {
_isCollapsed = !_isCollapsed;
});
},
visualDensity: VisualDensity.compact,
tooltip: _isCollapsed ? 'expandPoll'.tr() : 'collapsePoll'.tr(),
),
],
),
],
);
}
@override
Widget build(BuildContext context) {
if (_questions.isEmpty) {
return const SizedBox.shrink();
}
// If collapsed, show collapsed view for all states
if (_isCollapsed) {
return _buildCollapsedView(context);
}
// If poll is already submitted and not in readonly mode, and not in modification mode, show submitted view
if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [_buildSubmittedView(context), _buildNavBar(context)],
children: [
_buildCollapsedView(context),
const SizedBox(height: 8),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, anim) {
final offset = Tween<Offset>(
begin: const Offset(0, -0.1),
end: Offset.zero,
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut);
return FadeTransition(
opacity: fade,
child: SlideTransition(position: offset, child: child),
);
},
child: Column(
key: const ValueKey('submitted_expanded'),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [_buildSubmittedView(context), _buildNavBar(context)],
),
),
],
);
}
// If poll is in readonly mode, show readonly view
if (widget.isReadonly) {
return _buildReadonlyView(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildCollapsedView(context),
const SizedBox(height: 8),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, anim) {
final offset = Tween<Offset>(
begin: const Offset(0, -0.1),
end: Offset.zero,
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut);
return FadeTransition(
opacity: fade,
child: SlideTransition(position: offset, child: child),
);
},
child: _buildReadonlyView(context),
),
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildHeader(context),
const SizedBox(height: 12),
_AnimatedStep(
key: ValueKey(_current.id),
_buildCollapsedView(context),
const SizedBox(height: 8),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, anim) {
final offset = Tween<Offset>(
begin: const Offset(0, -0.1),
end: Offset.zero,
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut);
return FadeTransition(
opacity: fade,
child: SlideTransition(position: offset, child: child),
);
},
child: Column(
key: const ValueKey('normal_expanded'),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [_buildBody(context), _buildStats(context, _current)],
children: [
_buildHeader(context),
const SizedBox(height: 12),
_AnimatedStep(
key: ValueKey(_current.id),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildBody(context),
_buildStats(context, _current),
],
),
),
const SizedBox(height: 16),
_buildNavBar(context),
],
),
),
const SizedBox(height: 16),
_buildNavBar(context),
],
);
}

View File

@@ -0,0 +1,287 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/compose_shared.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class ComposeEmbedSheet extends HookConsumerWidget {
final ComposeState state;
const ComposeEmbedSheet({super.key, required this.state});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
// Listen to embed view changes
final currentEmbedView = useValueListenable(state.embedView);
// Form state
final uriController = useTextEditingController();
final aspectRatioController = useTextEditingController();
final selectedRenderer = useState<PostEmbedViewRenderer>(
PostEmbedViewRenderer.webView,
);
void clearForm() {
uriController.clear();
aspectRatioController.clear();
selectedRenderer.value = PostEmbedViewRenderer.webView;
}
// Populate form when embed view changes
useEffect(() {
if (currentEmbedView != null) {
uriController.text = currentEmbedView.uri;
aspectRatioController.text =
currentEmbedView.aspectRatio?.toString() ?? '';
selectedRenderer.value = currentEmbedView.renderer;
} else {
clearForm();
}
return null;
}, [currentEmbedView]);
void saveEmbedView() {
final uri = uriController.text.trim();
if (uri.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('embedUriRequired'.tr())));
return;
}
final aspectRatio =
aspectRatioController.text.trim().isNotEmpty
? double.tryParse(aspectRatioController.text.trim())
: null;
final embedView = SnPostEmbedView(
uri: uri,
aspectRatio: aspectRatio,
renderer: selectedRenderer.value,
);
if (currentEmbedView != null) {
ComposeLogic.updateEmbedView(state, embedView);
} else {
ComposeLogic.setEmbedView(state, embedView);
}
}
return SheetScaffold(
titleText: 'embedView'.tr(),
heightFactor: 0.7,
child: Column(
children: [
// Header with save button when editing
if (currentEmbedView != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Row(
children: [
Expanded(
child: Text(
'editEmbed'.tr(),
style: theme.textTheme.titleMedium,
),
),
TextButton(
onPressed: saveEmbedView,
child: Text('save'.tr()),
),
],
),
),
// Content area
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Form fields
TextField(
controller: uriController,
decoration: InputDecoration(
labelText: 'embedUri'.tr(),
hintText: 'https://example.com',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
keyboardType: TextInputType.url,
),
const Gap(16),
TextField(
controller: aspectRatioController,
decoration: InputDecoration(
labelText: 'aspectRatio'.tr(),
hintText: '16/9 = 1.777',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
keyboardType: TextInputType.numberWithOptions(
decimal: true,
),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$')),
],
),
const Gap(16),
DropdownButtonFormField2<PostEmbedViewRenderer>(
value: selectedRenderer.value,
decoration: InputDecoration(
labelText: 'renderer'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
items:
PostEmbedViewRenderer.values.map((renderer) {
return DropdownMenuItem(
value: renderer,
child: Text(renderer.name).tr(),
);
}).toList(),
onChanged: (value) {
if (value != null) {
selectedRenderer.value = value;
}
},
),
// Current embed view display (when exists)
if (currentEmbedView != null) ...[
const Gap(32),
Text(
'currentEmbed'.tr(),
style: theme.textTheme.titleMedium,
).padding(horizontal: 4),
const Gap(8),
Card(
margin: EdgeInsets.zero,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 12,
top: 4,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
currentEmbedView.renderer ==
PostEmbedViewRenderer.webView
? Symbols.web
: Symbols.web,
color: colorScheme.primary,
),
const Gap(12),
Expanded(
child: Text(
currentEmbedView.uri,
style: theme.textTheme.bodyMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
showDialog(
context: context,
builder:
(dialogContext) => AlertDialog(
title: Text('deleteEmbed').tr(),
content:
Text('deleteEmbedConfirm').tr(),
actions: [
TextButton(
onPressed:
() =>
Navigator.of(
dialogContext,
).pop(),
child: Text('cancel'.tr()),
),
TextButton(
onPressed: () {
ComposeLogic.deleteEmbedView(
state,
);
clearForm();
Navigator.of(
dialogContext,
).pop();
},
style: TextButton.styleFrom(
foregroundColor:
colorScheme.error,
),
child: Text('delete').tr(),
),
],
),
);
},
tooltip: 'delete'.tr(),
color: colorScheme.error,
),
],
),
const Gap(12),
Text(
'aspectRatio'.tr(),
style: theme.textTheme.labelMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const Gap(4),
Text(
currentEmbedView.aspectRatio != null
? currentEmbedView.aspectRatio!
.toStringAsFixed(2)
: 'notSet'.tr(),
style: theme.textTheme.bodyMedium,
),
],
),
),
),
] else ...[
// Save button for new embed
const Gap(16),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: saveEmbedView,
icon: const Icon(Symbols.add),
label: Text('addEmbed'.tr()),
),
),
],
],
),
),
),
],
),
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -36,6 +37,7 @@ class ComposeState {
final ValueNotifier<List<SnPostCategory>> categories;
StringTagController tagsController;
final ValueNotifier<SnRealm?> realm;
final ValueNotifier<SnPostEmbedView?> embedView;
final String draftId;
int postType;
// Linked poll id for this compose session (nullable)
@@ -55,6 +57,7 @@ class ComposeState {
required this.tagsController,
required this.categories,
required this.realm,
required this.embedView,
required this.draftId,
this.postType = 0,
String? pollId,
@@ -119,6 +122,7 @@ class ComposeLogic {
originalPost?.categories ?? [],
),
realm: ValueNotifier(originalPost?.realm),
embedView: ValueNotifier<SnPostEmbedView?>(originalPost?.embedView),
draftId: id,
postType: postType,
// initialize without poll by default
@@ -150,6 +154,7 @@ class ComposeLogic {
tagsController: tagsController,
categories: ValueNotifier<List<SnPostCategory>>([]),
realm: ValueNotifier(null),
embedView: ValueNotifier<SnPostEmbedView?>(draft.embedView),
draftId: draft.id,
postType: postType,
pollId: null,
@@ -252,6 +257,7 @@ class ComposeLogic {
tags: [],
categories: [],
collections: [],
embedView: state.embedView.value,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
@@ -328,6 +334,7 @@ class ComposeLogic {
tags: [],
categories: [],
collections: [],
embedView: state.embedView.value,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
@@ -388,26 +395,32 @@ class ComposeLogic {
}
static Future<void> pickPhotoMedia(WidgetRef ref, ComposeState state) async {
final result = await ref
.watch(imagePickerProvider)
.pickMultiImage(requestFullMetadata: true);
if (result.isEmpty) return;
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: true,
allowCompression: false,
);
if (result == null || result.count == 0) return;
state.attachments.value = [
...state.attachments.value,
...result.map(
(e) => UniversalFile(data: e, type: UniversalFileType.image),
...result.files.map(
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.image),
),
];
}
static Future<void> pickVideoMedia(WidgetRef ref, ComposeState state) async {
final result = await ref
.watch(imagePickerProvider)
.pickVideo(source: ImageSource.gallery);
if (result == null) return;
final result = await FilePicker.platform.pickFiles(
type: FileType.video,
allowMultiple: true,
allowCompression: false,
);
if (result == null || result.count == 0) return;
state.attachments.value = [
...state.attachments.value,
UniversalFile(data: result, type: UniversalFileType.video),
...result.files.map(
(e) => UniversalFile(data: e.xFile, type: UniversalFileType.video),
),
];
}
@@ -570,6 +583,18 @@ class ComposeLogic {
);
}
static void setEmbedView(ComposeState state, SnPostEmbedView embedView) {
state.embedView.value = embedView;
}
static void updateEmbedView(ComposeState state, SnPostEmbedView embedView) {
state.embedView.value = embedView;
}
static void deleteEmbedView(ComposeState state) {
state.embedView.value = null;
}
static Future<void> pickPoll(
WidgetRef ref,
ComposeState state,
@@ -653,6 +678,8 @@ class ComposeLogic {
'categories': state.categories.value.map((e) => e.slug).toList(),
if (state.realm.value != null) 'realm_id': state.realm.value?.id,
if (state.pollId.value != null) 'poll_id': state.pollId.value,
if (state.embedView.value != null)
'embed_view': state.embedView.value!.toJson(),
};
// Send request
@@ -746,6 +773,7 @@ class ComposeLogic {
state.tagsController.dispose();
state.categories.dispose();
state.realm.dispose();
state.embedView.dispose();
state.pollId.dispose();
}
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/services/compose_storage_db.dart';
import 'package:island/widgets/post/compose_embed_sheet.dart';
import 'package:island/widgets/post/compose_shared.dart';
import 'package:island/widgets/post/draft_manager.dart';
import 'package:material_symbols_icons/symbols.dart';
@@ -40,6 +41,14 @@ class ComposeToolbar extends HookConsumerWidget {
ComposeLogic.pickPoll(ref, state, context);
}
void showEmbedSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => ComposeEmbedSheet(state: state),
);
}
void showDraftManager() {
showModalBottomSheet(
context: context,
@@ -112,6 +121,25 @@ class ComposeToolbar extends HookConsumerWidget {
);
},
),
// Embed button with visual state when embed is present
ListenableBuilder(
listenable: state.embedView,
builder: (context, _) {
return IconButton(
onPressed: showEmbedSheet,
icon: const Icon(Symbols.web),
tooltip: 'embedView'.tr(),
color: colorScheme.primary,
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
state.embedView.value != null
? colorScheme.primary.withOpacity(0.15)
: null,
),
),
);
},
),
const Spacer(),
if (originalPost == null && state.isEmpty)
IconButton(

View File

@@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:url_launcher/url_launcher.dart';
class EmbedViewRenderer extends HookConsumerWidget {
final SnPostEmbedView embedView;
final double? maxHeight;
final BorderRadius? borderRadius;
const EmbedViewRenderer({
super.key,
required this.embedView,
this.maxHeight = 400,
this.borderRadius,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
constraints: BoxConstraints(maxHeight: maxHeight ?? 400),
decoration: BoxDecoration(
borderRadius: borderRadius ?? BorderRadius.circular(12),
border: Border.all(
color: colorScheme.outline.withOpacity(0.3),
width: 1,
),
color: colorScheme.surfaceContainerLowest,
),
child: ClipRRect(
borderRadius: borderRadius ?? BorderRadius.circular(12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header with embed info
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
border: Border(
bottom: BorderSide(
color: colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
),
child: Row(
children: [
Icon(
embedView.renderer == PostEmbedViewRenderer.webView
? Symbols.web
: Symbols.web,
size: 16,
color: colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_getUriDisplay(embedView.uri),
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
IconButton(
icon: Icon(
Symbols.open_in_new,
size: 16,
color: colorScheme.onSurfaceVariant,
),
onPressed: () async {
final uri = Uri.parse(embedView.uri);
if (await canLaunchUrl(uri)) {
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
}
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
tooltip: 'Open in browser',
),
],
),
),
// WebView content
AspectRatio(
aspectRatio: embedView.aspectRatio ?? 1,
child: InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(embedView.uri)),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
useShouldOverrideUrlLoading: true,
useOnLoadResource: true,
supportZoom: false,
useWideViewPort: false,
loadWithOverviewMode: true,
builtInZoomControls: false,
displayZoomControls: false,
minimumFontSize: 12,
preferredContentMode: UserPreferredContentMode.MOBILE,
allowsBackForwardNavigationGestures: false,
allowsLinkPreview: false,
isInspectable: false,
applicationNameForUserAgent: 'Solian/3.0',
userAgent:
'Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36 Solian/3.0',
),
onWebViewCreated: (controller) {
// Configure webview settings
controller.addJavaScriptHandler(
handlerName: 'onHeightChanged',
callback: (args) {
// Handle dynamic height changes if needed
},
);
},
onLoadStart: (controller, url) {
// Handle load start
},
onLoadStop: (controller, url) async {
// Inject CSS to improve mobile display and remove borders
await controller.evaluateJavascript(
source: '''
// Remove unwanted elements
var elements = document.querySelectorAll('nav, header, footer, .ads, .advertisement, .sidebar');
for (var i = 0; i < elements.length; i++) {
elements[i].style.display = 'none';
}
// Remove borders from embedded content (YouTube, Vimeo, etc.)
var iframes = document.querySelectorAll('iframe');
for (var i = 0; i < iframes.length; i++) {
iframes[i].style.border = 'none';
iframes[i].style.borderRadius = '0';
}
// Remove borders from video elements
var videos = document.querySelectorAll('video');
for (var i = 0; i < videos.length; i++) {
videos[i].style.border = 'none';
videos[i].style.borderRadius = '0';
}
// Remove borders from any element that might have them
var allElements = document.querySelectorAll('*');
for (var i = 0; i < allElements.length; i++) {
if (allElements[i].style.border) {
allElements[i].style.border = 'none';
}
}
// Improve readability
var body = document.body;
body.style.fontSize = '14px';
body.style.lineHeight = '1.4';
body.style.margin = '0';
body.style.padding = '0';
// Handle dynamic content
var observer = new MutationObserver(function(mutations) {
// Remove borders from newly added elements
var newIframes = document.querySelectorAll('iframe');
for (var i = 0; i < newIframes.length; i++) {
if (!newIframes[i].style.border || newIframes[i].style.border !== 'none') {
newIframes[i].style.border = 'none';
newIframes[i].style.borderRadius = '0';
}
}
var newVideos = document.querySelectorAll('video');
for (var i = 0; i < newVideos.length; i++) {
if (!newVideos[i].style.border || newVideos[i].style.border !== 'none') {
newVideos[i].style.border = 'none';
newVideos[i].style.borderRadius = '0';
}
}
window.flutter_inappwebview.callHandler('onHeightChanged', document.body.scrollHeight);
});
observer.observe(document.body, { childList: true, subtree: true });
''',
);
},
onLoadError: (controller, url, code, message) {
// Handle load errors
},
onLoadHttpError: (controller, url, statusCode, description) {
// Handle HTTP errors
},
shouldOverrideUrlLoading: (controller, navigationAction) async {
final uri = navigationAction.request.url;
if (uri != null && uri.toString() != embedView.uri) {
// Open external links in browser
// You might want to use url_launcher here
return NavigationActionPolicy.CANCEL;
}
return NavigationActionPolicy.ALLOW;
},
onProgressChanged: (controller, progress) {
// Handle progress changes if needed
},
onConsoleMessage: (controller, consoleMessage) {
// Handle console messages for debugging
debugPrint('WebView Console: ${consoleMessage.message}');
},
),
),
],
),
),
);
}
String _getUriDisplay(String uri) {
try {
final parsedUri = Uri.parse(uri);
return parsedUri.host.isNotEmpty ? parsedUri.host : uri;
} catch (e) {
return uri;
}
}
}

View File

@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -47,7 +48,7 @@ class PostFeaturedList extends HookConsumerWidget {
'PostFeaturedList: isCollapsed changed to ${isCollapsed.value}',
);
return null;
}, [isCollapsed.value]);
}, [isCollapsed]);
useEffect(() {
if (featuredPostsAsync.hasValue && featuredPostsAsync.value!.isNotEmpty) {
@@ -92,7 +93,7 @@ class PostFeaturedList extends HookConsumerWidget {
);
}
return null;
}, [featuredPostsAsync.value]);
}, [featuredPostsAsync]);
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
@@ -105,7 +106,7 @@ class PostFeaturedList extends HookConsumerWidget {
spacing: 8,
children: [
const Icon(Symbols.highlight),
Text('Highlight Posts'),
const Text('highlightPost').tr(),
Spacer(),
IconButton(
padding: EdgeInsets.zero,

View File

@@ -18,8 +18,10 @@ import 'package:island/screens/posts/compose.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/post/post_item_screenshot.dart';
import 'package:island/widgets/post/post_award_sheet.dart';
import 'package:island/widgets/post/post_pin_sheet.dart';
import 'package:island/widgets/post/post_shared.dart';
import 'package:island/widgets/post/embed_view_renderer.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';
@@ -36,6 +38,7 @@ class PostActionableItem extends HookConsumerWidget {
final bool isShowReference;
final bool isEmbedReply;
final bool isEmbedOpenable;
final bool isCompact;
final double? borderRadius;
final VoidCallback? onRefresh;
final Function(SnPost)? onUpdate;
@@ -48,6 +51,7 @@ class PostActionableItem extends HookConsumerWidget {
this.isShowReference = true,
this.isEmbedReply = true,
this.isEmbedOpenable = false,
this.isCompact = false,
this.borderRadius,
this.onRefresh,
this.onUpdate,
@@ -76,6 +80,7 @@ class PostActionableItem extends HookConsumerWidget {
isEmbedReply: isEmbedReply,
isEmbedOpenable: isEmbedOpenable,
isTextSelectable: false,
isCompact: isCompact,
onRefresh: onRefresh,
onUpdate: onUpdate,
onOpen: onOpen,
@@ -242,6 +247,18 @@ class PostActionableItem extends HookConsumerWidget {
);
},
),
MenuAction(
title: 'award'.tr(),
image: MenuImage.icon(Symbols.star),
callback: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => PostAwardSheet(post: item),
);
},
),
MenuSeparator(),
MenuAction(
title: 'share'.tr(),
@@ -298,6 +315,7 @@ class PostItem extends HookConsumerWidget {
final bool isEmbedOpenable;
final bool isTextSelectable;
final bool isTranslatable;
final bool isCompact;
final VoidCallback? onRefresh;
final Function(SnPost)? onUpdate;
final VoidCallback? onOpen;
@@ -311,6 +329,7 @@ class PostItem extends HookConsumerWidget {
this.isEmbedOpenable = false,
this.isTextSelectable = true,
this.isTranslatable = true,
this.isCompact = false,
this.onRefresh,
this.onUpdate,
this.onOpen,
@@ -340,7 +359,14 @@ class PostItem extends HookConsumerWidget {
final delta = isRemoving ? -1 : 1;
final reactionsCount = Map<String, int>.from(item.reactionsCount);
reactionsCount[symbol] = (reactionsCount[symbol] ?? 0) + delta;
onUpdate?.call(item.copyWith(reactionsCount: reactionsCount));
final reactionsMade = Map<String, bool>.from(item.reactionsMade);
reactionsMade[symbol] = delta == 1 ? true : false;
onUpdate?.call(
item.copyWith(
reactionsCount: reactionsCount,
reactionsMade: reactionsMade,
),
);
HapticFeedback.heavyImpact();
});
reacting.value = false;
@@ -458,54 +484,64 @@ class PostItem extends HookConsumerWidget {
PostHeader(
item: item,
isFullPost: isFullPost,
isCompact: isCompact,
renderingPadding: renderingPadding,
trailing: IconButton(
icon:
mostReaction == null
? const Icon(Symbols.add_reaction)
: Badge(
label: Center(
child: Text(
'x${item.reactionsCount[mostReaction]}',
style: const TextStyle(fontSize: 11),
textAlign: TextAlign.center,
),
),
offset: const Offset(4, 20),
backgroundColor: Theme.of(
context,
).colorScheme.primary.withOpacity(0.75),
textColor: Theme.of(context).colorScheme.onPrimary,
child: Text(
kReactionTemplates[mostReaction]?.icon ?? '',
style: const TextStyle(fontSize: 20),
trailing:
isCompact
? null
: IconButton(
icon:
mostReaction == null
? const Icon(Symbols.add_reaction)
: Badge(
label: Center(
child: Text(
'x${item.reactionsCount[mostReaction]}',
style: const TextStyle(fontSize: 11),
textAlign: TextAlign.center,
),
),
offset: const Offset(4, 20),
backgroundColor: Theme.of(
context,
).colorScheme.primary.withOpacity(0.75),
textColor:
Theme.of(context).colorScheme.onPrimary,
child: Text(
kReactionTemplates[mostReaction]?.icon ?? '',
style: const TextStyle(fontSize: 20),
),
),
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
(item.reactionsMade[mostReaction] ?? false)
? Theme.of(
context,
).colorScheme.primary.withOpacity(0.5)
: null,
),
),
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
(item.reactionsMade[mostReaction] ?? false)
? Theme.of(context).colorScheme.primary.withOpacity(0.5)
: null,
),
),
onPressed: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
builder: (BuildContext context) {
return _PostReactionSheet(
reactionsCount: item.reactionsCount,
reactionsMade: item.reactionsMade,
onReact: (symbol, attitude) {
reactPost(symbol, attitude);
onPressed: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
builder: (BuildContext context) {
return _PostReactionSheet(
reactionsCount: item.reactionsCount,
reactionsMade: item.reactionsMade,
onReact: (symbol, attitude) {
reactPost(symbol, attitude);
},
);
},
);
},
);
},
);
},
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(horizontal: -3, vertical: -3),
),
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(
horizontal: -3,
vertical: -3,
),
),
),
PostBody(
item: item,
@@ -514,6 +550,12 @@ class PostItem extends HookConsumerWidget {
translationSection: translationSection,
renderingPadding: renderingPadding,
),
if (item.embedView != null)
EmbedViewRenderer(
embedView: item.embedView!,
maxHeight: 400,
borderRadius: BorderRadius.circular(12),
).padding(horizontal: renderingPadding.horizontal, vertical: 8),
if (isShowReference)
ReferencedPostWidget(item: item, renderingPadding: renderingPadding),
if (item.repliesCount > 0 && isEmbedReply)

View File

@@ -10,6 +10,7 @@ import 'package:island/widgets/alert.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_shared.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:super_context_menu/super_context_menu.dart';
class PostItemCreator extends HookConsumerWidget {
@@ -33,7 +34,7 @@ class PostItemCreator extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final renderingPadding =
padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 16);
padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8);
return ContextMenuWidget(
menuProvider: (_) {
@@ -97,18 +98,22 @@ class PostItemCreator extends HookConsumerWidget {
context.goNamed('postDetail', pathParameters: {'id': item.id});
}
},
child: Padding(
padding: renderingPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PostHeader(item: item),
PostBody(item: item),
ReferencedPostWidget(item: item),
const Gap(16),
_buildAnalyticsSection(context),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Gap(renderingPadding.vertical),
PostHeader(item: item, renderingPadding: renderingPadding),
PostBody(item: item, renderingPadding: renderingPadding),
ReferencedPostWidget(
item: item,
renderingPadding: renderingPadding,
),
const Gap(16),
_buildAnalyticsSection(
context,
).padding(horizontal: renderingPadding.horizontal),
Gap(renderingPadding.vertical),
],
),
),
),

View File

@@ -532,6 +532,7 @@ class PostHeader extends StatelessWidget {
final bool isInteractive;
final EdgeInsets renderingPadding;
final bool isRelativeTime;
final bool isCompact;
const PostHeader({
super.key,
@@ -541,6 +542,7 @@ class PostHeader extends StatelessWidget {
this.isInteractive = true,
this.renderingPadding = EdgeInsets.zero,
this.isRelativeTime = true,
this.isCompact = false,
});
@override
@@ -584,11 +586,27 @@ class PostHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 4,
children: [
Text(item.publisher.nick).bold(),
Flexible(
child:
Text(
item.publisher.nick,
maxLines: 1,
overflow: TextOverflow.ellipsis,
).bold(),
),
if (item.publisher.verification != null)
VerificationMark(mark: item.publisher.verification!),
if (item.realm == null)
Text('@${item.publisher.name}').fontSize(11)
Flexible(
child:
isCompact
? const SizedBox.shrink()
: Text(
'@${item.publisher.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).fontSize(11),
)
else
...([
const Icon(Symbols.arrow_right, size: 14),
@@ -762,7 +780,9 @@ class PostBody extends ConsumerWidget {
).padding(bottom: 4),
MarkdownTextContent(
content:
item.isTruncated ? '${item.content!}...' : item.content!,
item.isTruncated
? '${item.content!}...'
: item.content ?? '',
isSelectable: isTextSelectable,
),
if (translationSection != null) translationSection!,
@@ -880,6 +900,7 @@ class PostBody extends ConsumerWidget {
poll: SnPollWithStats.fromJson(embedData['poll']),
onSubmit: (_) {},
isReadonly: !isInteractive,
isInitiallyExpanded: isFullPost,
).padding(horizontal: 16, vertical: 12),
),
_ => Text('Unable show embed: ${embedData['type']}'),

View File

@@ -25,7 +25,7 @@ class PostShuffleScreen extends HookConsumerWidget {
return cardSwiperController.dispose;
}, []);
const kBottomControlHeight = 64.0;
const kBottomControlHeight = 80.0;
return AppScaffold(
appBar: AppBar(title: const Text('postShuffle').tr()),
@@ -36,45 +36,50 @@ class PostShuffleScreen extends HookConsumerWidget {
bottom:
kBottomControlHeight + MediaQuery.of(context).padding.bottom,
),
child:
(postListState.value?.items.length ?? 0) > 0
? CardSwiper(
controller: cardSwiperController,
cardsCount: postListState.value!.items.length,
cardBuilder: (
context,
index,
horizontalOffsetPercentage,
verticalOffsetPercentage,
) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 540),
child: SingleChildScrollView(
child: Card(
margin: EdgeInsets.zero,
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: PostActionableItem(
item: postListState.value!.items[index],
),
child: Builder(
key: ValueKey(postListState.value?.items.length ?? 0),
builder: (context) {
if ((postListState.value?.items.length ?? 0) > 0) {
return CardSwiper(
controller: cardSwiperController,
cardsCount: postListState.value!.items.length,
isLoop: false,
cardBuilder: (
context,
index,
horizontalOffsetPercentage,
verticalOffsetPercentage,
) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 540),
child: SingleChildScrollView(
child: Card(
margin: EdgeInsets.zero,
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: PostActionableItem(
item: postListState.value!.items[index],
),
),
),
),
);
},
onEnd: () {
if (postListState.value?.hasMore ?? true) {
postListNotifier.fetch(
cursor: postListState.value?.nextCursor,
);
}
},
)
: Center(child: CircularProgressIndicator()),
),
);
},
onEnd: () async {
if (postListState.value?.hasMore ?? true) {
postListNotifier.forceRefresh();
}
},
);
} else {
return Center(child: CircularProgressIndicator());
}
},
),
),
Positioned(
left: 0,

View File

@@ -12,6 +12,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:flutter_popup_card/flutter_popup_card.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
part 'picker.g.dart';
@@ -208,7 +209,7 @@ class _PackSwitcherState extends State<_PackSwitcher> {
// Content
Expanded(
child: RefreshIndicator(
child: ExtendedRefreshIndicator(
onRefresh: widget.onRefresh,
child: _StickersGrid(
pack: selectedPack,

View File

@@ -22,6 +22,7 @@
#include <record_linux/record_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin.h>
#include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <volume_controller/volume_controller_plugin.h>
@@ -74,6 +75,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) super_native_extensions_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin");
super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar);
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@@ -19,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
record_linux
sqlite3_flutter_libs
super_native_extensions
tray_manager
url_launcher_linux
volume_controller
)

View File

@@ -16,6 +16,7 @@ import firebase_core
import firebase_crashlytics
import firebase_messaging
import flutter_inappwebview_macos
import flutter_local_notifications
import flutter_platform_alert
import flutter_secure_storage_macos
import flutter_timezone
@@ -37,6 +38,7 @@ import sign_in_with_apple
import sqflite_darwin
import sqlite3_flutter_libs
import super_native_extensions
import tray_manager
import url_launcher_macos
import volume_controller
import wakelock_plus
@@ -53,6 +55,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
@@ -74,6 +77,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin"))
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))

View File

@@ -13,85 +13,85 @@ PODS:
- FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- Firebase/CoreOnly (12.0.0):
- FirebaseCore (~> 12.0.0)
- Firebase/Crashlytics (12.0.0):
- Firebase/CoreOnly (12.2.0):
- FirebaseCore (~> 12.2.0)
- Firebase/Crashlytics (12.2.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 12.0.0)
- Firebase/Messaging (12.0.0):
- FirebaseCrashlytics (~> 12.2.0)
- Firebase/Messaging (12.2.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.0.0)
- firebase_analytics (12.0.0):
- FirebaseMessaging (~> 12.2.0)
- firebase_analytics (12.0.1):
- firebase_core
- FirebaseAnalytics (= 12.0.0)
- FirebaseAnalytics (= 12.2.0)
- FlutterMacOS
- firebase_core (4.0.0):
- Firebase/CoreOnly (~> 12.0.0)
- firebase_core (4.1.0):
- Firebase/CoreOnly (~> 12.2.0)
- FlutterMacOS
- firebase_crashlytics (5.0.0):
- Firebase/CoreOnly (~> 12.0.0)
- Firebase/Crashlytics (~> 12.0.0)
- firebase_crashlytics (5.0.1):
- Firebase/CoreOnly (~> 12.2.0)
- Firebase/Crashlytics (~> 12.2.0)
- firebase_core
- FlutterMacOS
- firebase_messaging (16.0.0):
- Firebase/CoreOnly (~> 12.0.0)
- Firebase/Messaging (~> 12.0.0)
- firebase_messaging (16.0.1):
- Firebase/CoreOnly (~> 12.2.0)
- Firebase/Messaging (~> 12.2.0)
- firebase_core
- FlutterMacOS
- FirebaseAnalytics (12.0.0):
- FirebaseAnalytics/Default (= 12.0.0)
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- FirebaseAnalytics (12.2.0):
- FirebaseAnalytics/Default (= 12.2.0)
- FirebaseCore (~> 12.2.0)
- FirebaseInstallations (~> 12.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- GoogleAppMeasurement/Default (= 12.0.0)
- FirebaseAnalytics/Default (12.2.0):
- FirebaseCore (~> 12.2.0)
- FirebaseInstallations (~> 12.2.0)
- GoogleAppMeasurement/Default (= 12.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseCore (12.0.0):
- FirebaseCoreInternal (~> 12.0.0)
- FirebaseCore (12.2.0):
- FirebaseCoreInternal (~> 12.2.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseCoreInternal (12.0.0):
- FirebaseCoreExtension (12.2.0):
- FirebaseCore (~> 12.2.0)
- FirebaseCoreInternal (12.2.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseCrashlytics (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- FirebaseRemoteConfigInterop (~> 12.0.0)
- FirebaseSessions (~> 12.0.0)
- FirebaseCrashlytics (12.2.0):
- FirebaseCore (~> 12.2.0)
- FirebaseInstallations (~> 12.2.0)
- FirebaseRemoteConfigInterop (~> 12.2.0)
- FirebaseSessions (~> 12.2.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (12.2.0):
- FirebaseCore (~> 12.2.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- FirebaseMessaging (12.2.0):
- FirebaseCore (~> 12.2.0)
- FirebaseInstallations (~> 12.2.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- FirebaseRemoteConfigInterop (12.0.0)
- FirebaseSessions (12.0.0):
- FirebaseCore (~> 12.0.0)
- FirebaseCoreExtension (~> 12.0.0)
- FirebaseInstallations (~> 12.0.0)
- FirebaseRemoteConfigInterop (12.2.0)
- FirebaseSessions (12.2.0):
- FirebaseCore (~> 12.2.0)
- FirebaseCoreExtension (~> 12.2.0)
- FirebaseInstallations (~> 12.2.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
@@ -100,6 +100,8 @@ PODS:
- flutter_inappwebview_macos (0.0.1):
- FlutterMacOS
- OrderedSet (~> 6.0.3)
- flutter_local_notifications (0.0.1):
- FlutterMacOS
- flutter_platform_alert (0.0.1):
- FlutterMacOS
- flutter_secure_storage_macos (6.1.3):
@@ -109,30 +111,30 @@ PODS:
- flutter_udid (0.0.1):
- FlutterMacOS
- SAMKeychain
- flutter_webrtc (1.0.0):
- flutter_webrtc (1.1.0):
- FlutterMacOS
- WebRTC-SDK (= 137.7151.02)
- WebRTC-SDK (= 137.7151.03)
- FlutterMacOS (1.0.0)
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleAppMeasurement/Core (12.0.0):
- GoogleAppMeasurement/Core (12.2.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (12.0.0):
- GoogleAdsOnDeviceConversion (= 2.1.0)
- GoogleAppMeasurement/Core (= 12.0.0)
- GoogleAppMeasurement/IdentitySupport (= 12.0.0)
- GoogleAppMeasurement/Default (12.2.0):
- GoogleAdsOnDeviceConversion (= 2.3.0)
- GoogleAppMeasurement/Core (= 12.2.0)
- GoogleAppMeasurement/IdentitySupport (= 12.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (12.0.0):
- GoogleAppMeasurement/Core (= 12.0.0)
- GoogleAppMeasurement/IdentitySupport (12.2.0):
- GoogleAppMeasurement/Core (= 12.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
@@ -173,7 +175,7 @@ PODS:
- livekit_client (2.5.0):
- flutter_webrtc
- FlutterMacOS
- WebRTC-SDK (= 137.7151.02)
- WebRTC-SDK (= 137.7151.03)
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
@@ -237,13 +239,15 @@ PODS:
- sqlite3/session
- super_native_extensions (0.0.1):
- FlutterMacOS
- tray_manager (0.0.1):
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
- volume_controller (0.0.1):
- FlutterMacOS
- wakelock_plus (0.0.1):
- FlutterMacOS
- WebRTC-SDK (137.7151.02)
- WebRTC-SDK (137.7151.03)
DEPENDENCIES:
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
@@ -258,6 +262,7 @@ DEPENDENCIES:
- firebase_crashlytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_crashlytics/macos`)
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- flutter_platform_alert (from `Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos`)
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
- flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`)
@@ -280,6 +285,7 @@ DEPENDENCIES:
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`)
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- volume_controller (from `Flutter/ephemeral/.symlinks/plugins/volume_controller/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
@@ -332,6 +338,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
flutter_inappwebview_macos:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
flutter_local_notifications:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
flutter_platform_alert:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_platform_alert/macos
flutter_secure_storage_macos:
@@ -376,6 +384,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin
super_native_extensions:
:path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos
tray_manager:
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
volume_controller:
@@ -391,33 +401,34 @@ SPEC CHECKSUMS:
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
Firebase: 800d487043c0557d9faed71477a38d9aafb08a41
firebase_analytics: 53f0dc87ad10f56a6df8746da60d8a5fe41f886f
firebase_core: eeea10f64026b68cd0bc3dee079ab4717e22909e
firebase_crashlytics: 7be1dacc38809971354def57193b280636a3d51a
firebase_messaging: 5eefcd5bde556bfacdd9968e11c52f39032dfbe5
FirebaseAnalytics: 6d790cd1b159b4eb61a99948df0934ce505a34f7
FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a
FirebaseCoreExtension: 639afb3de6abd611952be78a794c54a47fa0f361
FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55
FirebaseCrashlytics: db75aa0cab8d00f68406fa247c32fe17ade884d7
FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988
FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde
FirebaseRemoteConfigInterop: bfa0ea72ba3dc5af739777296424e46bd6f42613
FirebaseSessions: 4e784acda213108aafef536535cdfc03504acc42
Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1
firebase_analytics: efe6e51156f4565f3791d99072e8e3b0fcca0e91
firebase_core: a8d3b82b0a87bd1d0ebc21e686b37e939c56e6e1
firebase_crashlytics: fdbe67a1229a9e583ebf2b155541491aa83927bb
firebase_messaging: 6fb526705903e2e56e38a6ff56b43668b052b01b
FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8
FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd
FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5
FirebaseCoreInternal: 56ea29f3dad2894f81b060f706f9d53509b6ed3b
FirebaseCrashlytics: f83cbf176d5c637ade108c0aacf1ccbd5ec499bf
FirebaseInstallations: 3e884b01feabdf67582a80f3250425a00979b4ed
FirebaseMessaging: 43ec73bbfedd0c385a849bb91593ab4ad4b9e48e
FirebaseRemoteConfigInterop: 0896fd52ab72586a355c8f389ff85aaa9e5375e1
FirebaseSessions: f4692789e770bec66ce17d772c0e9561c4f11737
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
flutter_timezone: d59eea86178cbd7943cd2431cc2eaa9850f935d8
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
flutter_webrtc: 0d70bd8782c19bde286dc52f766eebbea26de201
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
flutter_webrtc: 1ce7fe9a42f085286378355a575e682edd7f114d
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleAppMeasurement: 8f6ab04ad6ae493b53fcf56bd26323fb2f1384f3
GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
livekit_client: 0b0515e03858b86a7c14cc7fd6f772331f6ee84c
livekit_client: 5a5c0f1081978542bbf9a986c7ac9bffcdb73906
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
@@ -437,10 +448,11 @@ SPEC CHECKSUMS:
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
WebRTC-SDK: d20de357dcbf7c9696b124b39f3ff62125107e4b
WebRTC-SDK: 69d4e56b0b4b27d788e87bab9b9a1326ed05b1e3
PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f

View File

@@ -586,7 +586,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -674,7 +674,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@@ -724,7 +724,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;

Some files were not shown because too many files have changed in this diff Show More