Compare commits

..

88 Commits

Author SHA1 Message Date
bc9d2ab8ce 🚀 Launch 3.5.0+157 2025-12-28 00:56:40 +08:00
8bc01f1b97 Optimize call 2025-12-28 00:46:21 +08:00
200cf3ec80 💄 Better call UI 2025-12-28 00:40:20 +08:00
d910d837eb 💄 Improve of the message notifier 2025-12-27 23:59:49 +08:00
56d1f14206 💄 Optimize embed links 2025-12-27 23:51:47 +08:00
a7c8a8d2ee 💄 Better screenshot of post 2025-12-27 23:41:10 +08:00
411c71dae0 💄 Improve performance and bugs 2025-12-27 23:19:58 +08:00
a8430604f9 🐛 Fix unsubscribed status cause subscription status loading infinitly 2025-12-27 23:09:52 +08:00
fe37d219b7 💄 Optimize post subscription filter card 2025-12-27 23:07:05 +08:00
bc1ebc799a 🐛 Fix sticker filter didn't apply as expected in marketplace, close #219 2025-12-27 23:02:48 +08:00
37940ef12a 🐛 Fix sticker pack detail placeholder still using v1 2025-12-27 23:02:32 +08:00
5d0469e187 🐛 Fix the is syncing state of chat shows wrongly 2025-12-27 22:55:13 +08:00
7ad7ab53a6 👔 Make request timeout not seem as device offline 2025-12-27 22:50:40 +08:00
6b0343d3dc 🐛 Fixes the lifecycle issue of chat #211 2025-12-27 22:48:55 +08:00
f541580281 🐛 Fix cursor based loading in timeline cause duplicate data due to not covered the popularity reordered case, close #213 2025-12-27 22:38:41 +08:00
6e7eedc026 💄 Optimize chat list expansion tiles and close #210 2025-12-27 22:18:21 +08:00
5d5bda7925 ♻️ Better refresh indicator 2025-12-27 22:16:00 +08:00
48e66580c3 💄 Optimize tabs design 2025-12-27 22:07:12 +08:00
836449e3f4 ♻️ Remove fab menu 2025-12-27 22:01:54 +08:00
804dd029b1 💄 Unifined layout of dashboard cards 2025-12-27 21:32:52 +08:00
e13928b03f 🍱 Sync the translations with crowdin, close #217 2025-12-27 17:10:59 +08:00
5c14236603 📝 Update english localization file 2025-12-25 19:07:55 +08:00
738ed357bf 🚀 Launch 3.5.0+156 (SNAPSHOT) 2025-12-25 00:34:59 +08:00
0876ab9b74 💄 Optimized fortune saying and dashboard clock 2025-12-25 00:29:11 +08:00
7071399cd8 💄 Reduce the space usage in account screen 2025-12-25 00:00:25 +08:00
af23df6e48 💄 Update account screen 2025-12-24 23:59:41 +08:00
e7e7cc424b 🐛 Update the image site file list, close #204 2025-12-24 23:55:46 +08:00
56ad8f60ea Default screen 2025-12-24 23:44:15 +08:00
026dd3eb01 💄 Optimize the talker route 2025-12-24 23:16:42 +08:00
72baf0ca5c 👔 Make all network status sheet dismissable 2025-12-24 23:14:18 +08:00
82cb8c7ff9 API network request status 2025-12-24 23:08:27 +08:00
a266177628 Connection sheet got API status 2025-12-24 22:58:07 +08:00
2474c7f97c 💄 Redesign the network status sheet 2025-12-24 22:43:52 +08:00
1716afd66c 💄 Better ws reconnect experience 2025-12-24 22:33:28 +08:00
78a3cd6dd2 💄 Redesigned ws indicator 2025-12-24 22:27:02 +08:00
d655840e85 👔 No longer rapid websocket reconnect (stops after 5 times in a minute) 2025-12-24 22:15:33 +08:00
2e3e988125 🚀 Launch 3.5.0+155 2025-12-24 00:54:37 +08:00
2a94ed5171 🐛 Fix publisher avatar fallback didn't apply on featured replies 2025-12-24 00:50:59 +08:00
0948810993 Use old discovery card for the realm explore 2025-12-24 00:48:12 +08:00
689965c582 Categories and tags in subscription filter 2025-12-24 00:45:35 +08:00
ac82fdb8c8 🧱 New categories, tags subscription type 2025-12-24 00:22:59 +08:00
d94baab877 Explore subscription filter card 2025-12-23 01:03:46 +08:00
0a179acb13 ♻️ Refactored publisher subscription 2025-12-23 00:23:25 +08:00
33686b83e3 💄 Dynamic hide pinned chat if no need 2025-12-22 23:31:40 +08:00
09abe79f6a 💄 Better bottom nav, snowing animation and notification tile
💫 Animated snowing animation
2025-12-22 23:28:45 +08:00
b0b227f36b Notify with haptic feedback 2025-12-22 23:12:14 +08:00
62a45317a9 Configurable command pattle search engine 2025-12-22 23:10:37 +08:00
f727882b93 App ask for review after first time boot for 3 days 2025-12-22 23:04:47 +08:00
ba6d6ef97a Festival limited snowing animation 2025-12-22 22:44:47 +08:00
c904826c49 🚀 Launch 3.5.0+154 (SNAPSHOT) 2025-12-22 01:45:29 +08:00
595aa45378 Grouped chat 2025-12-22 00:25:29 +08:00
a481b1b82f Pinned chat room 2025-12-22 00:04:23 +08:00
2df31e4244 💄 Update the account settings to match the app settings style 2025-12-21 23:06:12 +08:00
9c1eb8e5bc 💄 Optimized settings screen 2025-12-21 23:04:15 +08:00
4d095aa333 Chat shows realm it is belongs to 2025-12-21 22:56:34 +08:00
fb62ce7735 Add tooltip to icon button in chat screen 2025-12-21 22:20:34 +08:00
b258df56c9 💄 Optimize chat appbar 2025-12-21 22:19:28 +08:00
2bf54099f9 Dashboard unread count 2025-12-21 21:57:14 +08:00
eb89d9223a 🐛 Bug fixes for the AI thought 2025-12-21 21:48:46 +08:00
87a54625aa 💄 Optimized CMP
 CMP now available to search the web
2025-12-21 16:17:41 +08:00
30b2c0a0b4 🗑️ Clean up about page 2025-12-21 16:01:12 +08:00
59c34ada40 🐛 Add realm entry to the account page, close #205 2025-12-21 15:47:19 +08:00
67a522753e 🐛 Fix safe area on dashboard, close #206 2025-12-21 13:37:49 +08:00
e6338e8a5a 🗑️ Clean up duplicate widget in explore and dashboard 2025-12-21 13:37:35 +08:00
cb7eef943c 💄 Optimized featured post 2025-12-21 13:07:44 +08:00
7a56e7882e 🌐 Localized the dashboard and the command pattle 2025-12-21 12:37:27 +08:00
b0085c2ab0 Remove unused dependecies 2025-12-21 11:57:17 +08:00
d3f990691e 🚀 Launch 3.5.0+135 (SNAPSHOT) 2025-12-21 02:56:35 +08:00
46a773cfe9 💄 Optimized loading style 2025-12-21 02:49:45 +08:00
f5fb5d8a98 Shake to show command pattle 2025-12-21 02:41:50 +08:00
4d87ca7cca 🐛 Fix bugs again 2025-12-21 02:40:48 +08:00
e16a04bd5a 🐛 Fix bugs in the new changes 2025-12-21 02:29:43 +08:00
d68b39f80f 🐛 Fix safe area 2025-12-21 02:15:46 +08:00
b7360f1f91 🍱 Update localization assets 2025-12-21 00:37:44 +08:00
5f094aca4b Better command pattle 2025-12-20 23:56:43 +08:00
6010c17900 💫 Animated command pattle 2025-12-20 22:58:53 +08:00
2ee6b3514c Command pattle search pages 2025-12-20 22:56:49 +08:00
8c83ee9b88 Command pattle basis 2025-12-20 22:49:09 +08:00
18c81503f1 📱 Dashboard supports mobile 2025-12-20 22:19:36 +08:00
53137aed3f Dashboard basis 2025-12-20 21:50:36 +08:00
b2aa8b8ec1 🐛 Fix windows app won't exit by tray 2025-12-20 17:02:53 +08:00
b13a4f5bcf ⬆️ Upgrade pods 2025-12-20 16:58:09 +08:00
8fe703ef6d ⬆️ Upgrade dependecies 2025-12-20 16:48:35 +08:00
2297fb3c47 🐛 Fix wrong update way of data 2025-12-12 00:13:26 +08:00
580663dcda Post page form 2025-12-12 00:11:52 +08:00
de20803119 Able to edit publication site config 2025-12-11 23:21:08 +08:00
fb51d2076f 🗑️ Remove pfp decoration test code 2025-12-10 23:12:02 +08:00
d8485954fa Profile decoration 2025-12-10 23:11:46 +08:00
141 changed files with 17572 additions and 9778 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -158,7 +158,7 @@
"checkIn": "Check In",
"checkInNone": "Not checked-in yet",
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
"checkInResultLevel0": "Wrost Luck",
"checkInResultLevel0": "Worst Luck",
"checkInResultLevel1": "Bad Luck",
"checkInResultLevel2": "A Normal Day",
"checkInResultLevel3": "Good Luck",
@@ -314,7 +314,6 @@
"settingsAutoTranslate": "Auto Translate",
"settingsHideBottomNav": "Hide Bottom Navigation",
"settingsSoundEffects": "Sound Effects",
"settingsAprilFoolFeatures": "April Fool Features",
"settingsEnterToSend": "Enter to Send",
"settingsTransparentAppBar": "Transparent App Bar",
"settingsCustomFonts": "Custom Fonts",
@@ -682,9 +681,9 @@
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
"postVisibility": "Post Visibility",
"currentMembershipMember": "A member of Stellar Program · {}",
"membershipPriceStellar": "1200 NSP per month, level 3+ required",
"membershipPriceNova": "2400 NSP per month, level 6+ required",
"membershipPriceSupernova": "3600 NSP per month, level 9+ required",
"membershipPriceStellar": "1200 NSP per month, level 20+ required",
"membershipPriceNova": "2400 NSP per month, level 40+ required",
"membershipPriceSupernova": "3600 NSP per month, level 60+ required",
"sharePostPhoto": "Share Post as Photo",
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
"abuseReports": "Abuse Reports",
@@ -705,7 +704,7 @@
"aboutScreenDeveloperSectionTitle": "Developer",
"aboutScreenContactUsTitle": "Contact Us",
"aboutScreenLicenseTitle": "License",
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
"aboutScreenLicenseContent": "AGPLv3",
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
@@ -1474,5 +1473,74 @@
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
"lotteryMultiplierRequired": "Please enter a multiplier",
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
"dropToShare": "Drop to share"
"dropToShare": "Drop to share",
"affiliationSpell": "Affiliation Spell",
"affiliationSpellHint": "If you have an affiliation spell, enter it here.",
"friendsOnline": "Friends Online",
"createAccountAlmostThere": "Almost There",
"createAccountAlmostThereHint": "You're one step away from joining the Solar Network! Please solve the captcha puzzle shows next.",
"createAccountNotice": "Things you need to know before you create an account:",
"createAccountConfirmEmail": "After your account being created, you need go to your email inbox to active your account to get permission to use all features.",
"createAccountNoAltAccounts": "Multiple or alternative accounts are banned from the Solar Network, that will violates our terms of services.",
"createAccountAgreeTerms": "I've read these terms and agree to the terms of service.",
"createAccountProfile": "Create your profile",
"createAccountToS": "Review Terms & Conditions",
"updateYourProfileDescription": "Adjust how you looks on the Solar Network.",
"realmsDescription": "Manage realms you've joined.",
"exploreDescription": "Explore contents on the Solar Network.",
"accountDescription": "Information about your account.",
"chatDescription": "Group Chats and Direct Messages",
"connectionServerDown": "Unable to Connect",
"appSettingsDescription": "Customize your app.",
"accountSettingsDescription": "Manage your preferences on the Solar Network.",
"walletDescription": "Your source point wallet.",
"relationshipsDescription": "Friends and connections.",
"notificationsDescription": "See what's happended related to you recently.",
"settingsFestivalFeatures": "Festival Limited Features",
"categoriesAndTags": "Categories & Tags",
"webArticlesStandDescription": "Explore external sites articles.",
"aboutDescription": "Learn more about the Solar Network.",
"abuseReportsDescription": "View and manage abuse reports.",
"stickerMarketplaceDescription": "Browse and add sticker packs from the Solar Network marketplace.",
"webFeedsDescription": "Browse and subscribe to web feeds from the Solar Network.",
"discoverRealmsDescription": "Discover new realms and join them.",
"postShuffleDescription": "Shuffle posts to see the posts randomly.",
"levelingDescription": "See your leveling progress and history.",
"notableDayToday": "{} is today!",
"authSessionLogout": "Logout Session",
"authSessionLogoutHint": "Are you sure you want to logout this session? This will terminate this specific login session.",
"filesDescription": "Manage your files on the Solar Network Drive.",
"postComposeDescription": "Compose a new post",
"searchPostsDescription": "Search posts by title, content, or else.",
"accountActivationAlert": "Activate your account",
"accountActivationAlertHint": "Unactivated account may leads to various of permission issues, activate your account by clicking the link we sent to your email inbox.",
"accountActivationResendHint": "Didn't see it? Try click the button below to resend one. If you need to update your email while your account was unactivated, feel free to contact our customer service.",
"accountActivationResend": "Resend",
"ipAddress": "IP Address",
"noFurtherData": "No further data",
"searchAnything": "Search Anything...",
"tapToViewAllNotifications": "Tap to view all notifications",
"mostRecent": "Most Recent",
"noNotificationsYet": "No notifications yet",
"recentChats": "Recent Chats",
"noFeaturedPostsAvailable": "No featured posts available",
"searchChatsAndPages": "Search chats and pages...",
"dashboard": "Dashboard",
"dashboardDescription": "All your data in one place.",
"postTagsCategories": "Post Tags and Categories",
"postTagsCategoriesDescription": "Browse posts by category and tags.",
"debugLogs": "Debug Logs",
"debugLogsDescription": "View debug logs for troubleshooting.",
"pinChatRoom": "Pin Chat Room",
"pinChatRoomDescription": "Pin this chat room to the top.",
"chatRoomPinned": "Chat room pinned successfully.",
"chatRoomUnpinned": "Chat room unpinned successfully.",
"pinnedChatRoom": "Pinned Rooms",
"settingsGroupedChatList": "Grouped Chat List",
"settingsNotifyWithHaptic": "Notification with Haptic Feedback",
"settingsDashSearchEngine": "Search Engine for web",
"settingsDashSearchEngineHelper": "Use %s as the placeholder for the query.",
"settingsDefaultScreen": "Default Screen",
"notableDayChristmas": "Christmas",
"notableDayNewYear": "New Year"
}

View File

@@ -158,7 +158,7 @@
"checkIn": "Check In",
"checkInNone": "Not checked-in yet",
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
"checkInResultLevel0": "Wrost Luck",
"checkInResultLevel0": "Worst Luck",
"checkInResultLevel1": "Bad Luck",
"checkInResultLevel2": "A Normal Day",
"checkInResultLevel3": "Good Luck",
@@ -314,7 +314,6 @@
"settingsAutoTranslate": "Auto Translate",
"settingsHideBottomNav": "Hide Bottom Navigation",
"settingsSoundEffects": "Sound Effects",
"settingsAprilFoolFeatures": "April Fool Features",
"settingsEnterToSend": "Enter to Send",
"settingsTransparentAppBar": "Transparent App Bar",
"settingsCustomFonts": "Custom Fonts",
@@ -682,9 +681,9 @@
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
"postVisibility": "Post Visibility",
"currentMembershipMember": "A member of Stellar Program · {}",
"membershipPriceStellar": "1200 NSP per month, level 3+ required",
"membershipPriceNova": "2400 NSP per month, level 6+ required",
"membershipPriceSupernova": "3600 NSP per month, level 9+ required",
"membershipPriceStellar": "1200 NSP per month, level 20+ required",
"membershipPriceNova": "2400 NSP per month, level 40+ required",
"membershipPriceSupernova": "3600 NSP per month, level 60+ required",
"sharePostPhoto": "Share Post as Photo",
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
"abuseReports": "Abuse Reports",
@@ -705,7 +704,7 @@
"aboutScreenDeveloperSectionTitle": "Developer",
"aboutScreenContactUsTitle": "Contact Us",
"aboutScreenLicenseTitle": "License",
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
"aboutScreenLicenseContent": "AGPLv3",
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
@@ -1474,5 +1473,74 @@
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
"lotteryMultiplierRequired": "Please enter a multiplier",
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
"dropToShare": "Drop to share"
"dropToShare": "Drop to share",
"affiliationSpell": "Affiliation Spell",
"affiliationSpellHint": "If you have an affiliation spell, enter it here.",
"friendsOnline": "Friends Online",
"createAccountAlmostThere": "Almost There",
"createAccountAlmostThereHint": "You're one step away from joining the Solar Network! Please solve the captcha puzzle shows next.",
"createAccountNotice": "Things you need to know before you create an account:",
"createAccountConfirmEmail": "After your account being created, you need go to your email inbox to active your account to get permission to use all features.",
"createAccountNoAltAccounts": "Multiple or alternative accounts are banned from the Solar Network, that will violates our terms of services.",
"createAccountAgreeTerms": "I've read these terms and agree to the terms of service.",
"createAccountProfile": "Create your profile",
"createAccountToS": "Review Terms & Conditions",
"updateYourProfileDescription": "Adjust how you looks on the Solar Network.",
"realmsDescription": "Manage realms you've joined.",
"exploreDescription": "Explore contents on the Solar Network.",
"accountDescription": "Information about your account.",
"chatDescription": "Group Chats and Direct Messages",
"connectionServerDown": "Unable to Connect",
"appSettingsDescription": "Customize your app.",
"accountSettingsDescription": "Manage your preferences on the Solar Network.",
"walletDescription": "Your source point wallet.",
"relationshipsDescription": "Friends and connections.",
"notificationsDescription": "See what's happended related to you recently.",
"settingsFestivalFeatures": "Festival Limited Features",
"categoriesAndTags": "Categories & Tags",
"webArticlesStandDescription": "Explore external sites articles.",
"aboutDescription": "Learn more about the Solar Network.",
"abuseReportsDescription": "View and manage abuse reports.",
"stickerMarketplaceDescription": "Browse and add sticker packs from the Solar Network marketplace.",
"webFeedsDescription": "Browse and subscribe to web feeds from the Solar Network.",
"discoverRealmsDescription": "Discover new realms and join them.",
"postShuffleDescription": "Shuffle posts to see the posts randomly.",
"levelingDescription": "See your leveling progress and history.",
"notableDayToday": "{} is today!",
"authSessionLogout": "Logout Session",
"authSessionLogoutHint": "Are you sure you want to logout this session? This will terminate this specific login session.",
"filesDescription": "Manage your files on the Solar Network Drive.",
"postComposeDescription": "Compose a new post",
"searchPostsDescription": "Search posts by title, content, or else.",
"accountActivationAlert": "Activate your account",
"accountActivationAlertHint": "Unactivated account may leads to various of permission issues, activate your account by clicking the link we sent to your email inbox.",
"accountActivationResendHint": "Didn't see it? Try click the button below to resend one. If you need to update your email while your account was unactivated, feel free to contact our customer service.",
"accountActivationResend": "Resend",
"ipAddress": "IP Address",
"noFurtherData": "No further data",
"searchAnything": "Search Anything...",
"tapToViewAllNotifications": "Tap to view all notifications",
"mostRecent": "Most Recent",
"noNotificationsYet": "No notifications yet",
"recentChats": "Recent Chats",
"noFeaturedPostsAvailable": "No featured posts available",
"searchChatsAndPages": "Search chats and pages...",
"dashboard": "Dashboard",
"dashboardDescription": "All your data in one place.",
"postTagsCategories": "Post Tags and Categories",
"postTagsCategoriesDescription": "Browse posts by category and tags.",
"debugLogs": "Debug Logs",
"debugLogsDescription": "View debug logs for troubleshooting.",
"pinChatRoom": "Pin Chat Room",
"pinChatRoomDescription": "Pin this chat room to the top.",
"chatRoomPinned": "Chat room pinned successfully.",
"chatRoomUnpinned": "Chat room unpinned successfully.",
"pinnedChatRoom": "Pinned Rooms",
"settingsGroupedChatList": "Grouped Chat List",
"settingsNotifyWithHaptic": "Notification with Haptic Feedback",
"settingsDashSearchEngine": "Search Engine for web",
"settingsDashSearchEngineHelper": "Use %s as the placeholder for the query.",
"settingsDefaultScreen": "Default Screen",
"notableDayChristmas": "Christmas",
"notableDayNewYear": "New Year"
}

View File

@@ -158,7 +158,7 @@
"checkIn": "Check In",
"checkInNone": "Not checked-in yet",
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
"checkInResultLevel0": "Wrost Luck",
"checkInResultLevel0": "Worst Luck",
"checkInResultLevel1": "Bad Luck",
"checkInResultLevel2": "A Normal Day",
"checkInResultLevel3": "Good Luck",
@@ -314,7 +314,6 @@
"settingsAutoTranslate": "Auto Translate",
"settingsHideBottomNav": "Hide Bottom Navigation",
"settingsSoundEffects": "Sound Effects",
"settingsAprilFoolFeatures": "April Fool Features",
"settingsEnterToSend": "Enter to Send",
"settingsTransparentAppBar": "Transparent App Bar",
"settingsCustomFonts": "Custom Fonts",
@@ -682,9 +681,9 @@
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
"postVisibility": "Post Visibility",
"currentMembershipMember": "A member of Stellar Program · {}",
"membershipPriceStellar": "1200 NSP per month, level 3+ required",
"membershipPriceNova": "2400 NSP per month, level 6+ required",
"membershipPriceSupernova": "3600 NSP per month, level 9+ required",
"membershipPriceStellar": "1200 NSP per month, level 20+ required",
"membershipPriceNova": "2400 NSP per month, level 40+ required",
"membershipPriceSupernova": "3600 NSP per month, level 60+ required",
"sharePostPhoto": "Share Post as Photo",
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
"abuseReports": "Abuse Reports",
@@ -705,7 +704,7 @@
"aboutScreenDeveloperSectionTitle": "Developer",
"aboutScreenContactUsTitle": "Contact Us",
"aboutScreenLicenseTitle": "License",
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
"aboutScreenLicenseContent": "AGPLv3",
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
@@ -1474,5 +1473,74 @@
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
"lotteryMultiplierRequired": "Please enter a multiplier",
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
"dropToShare": "Drop to share"
"dropToShare": "Drop to share",
"affiliationSpell": "Affiliation Spell",
"affiliationSpellHint": "If you have an affiliation spell, enter it here.",
"friendsOnline": "Friends Online",
"createAccountAlmostThere": "Almost There",
"createAccountAlmostThereHint": "You're one step away from joining the Solar Network! Please solve the captcha puzzle shows next.",
"createAccountNotice": "Things you need to know before you create an account:",
"createAccountConfirmEmail": "After your account being created, you need go to your email inbox to active your account to get permission to use all features.",
"createAccountNoAltAccounts": "Multiple or alternative accounts are banned from the Solar Network, that will violates our terms of services.",
"createAccountAgreeTerms": "I've read these terms and agree to the terms of service.",
"createAccountProfile": "Create your profile",
"createAccountToS": "Review Terms & Conditions",
"updateYourProfileDescription": "Adjust how you looks on the Solar Network.",
"realmsDescription": "Manage realms you've joined.",
"exploreDescription": "Explore contents on the Solar Network.",
"accountDescription": "Information about your account.",
"chatDescription": "Group Chats and Direct Messages",
"connectionServerDown": "Unable to Connect",
"appSettingsDescription": "Customize your app.",
"accountSettingsDescription": "Manage your preferences on the Solar Network.",
"walletDescription": "Your source point wallet.",
"relationshipsDescription": "Friends and connections.",
"notificationsDescription": "See what's happended related to you recently.",
"settingsFestivalFeatures": "Festival Limited Features",
"categoriesAndTags": "Categories & Tags",
"webArticlesStandDescription": "Explore external sites articles.",
"aboutDescription": "Learn more about the Solar Network.",
"abuseReportsDescription": "View and manage abuse reports.",
"stickerMarketplaceDescription": "Browse and add sticker packs from the Solar Network marketplace.",
"webFeedsDescription": "Browse and subscribe to web feeds from the Solar Network.",
"discoverRealmsDescription": "Discover new realms and join them.",
"postShuffleDescription": "Shuffle posts to see the posts randomly.",
"levelingDescription": "See your leveling progress and history.",
"notableDayToday": "{} is today!",
"authSessionLogout": "Logout Session",
"authSessionLogoutHint": "Are you sure you want to logout this session? This will terminate this specific login session.",
"filesDescription": "Manage your files on the Solar Network Drive.",
"postComposeDescription": "Compose a new post",
"searchPostsDescription": "Search posts by title, content, or else.",
"accountActivationAlert": "Activate your account",
"accountActivationAlertHint": "Unactivated account may leads to various of permission issues, activate your account by clicking the link we sent to your email inbox.",
"accountActivationResendHint": "Didn't see it? Try click the button below to resend one. If you need to update your email while your account was unactivated, feel free to contact our customer service.",
"accountActivationResend": "Resend",
"ipAddress": "IP Address",
"noFurtherData": "No further data",
"searchAnything": "Search Anything...",
"tapToViewAllNotifications": "Tap to view all notifications",
"mostRecent": "Most Recent",
"noNotificationsYet": "No notifications yet",
"recentChats": "Recent Chats",
"noFeaturedPostsAvailable": "No featured posts available",
"searchChatsAndPages": "Search chats and pages...",
"dashboard": "Dashboard",
"dashboardDescription": "All your data in one place.",
"postTagsCategories": "Post Tags and Categories",
"postTagsCategoriesDescription": "Browse posts by category and tags.",
"debugLogs": "Debug Logs",
"debugLogsDescription": "View debug logs for troubleshooting.",
"pinChatRoom": "Pin Chat Room",
"pinChatRoomDescription": "Pin this chat room to the top.",
"chatRoomPinned": "Chat room pinned successfully.",
"chatRoomUnpinned": "Chat room unpinned successfully.",
"pinnedChatRoom": "Pinned Rooms",
"settingsGroupedChatList": "Grouped Chat List",
"settingsNotifyWithHaptic": "Notification with Haptic Feedback",
"settingsDashSearchEngine": "Search Engine for web",
"settingsDashSearchEngineHelper": "Use %s as the placeholder for the query.",
"settingsDefaultScreen": "Default Screen",
"notableDayChristmas": "Christmas",
"notableDayNewYear": "New Year"
}

File diff suppressed because it is too large Load Diff

View File

@@ -314,7 +314,6 @@
"settingsAutoTranslate": "自轉譯",
"settingsHideBottomNav": "隱底航",
"settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "謔辰妙能",
"settingsEnterToSend": "叩回車鍵即送",
"settingsTransparentAppBar": "透光應用欄",
"settingsCustomFonts": "自定字體",
@@ -328,13 +327,13 @@
"accountDeletion": "除去此账户",
"accountDeletionHint": "汝已存心除此账户?如已一意,信将至汝邮址,随信中之言续此行。",
"accountDeletionSent": "除此账户之信已送至,请于汝之邮址查阅。",
"accountSecurityTitle": "Security",
"accountDangerZoneTitle": "Danger Zone",
"accountSecurityTitle": "保护措施",
"accountDangerZoneTitle": "危险地区",
"accountPassword": "密碼",
"accountPasswordDescription": "變更账户密碼",
"accountPasswordChange": "變更密碼",
"accountPasswordChangeSent": "Password reset link sent, please check your email inbox.",
"accountPasswordChangeDescription": "We will send an email to your primary email address to reset your password.",
"accountPasswordChangeSent": "密码重置链接已发送,请查看您的电子邮箱收件箱。",
"accountPasswordChangeDescription": "我们将向您的主要电子邮箱发送一封邮件以重置您的密码。",
"accountAuthFactor": "Auth factors",
"accountAuthFactorDescription": "Multi-factor authentication to ensure safety and convience",
"accountDeletionDescription": "Permanently delete your account and all your data",
@@ -682,9 +681,9 @@
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
"postVisibility": "Post Visibility",
"currentMembershipMember": "A member of Stellar Program · {}",
"membershipPriceStellar": "1200 NSP per month, level 3+ required",
"membershipPriceNova": "2400 NSP per month, level 6+ required",
"membershipPriceSupernova": "3600 NSP per month, level 9+ required",
"membershipPriceStellar": "1200 NSP per month, level 20+ required",
"membershipPriceNova": "2400 NSP per month, level 40+ required",
"membershipPriceSupernova": "3600 NSP per month, level 60+ required",
"sharePostPhoto": "Share Post as Photo",
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
"abuseReports": "Abuse Reports",
@@ -705,7 +704,7 @@
"aboutScreenDeveloperSectionTitle": "Developer",
"aboutScreenContactUsTitle": "Contact Us",
"aboutScreenLicenseTitle": "License",
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
"aboutScreenLicenseContent": "AGPLv3",
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
@@ -1474,5 +1473,74 @@
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
"lotteryMultiplierRequired": "Please enter a multiplier",
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
"dropToShare": "Drop to share"
"dropToShare": "Drop to share",
"affiliationSpell": "Affiliation Spell",
"affiliationSpellHint": "If you have an affiliation spell, enter it here.",
"friendsOnline": "Friends Online",
"createAccountAlmostThere": "Almost There",
"createAccountAlmostThereHint": "You're one step away from joining the Solar Network! Please solve the captcha puzzle shows next.",
"createAccountNotice": "Things you need to know before you create an account:",
"createAccountConfirmEmail": "After your account being created, you need go to your email inbox to active your account to get permission to use all features.",
"createAccountNoAltAccounts": "Multiple or alternative accounts are banned from the Solar Network, that will violates our terms of services.",
"createAccountAgreeTerms": "I've read these terms and agree to the terms of service.",
"createAccountProfile": "Create your profile",
"createAccountToS": "Review Terms & Conditions",
"updateYourProfileDescription": "Adjust how you looks on the Solar Network.",
"realmsDescription": "Manage realms you've joined.",
"exploreDescription": "Explore contents on the Solar Network.",
"accountDescription": "Information about your account.",
"chatDescription": "Group Chats and Direct Messages",
"connectionServerDown": "Unable to Connect",
"appSettingsDescription": "Customize your app.",
"accountSettingsDescription": "Manage your preferences on the Solar Network.",
"walletDescription": "Your source point wallet.",
"relationshipsDescription": "Friends and connections.",
"notificationsDescription": "See what's happended related to you recently.",
"settingsFestivalFeatures": "Festival Limited Features",
"categoriesAndTags": "Categories & Tags",
"webArticlesStandDescription": "Explore external sites articles.",
"aboutDescription": "Learn more about the Solar Network.",
"abuseReportsDescription": "View and manage abuse reports.",
"stickerMarketplaceDescription": "Browse and add sticker packs from the Solar Network marketplace.",
"webFeedsDescription": "Browse and subscribe to web feeds from the Solar Network.",
"discoverRealmsDescription": "Discover new realms and join them.",
"postShuffleDescription": "Shuffle posts to see the posts randomly.",
"levelingDescription": "See your leveling progress and history.",
"notableDayToday": "{} is today!",
"authSessionLogout": "Logout Session",
"authSessionLogoutHint": "Are you sure you want to logout this session? This will terminate this specific login session.",
"filesDescription": "Manage your files on the Solar Network Drive.",
"postComposeDescription": "Compose a new post",
"searchPostsDescription": "Search posts by title, content, or else.",
"accountActivationAlert": "Activate your account",
"accountActivationAlertHint": "Unactivated account may leads to various of permission issues, activate your account by clicking the link we sent to your email inbox.",
"accountActivationResendHint": "Didn't see it? Try click the button below to resend one. If you need to update your email while your account was unactivated, feel free to contact our customer service.",
"accountActivationResend": "Resend",
"ipAddress": "IP Address",
"noFurtherData": "No further data",
"searchAnything": "Search Anything...",
"tapToViewAllNotifications": "Tap to view all notifications",
"mostRecent": "Most Recent",
"noNotificationsYet": "No notifications yet",
"recentChats": "Recent Chats",
"noFeaturedPostsAvailable": "No featured posts available",
"searchChatsAndPages": "Search chats and pages...",
"dashboard": "Dashboard",
"dashboardDescription": "All your data in one place.",
"postTagsCategories": "Post Tags and Categories",
"postTagsCategoriesDescription": "Browse posts by category and tags.",
"debugLogs": "Debug Logs",
"debugLogsDescription": "View debug logs for troubleshooting.",
"pinChatRoom": "Pin Chat Room",
"pinChatRoomDescription": "Pin this chat room to the top.",
"chatRoomPinned": "Chat room pinned successfully.",
"chatRoomUnpinned": "Chat room unpinned successfully.",
"pinnedChatRoom": "Pinned Rooms",
"settingsGroupedChatList": "Grouped Chat List",
"settingsNotifyWithHaptic": "Notification with Haptic Feedback",
"settingsDashSearchEngine": "Search Engine for web",
"settingsDashSearchEngineHelper": "Use %s as the placeholder for the query.",
"settingsDefaultScreen": "Default Screen",
"notableDayChristmas": "Christmas",
"notableDayNewYear": "New Year"
}

View File

@@ -314,7 +314,6 @@
"settingsAutoTranslate": "自動翻譯",
"settingsHideBottomNav": "隱藏底部導航",
"settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "愚人節功能",
"settingsEnterToSend": "按下 Enter 發送",
"settingsTransparentAppBar": "使用完全透明的狀態欄",
"settingsCustomFonts": "自定義字體",
@@ -682,9 +681,9 @@
"articleAttachmentHint": "附件必須上傳並插入到文章主體中才能顯示出來。",
"postVisibility": "可見性",
"currentMembershipMember": "恆星計劃成員 · {}",
"membershipPriceStellar": "需要用戶等級 3+,每月價格 1200 NSP",
"membershipPriceNova": "需要用戶等級 6+,每月價格 2400 NSP",
"membershipPriceSupernova": "需要用戶等級 9+,每月價格 3600 NSP",
"membershipPriceStellar": "需要用戶等級 20+,每月價格 1200 NSP",
"membershipPriceNova": "需要用戶等級 40+,每月價格 2400 NSP",
"membershipPriceSupernova": "需要用戶等級 60+,每月價格 3600 NSP",
"sharePostPhoto": "通過圖片分享帖子",
"wouldYouLikeToNavigateToChat": "你想要前往聊天頁面嗎?",
"abuseReports": "舉報",
@@ -1060,7 +1059,7 @@
"failedToDeleteRecycledFiles": "已回收檔案刪除失敗",
"upload": "上傳",
"deleteMessage": "刪除訊息",
"deleteMessageConfirmation": "確定要刪除此郵件嗎?",
"deleteMessageConfirmation": "確定要刪除此郵件嗎",
"customReaction": "自訂反應",
"customReactions": "自訂反應",
"stickerPlaceholder": "貼紙佔位符",
@@ -1131,348 +1130,417 @@
"noReceivedGifts": "没有收到过礼物",
"stellarGift": "恆星禮物",
"novaGift": "新星禮物",
"supernovaGift": "Supernova Gift",
"sameAsMembership": "Same as membership",
"enterGiftCodeToRedeem": "Enter gift code to redeem",
"enterGiftCode": "Enter gift code",
"giftPurchased": "Gift Purchased!",
"shareCodeWithRecipient": "Share this code with the recipient to redeem the gift.",
"openGiftAnyoneCanRedeem": "This is an open gift that anyone can redeem.",
"ok": "OK",
"selectedRecipient": "Selected recipient",
"noRecipientSelected": "No recipient selected",
"thisWillBeAnOpenGift": "This will be an open gift",
"personalMessage": "Personal Message",
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
"giftStatusCreated": "Created",
"giftStatusSent": "Sent",
"giftStatusRedeemed": "Redeemed",
"giftStatusCancelled": "Cancelled",
"giftStatusExpired": "Expired",
"giftStatusUnknown": "Unknown",
"giftCodeCopiedToClipboard": "Gift code copied to clipboard",
"codeLabel": "Code: ",
"subscriptionLabel": "Subscription: ",
"toLabel": "To: ",
"fromLabel": "From: ",
"messageLabel": "Message: ",
"giftRedeemed": "Gift Redeemed!",
"giftRedeemedSuccessfully": "You have successfully redeemed the gift. Your new subscription is now active.",
"cancelGift": "Cancel Gift",
"cancelGiftConfirm": "Are you sure you want to cancel this gift? This action cannot be undone.",
"giftCancelledSuccessfully": "Gift cancelled successfully",
"createFund": "Create Fund",
"fundAmount": "Fund Amount",
"enterAmount": "Enter Amount",
"selectCurrency": "Select Currency",
"splitType": "Split Type",
"evenSplit": "Even Split",
"equalAmountEach": "Equal amount for each recipient",
"randomSplit": "Random Split",
"randomAmountEach": "Random amount for each recipient",
"recipientCount": "Recipient Count",
"numberOfRecipients": "Number of Recipients",
"addPersonalMessageForRecipients": "Add a personal message for recipients",
"invalidAmount": "Invalid amount",
"invalidRecipientCount": "Invalid recipient count",
"fundOverview": "Fund Overview",
"totalFundsSent": "Total Funds Sent",
"totalFundsReceived": "Total Funds Received",
"transactions": "Transactions",
"myFunds": "My Funds",
"availableFunds": "Available Funds",
"fundStatusCreated": "Created",
"fundStatusPartial": "Partially Claimed",
"fundStatusCompleted": "Fully Claimed",
"fundStatusExpired": "Expired",
"fundStatusUnknown": "Unknown",
"recipients": "Recipients",
"fundClaimedSuccessfully": "Fund claimed successfully!",
"claim": "Claim",
"noFundsCreated": "No funds created yet",
"createYourFirstFund": "Create your first fund to get started",
"noAvailableFunds": "No available funds",
"fundsWillAppearHere": "Funds you can claim will appear here",
"fundCreatedSuccessfully": "Fund created successfully!",
"selectRecipients": "Select Recipients",
"noRecipientsSelected": "No recipients selected",
"selectRecipientsToSendFund": "Select recipients to send the fund to",
"addRecipient": "Add Recipient",
"addMoreRecipients": "Add More Recipients",
"transactionDetails": "Transaction Details",
"remarks": "Remarks",
"payer": "Payer",
"payee": "Payee",
"transactionType": "Transaction Type",
"transfer": "Transfer",
"payment": "Payment",
"systemWallet": "System Wallet",
"date": "Date",
"createTransfer": "Create Transfer",
"transferAmount": "Transfer Amount",
"selectPayee": "Select Payee",
"selectedPayee": "Selected Payee",
"noPayeeSelected": "No payee selected",
"selectPayeeToTransfer": "Select payee to transfer to",
"addRemark": "Add Remark",
"transferRemark": "Transfer Remark",
"addRemarkForTransfer": "Add remark for transfer",
"enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer",
"transferCreatedSuccessfully": "Transfer created successfully!",
"postUpdate": "Update",
"fileMetadata": "File Metadata",
"resend": "Resend",
"fileInfoTitle": "File Information",
"download": "Download",
"info": "Info",
"noStickers": "No Stickers",
"noStickersInPack": "This pack does not contains stickers",
"noStickerPacks": "No Sticker Packs",
"refresh": "Refresh",
"spoiler": "Spoiler",
"activityHeatmap": "Activity Heatmap",
"custom": "Custom",
"usernameColor": "Username Color",
"colorType": "Color Type",
"plain": "Plain",
"gradient": "Gradient",
"colorValue": "Color Value",
"gradientDirection": "Gradient Direction",
"gradientDirectionToRight": "To Right",
"gradientDirectionToLeft": "To Left",
"gradientDirectionToBottom": "To Bottom",
"gradientDirectionToTop": "To Top",
"gradientDirectionToBottomRight": "To Bottom Right",
"gradientDirectionToBottomLeft": "To Bottom Left",
"gradientDirectionToTopRight": "To Top Right",
"gradientDirectionToTopLeft": "To Top Left",
"gradientColors": "Gradient Colors",
"color": "Color",
"addColor": "Add Color",
"availableWithYourPlan": "Available with your plan",
"upgradeRequired": "Upgrade required",
"settingsDisableAnimation": "Disable Animation",
"addTag": "Add Tag",
"supernovaGift": "超新星訂閱",
"sameAsMembership": "於成員相同",
"enterGiftCodeToRedeem": "輸入禮物程式碼以兌換",
"enterGiftCode": "輸入禮物程式碼",
"giftPurchased": "已購買禮物!",
"shareCodeWithRecipient": "與收件人分享此程式碼來兌換禮物。",
"openGiftAnyoneCanRedeem": "這是一份任何人都可以兌換的公開禮物。",
"ok": "好的",
"selectedRecipient": "選擇接收者",
"noRecipientSelected": "沒有選中的接受者",
"thisWillBeAnOpenGift": "這將是一份公開的禮物",
"personalMessage": "個人信息",
"addPersonalMessageForRecipient": "為收件人添加個人訊息",
"giftStatusCreated": "已創建",
"giftStatusSent": "發送",
"giftStatusRedeemed": "已兌換",
"giftStatusCancelled": "已取消",
"giftStatusExpired": "已過期",
"giftStatusUnknown": "未知",
"giftCodeCopiedToClipboard": "禮物程式碼已經複製到剪貼簿",
"codeLabel": "程式碼:",
"subscriptionLabel": "訂閱:",
"toLabel": "至:",
"fromLabel": "從:",
"messageLabel": "消息:",
"giftRedeemed": "禮物已兌換!",
"giftRedeemedSuccessfully": "您已成功兌換了禮物。您的新訂閱現在已經生效。",
"cancelGift": "取消禮物",
"cancelGiftConfirm": "您確定要取消此禮物?此操作無法撤銷。",
"giftCancelledSuccessfully": "禮物成功取消",
"createFund": "創建支票",
"fundAmount": "支票金額",
"enterAmount": "輸入金額",
"selectCurrency": "選擇貨幣",
"splitType": "拆分類型",
"evenSplit": "平均拆分",
"equalAmountEach": "每個收款人的金額相同",
"randomSplit": "隨機拆分",
"randomAmountEach": "每個收款人的金額隨機",
"recipientCount": "收款人總計",
"numberOfRecipients": "收款人數量",
"addPersonalMessageForRecipients": "為收款人添加個人信息",
"invalidAmount": "無效的金額",
"invalidRecipientCount": "收款人數量無效",
"fundOverview": "支票概覽",
"totalFundsSent": "共發送支票",
"totalFundsReceived": "共領取支票",
"transactions": "交易",
"myFunds": "我的支票",
"availableFunds": "可用支票",
"fundStatusCreated": "已創建",
"fundStatusPartial": "部分領取",
"fundStatusCompleted": "已領完",
"fundStatusExpired": "已過期",
"fundStatusUnknown": "未知",
"recipients": "收款人",
"fundClaimedSuccessfully": "支票成功領取!",
"claim": "領取",
"noFundsCreated": "還沒有創建的支票",
"createYourFirstFund": "創建您的第一個支票以開始",
"noAvailableFunds": "暫無可用支票",
"fundsWillAppearHere": "您可以領取的支票將出現在這裡",
"fundCreatedSuccessfully": "支票成功創建!",
"selectRecipients": "選擇收款人",
"noRecipientsSelected": "沒有選擇收款人",
"selectRecipientsToSendFund": "選擇收款人將支票發送到",
"addRecipient": "添加收款人",
"addMoreRecipients": "添加更多收款人",
"transactionDetails": "交易詳情",
"remarks": "備註",
"payer": "付款方",
"payee": "交易方",
"transactionType": "交易類型",
"transfer": "轉帳",
"payment": "支付",
"systemWallet": "系統錢包",
"date": "日期",
"createTransfer": "創建交易",
"transferAmount": "交易金額",
"selectPayee": "請選擇收款人",
"selectedPayee": "選定的收款人",
"noPayeeSelected": "沒有選擇收款人",
"selectPayeeToTransfer": "選擇要轉帳的收款人",
"addRemark": "添加備註",
"transferRemark": "交易備註",
"addRemarkForTransfer": "為轉帳添加備註",
"enterPinToConfirmTransfer": "輸入您的 6 位PIN碼以確認轉帳",
"transferCreatedSuccessfully": "轉帳成功創建",
"postUpdate": "更新",
"fileMetadata": "檔案資訊",
"resend": "重新發送",
"fileInfoTitle": "檔案信息",
"download": "下載",
"info": "信息",
"noStickers": "沒有貼圖",
"noStickersInPack": "這個包不包含貼圖",
"noStickerPacks": "沒有貼圖包",
"refresh": "刷新",
"spoiler": "已隱藏",
"activityHeatmap": "活动热力图",
"custom": "自定義",
"usernameColor": "用戶名顏色",
"colorType": "顏色類型",
"plain": "純色",
"gradient": "漸變",
"colorValue": "色值",
"gradientDirection": "漸變方向",
"gradientDirectionToRight": "向右",
"gradientDirectionToLeft": "向左",
"gradientDirectionToBottom": "向底部",
"gradientDirectionToTop": "向上",
"gradientDirectionToBottomRight": "向右下角",
"gradientDirectionToBottomLeft": "向左下角",
"gradientDirectionToTopRight": "向右上角",
"gradientDirectionToTopLeft": "向左下角",
"gradientColors": "漸變顏色",
"color": "顏色",
"addColor": "添加顏色",
"availableWithYourPlan": "隨您的方案提供",
"upgradeRequired": "需要升級",
"settingsDisableAnimation": "停用動畫",
"addTag": "添加標籤",
"accountConnectionProviderSpotify": "Spotify",
"accountConnectionProviderSteam": "Steam",
"timezoneNotFound": "Time zone not found",
"awardPoints": "Awarded {} points",
"postFeaturedOn": "Post featured on {}",
"messageSentAt": "Sent at {}",
"myTickets": "My Tickets",
"drawHistory": "Draw History",
"lottery": "Lottery",
"noLotteryTickets": "No lottery tickets yet",
"buyYourFirstTicket": "Buy your first lottery ticket to get started!",
"buyTicket": "Buy Ticket",
"ticketNumbers": "Numbers: {}, Special: {}",
"cost": "Cost",
"multiplier": "Multiplier",
"prizeWon": "Prize Won",
"pending": "Pending",
"drawn": "Drawn",
"won": "Won",
"lost": "Lost",
"noDrawHistory": "No draw history yet",
"buyLotteryTicket": "Buy Lottery Ticket",
"selectNumbers": "Select Numbers",
"select5UniqueNumbers": "Select 5 unique numbers",
"selectSpecialNumber": "Select Special Number",
"selectMultiplier": "Select Multiplier",
"baseCost": "Base Cost",
"totalCost": "Total Cost",
"prizeStructure": "Prize Structure",
"enterPinToConfirmPurchase": "Enter your PIN to confirm purchase",
"ticketPurchasedSuccessfully": "Ticket purchased successfully!",
"winningNumbers": "Winning Numbers",
"specialNumber": "Special Number",
"totalTickets": "Total Tickets",
"totalWinners": "Total Winners",
"prizePool": "Prize Pool",
"enterPinToConfirmPayment": "Enter your PIN code to confirm payment",
"purchase": "Purchase",
"multiplierLabel": "Multiplier",
"specialOnly": "Special Only",
"matches": "Matches",
"thoughtDefaultTopic": "Reflection",
"thoughtAiName": "SN-chan",
"thoughtUserName": "You",
"thoughtStreamingHint": "Sn-chan is thinking...",
"thoughtInputHint": "Ask sn-chan anything...",
"thoughtNewConversation": "Start New Conversation",
"thoughtParseError": "Failed to parse AI response",
"thoughtFunctionCall": "Use {}",
"aiThought": "AI Thought",
"aiThoughtTitle": "Let sn-chan think",
"postReferenceUnavailable": "Referenced post is unavailable",
"fabLocation": "FAB Location",
"activities": "Activities",
"presenceTypeGaming": "Playing",
"presenceTypeMusic": "Listening to Music",
"presenceTypeWorkout": "Working out",
"articleCompose": "Compose Article",
"backToHub": "Back to Hub",
"advancedFilters": "Advanced Filters",
"searchPosts": "Search Posts",
"sortBy": "Sort by",
"fromDate": "From Date",
"toDate": "To Date",
"popularity": "Popularity",
"descendingOrder": "Descending Order",
"selectDate": "Select Date",
"pinnedPosts": "Pinned Posts",
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
"publicationSites": "Publication Sites",
"uploadTasks": "Upload Tasks",
"thoughtFunctionCallBegin": "Calling tool {}",
"thoughtFunctionCallFinish": "{} responded",
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
"more": "More",
"collapse": "Collapse",
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
"timezoneNotFound": "找不到時區",
"awardPoints": "獎賞 {} 點",
"postFeaturedOn": "帖文在 {} 被精選",
"messageSentAt": "發送在 {}",
"myTickets": "我的彩票",
"drawHistory": "開獎歷史",
"lottery": "彩票",
"noLotteryTickets": "還沒有彩票",
"buyYourFirstTicket": "購買您的第一張彩票以開始!",
"buyTicket": "買彩票",
"ticketNumbers": "數字:{},特殊數字:{}",
"cost": "花費",
"multiplier": "倍率",
"prizeWon": "獲勝者",
"pending": "待開獎",
"drawn": "已開獎",
"won": "獲勝",
"lost": "失敗",
"noDrawHistory": "還沒有開獎曆史",
"buyLotteryTicket": "購買彩票",
"selectNumbers": "選擇數字",
"select5UniqueNumbers": "選擇 5 個不同的數字",
"selectSpecialNumber": "選擇特殊數字",
"selectMultiplier": "選擇倍率",
"baseCost": "基礎花費",
"totalCost": "總費用",
"prizeStructure": "獎金分級",
"enterPinToConfirmPurchase": "輸入您的 PIN 碼以確認購買",
"ticketPurchasedSuccessfully": "彩票購買成功!",
"winningNumbers": "獲勝數字",
"specialNumber": "特殊數字",
"totalTickets": "總售出票數",
"totalWinners": "總中獎者",
"prizePool": "獎池",
"enterPinToConfirmPayment": "輸入您的 PIN 碼以確認交易",
"purchase": "購買",
"multiplierLabel": "倍率",
"specialOnly": "僅特殊數字",
"matches": "場次",
"thoughtDefaultTopic": "尋思",
"thoughtAiName": "SN",
"thoughtUserName": "",
"thoughtStreamingHint": "SN醬正在思考……",
"thoughtInputHint": "問SN醬一些東西……",
"thoughtNewConversation": "開始新對話",
"thoughtParseError": "解析 AI 響應失敗",
"thoughtFunctionCall": "使用 {}",
"aiThought": "尋思",
"aiThoughtTitle": "讓SN醬思考",
"postReferenceUnavailable": "應用的帖子不可用",
"fabLocation": "底部菜單按鈕位置",
"activities": "活動",
"presenceTypeGaming": "正在玩",
"presenceTypeMusic": "正在聽音樂",
"presenceTypeWorkout": "鍛煉中",
"articleCompose": "撰寫文章",
"backToHub": "返回至主頁",
"advancedFilters": "高級篩選",
"searchPosts": "搜索帖子",
"sortBy": "排序方式",
"fromDate": "起始日期",
"toDate": "截止日期",
"popularity": "按熱度",
"descendingOrder": "降序排序",
"selectDate": "選擇日期",
"pinnedPosts": "已置頂的帖子",
"customReactionHint": "自訂反應允許你使用用戶上傳貼紙作為帖子反應的符號,需要恆星計劃訂閱。",
"publicationSites": "發佈者網站",
"uploadTasks": "上傳任務",
"thoughtFunctionCallBegin": "調用工具 {}",
"thoughtFunctionCallFinish": "工具 {}",
"thoughtUnpaidHint": "尋思因為有未支付的訂單而被禁用",
"more": "更多",
"collapse": "折疊",
"pollConfirmDiscard": "您確定要離開嗎?您編輯的所有資料都不會被保存。",
"discard": "Discard",
"fund": "Fund",
"fundsRecent": "Recent Funds",
"fundCreateNew": "Create New",
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
"amountOfSplits": "Amount of Splits",
"enterNumberOfSplits": "Enter Splits Amount",
"orCreateWith": "Or\ncreate with",
"unindexedFiles": "Unindexed files",
"folder": "Folder",
"clearCompleted": "Clear Completed",
"uploadSuccess": "Upload successful!",
"wouldYouLikeToViewFile": "Would you like to view the file?",
"contentCantEmpty": "Content cannot be empty",
"features": "Features",
"unnamed": "Unnamed",
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
"fundEnvelope": "Fund Envelope",
"fundEnvelopeRemaining": "Remaining: {} {}",
"fundEnvelopeSplit": "Split: {}",
"fundEnvelopeSplitEvenly": "Evenly",
"fundEnvelopeSplitRandomly": "Randomly",
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
"fundEnvelopeStatusCreated": "Created",
"fundEnvelopeStatusPartial": "Partially Claimed",
"fundEnvelopeStatusCompleted": "Fully Claimed",
"fundEnvelopeStatusExpired": "Expired",
"fundEnvelopeStatusUnknown": "Unknown",
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
"fund": "支票",
"fundsRecent": "最近的支票",
"fundCreateNew": "創建新的",
"fundCreateNewHint": "為您的消息創建一個新的紅包。選擇接收者和金額。",
"amountOfSplits": "份數",
"enterNumberOfSplits": "單份金額",
"orCreateWith": "或\n使用第三方賬戶登錄",
"unindexedFiles": "未索引的檔案",
"folder": "文件夾",
"clearCompleted": "清除已經完成的",
"uploadSuccess": "上傳成功!",
"wouldYouLikeToViewFile": "您想查看檔案嗎?",
"contentCantEmpty": "內容不能為空",
"features": "功能",
"unnamed": "未命名",
"fundEnvelopeLoadFailed": "載入支票信封失敗",
"fundEnvelope": "支票信封",
"fundEnvelopeRemaining": "剩餘:{} {}",
"fundEnvelopeSplit": "拆分:{}",
"fundEnvelopeSplitEvenly": "均分",
"fundEnvelopeSplitRandomly": "隨機",
"fundEnvelopeClaimSuccess": "支票領取成功!",
"fundEnvelopeStatusCreated": "已創建",
"fundEnvelopeStatusPartial": "已領取部分",
"fundEnvelopeStatusCompleted": "已全部領取",
"fundEnvelopeStatusExpired": "已過期",
"fundEnvelopeStatusUnknown": "未知",
"fundEnvelopeRecipients": "收款人 {}/{}已領取)",
"fundEnvelopeExpiredDaysAgo": {
"one": "Expired {} day ago",
"other": "Expired {} days ago"
"one": "{}天前過期",
"other": "{}天前過期"
},
"fundEnvelopeExpiresSoon": "Expires soon",
"fundEnvelopeExpiresSoon": "即將過期",
"fundEnvelopeExpiresInHours": {
"one": "Expires in {} hour",
"other": "Expires in {} hours"
"one": "{}小時後過期",
"other": "{}小時後過期"
},
"fundEnvelopeExpiresInDays": {
"one": "Expires in {} day",
"other": "Expires in {} days"
"one": "{}天後過期",
"other": "{}天後過期"
},
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
"fundEnvelopeUnknownUser": "Unknown User",
"deleteSite": "Delete Site",
"deleteSiteConfirm": "Are you sure you want to delete this site?",
"siteDeletedSuccess": "Site deleted successfully",
"siteSlug": "Slug",
"siteSlugHint": "my-site",
"siteSlugRequired": "Please enter a slug",
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
"siteName": "Site Name",
"siteNameHint": "My Publication Site",
"siteNameRequired": "Please enter a site name",
"siteMode": "Mode",
"siteModeFullyManaged": "Fully Managed",
"siteModeSelfManaged": "Self-Managed",
"editPublicationSite": "Edit Publication Site",
"deletePublicationSite": "Delete Publication Site",
"publicationSiteSavedSuccess": "Publication site saved successfully",
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
"newPublicationSite": "New Publication Site",
"siteDetails": "Site Details",
"siteInformation": "Site Information",
"siteDomain": "Domain",
"siteCreated": "Created",
"siteUpdated": "Updated",
"failedToLoadSite": "Failed to load site",
"sitePages": "Pages",
"noPagesYet": "No pages yet",
"createFirstPage": "Create your first page to get started",
"failedToLoadPages": "Failed to load pages",
"fileManagement": "File Management",
"siteFiles": "Files",
"siteFolder": "Folder",
"siteRoot": "Root",
"noFilesUploadedYet": "No files uploaded yet",
"uploadFirstFile": "Upload your first file to get started",
"failedToLoadFiles": "Failed to load files",
"noFilesFoundInFolder": "No files found in the selected folder",
"fileActions": "File Actions",
"purgeFiles": "Purge Files",
"purgeFilesDescription": "Remove all uploaded files from the site",
"deploySite": "Deploy Site",
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
"confirmPurge": "Confirm Purge",
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
"purgeAllFiles": "Purge All Files",
"allFilesPurgedSuccess": "All files purged successfully",
"failedToPurgeFiles": "Failed to purge files: {}",
"siteDeployedSuccess": "Site deployed successfully",
"failedToDeploySite": "Failed to deploy site: {}",
"createPage": "Create Page",
"editPage": "Edit Page",
"pageType": "Page Type",
"htmlPage": "HTML Page",
"redirectPage": "Redirect Page",
"pageTypeRequired": "Please select a page type",
"pagePath": "Page Path",
"pagePathHint": "/about, /contact, etc.",
"pagePathRequired": "Please enter a page path",
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
"pagePathMustStartWithSlash": "Page path must start with /",
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
"pageTitle": "Page Title",
"pageTitleHint": "About Us, Contact, etc.",
"pageTitleRequired": "Please enter a page title",
"pageContentHtml": "Page Content (HTML)",
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
"pageContentRequired": "Please enter HTML content for the page",
"redirectTarget": "Redirect Target",
"redirectTargetHint": "/new-page, https://example.com, etc.",
"redirectTargetRequired": "Please enter a redirect target",
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
"deletePage": "Delete Page",
"deletePageConfirm": "Are you sure you want to delete this page?",
"savePage": "Save Page",
"pageCreatedSuccess": "Page created successfully",
"pageUpdatedSuccess": "Page updated successfully",
"pageDeletedSuccess": "Page deleted successfully",
"uploadFiles": "Upload Files",
"uploadPath": "Upload Path",
"uploadPathHint": "/ (root) or /assets/images/",
"uploadPathRequired": "Please enter an upload path",
"uploadPathMustStartWithSlash": "Path must start with /",
"uploadPathNoSpaces": "Path cannot contain spaces",
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
"percentCompleted": "{}% completed",
"filesToUpload": "{} files to upload",
"fileSizeKb": "Size: {} KB",
"uploadingEllipsis": "Uploading...",
"fundEnvelopeRemainingWithSplits": "{} {} / {} ",
"fundEnvelopeUnknownUser": "未知用戶",
"deleteSite": "刪除網站",
"deleteSiteConfirm": "您確定要刪除此網站嗎?",
"siteDeletedSuccess": "網站成功刪除",
"siteSlug": "標識符",
"siteSlugHint": "我的站點",
"siteSlugRequired": "請輸入一個標識符",
"siteSlugInvalid": "標識符只能包含小寫字母、數字和連字符",
"siteName": "網站名稱",
"siteNameHint": "我的發佈者網站",
"siteNameRequired": "請輸入一個站點名稱",
"siteMode": "模式",
"siteModeFullyManaged": "全託管",
"siteModeSelfManaged": "自託管",
"editPublicationSite": "編輯發佈者網站",
"deletePublicationSite": "刪除發佈者網站",
"publicationSiteSavedSuccess": "發佈者網站保存成功",
"publicationSiteDeleteConfirm": "您確定要刪除此發佈者網站嗎?此操作不能撤銷。",
"publicationSiteDeletedSuccess": "發佈者網站成功刪除",
"newPublicationSite": "新建發佈者網站",
"siteDetails": "網站描述",
"siteInformation": "網站信息",
"siteDomain": "域名",
"siteCreated": "創建于",
"siteUpdated": "更新于",
"failedToLoadSite": "加載網站失敗",
"sitePages": "頁面",
"noPagesYet": "還沒有頁面",
"createFirstPage": "創建您的第一個頁面以開始",
"failedToLoadPages": "加載頁面失敗",
"fileManagement": "檔案管理器",
"siteFiles": "檔案",
"siteFolder": "資料夾",
"siteRoot": "",
"noFilesUploadedYet": "尚未上傳任何檔案",
"uploadFirstFile": "上傳您的第一個檔案以開始",
"failedToLoadFiles": "加載檔案失敗",
"noFilesFoundInFolder": "在選擇的資料夾中沒有檔案",
"fileActions": "檔案選項",
"purgeFiles": "清除檔案",
"purgeFilesDescription": "從這個網站刪除全部文件",
"deploySite": "部署網站",
"deploySiteDescription": "從ZIP存檔上傳和部署新版本",
"confirmPurge": "確認清空",
"purgeFilesConfirm": "這將永久刪除上傳到本網站的所有檔案。此操作無法復原。您確定要繼續嗎?",
"purgeAllFiles": "清除所有文檔案",
"allFilesPurgedSuccess": "全部檔案成功清空",
"failedToPurgeFiles": "清除檔案失敗:{}",
"siteDeployedSuccess": "網站成功部署",
"failedToDeploySite": "部署網站失敗:{}",
"createPage": "創建頁面",
"editPage": "編輯頁面",
"pageType": "頁面類型",
"htmlPage": "HTML 頁面",
"redirectPage": "重定向頁面",
"pageTypeRequired": "請選擇一個頁面類型",
"pagePath": "頁面路徑",
"pagePathHint": "例如/about/contact",
"pagePathRequired": "請輸入一個頁面路徑",
"pagePathInvalid": "頁面路徑只能包含字母、數字、連字符、底線和斜線",
"pagePathMustStartWithSlash": "頁面路徑以/開始",
"pagePathNoConsecutiveSlashes": "頁面路徑不能有連續的斜線",
"pageTitle": "頁面標題",
"pageTitleHint": "例如About UsContact",
"pageTitleRequired": "請輸入一個頁面標題",
"pageContentHtml": "頁面內容(HTML",
"pageContentHint": "<h1> Hello World</h1><p>這是我的頁面內容…</p>",
"pageContentRequired": "請為頁面輸入HTML內容",
"redirectTarget": "重定向目標",
"redirectTargetHint": "例如/new-pagehttps://example.com",
"redirectTargetRequired": "請輸入一個重定向目標",
"redirectTargetInvalid": "目標必須是相對路徑(/)或絕對 URLhttp/https",
"deletePage": "刪除頁面",
"deletePageConfirm": "您確定要刪除此頁面嗎?",
"savePage": "保存頁面",
"pageCreatedSuccess": "頁面創建成功",
"pageUpdatedSuccess": "頁面更新成功",
"pageDeletedSuccess": "頁面刪除成功",
"uploadFiles": "上傳檔案",
"uploadPath": "上傳路徑",
"uploadPathHint": "/ (根目錄) 或 /assets/images/",
"uploadPathRequired": "請輸入一個上傳路徑",
"uploadPathMustStartWithSlash": "路徑以/開始",
"uploadPathNoSpaces": "路徑不能包含空格",
"uploadPathNoConsecutiveSlashes": "路徑不能包含連續的斜槓",
"percentCompleted": "{}%已完成",
"filesToUpload": "{} 個檔案要上傳",
"fileSizeKb": "大小:{} KB",
"uploadingEllipsis": "上傳中……",
"uploadFilesCount": {
"one": "Upload {} File",
"other": "Upload {} Files"
"one": "上傳 {} 個檔案",
"other": "上傳 {} 個檔案"
},
"allUploadsCompleted": "All uploads completed",
"someUploadsFailed": "Some uploads failed",
"uploadingInProgress": "Uploading in progress",
"readyToUpload": "Ready to upload",
"allFilesUploadedSuccess": "All files uploaded successfully",
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
"lotteryMultiplierRequired": "Please enter a multiplier",
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
"dropToShare": "Drop to share"
"allUploadsCompleted": "上傳全部完成",
"someUploadsFailed": "部分上傳失敗",
"uploadingInProgress": "正在上傳",
"readyToUpload": "準備好上傳",
"allFilesUploadedSuccess": "全部檔案完成上傳",
"lotteryLastNumberSpecial": "最後一個選擇的數字將是您的特殊數字。",
"lotteryMultiplierRequired": "請輸入一個倍率",
"lotteryMultiplierRange": "倍率需要在1到10之間",
"dropToShare": "拖拽以分享",
"affiliationSpell": "Affiliation Spell",
"affiliationSpellHint": "If you have an affiliation spell, enter it here.",
"friendsOnline": "Friends Online",
"createAccountAlmostThere": "Almost There",
"createAccountAlmostThereHint": "You're one step away from joining the Solar Network! Please solve the captcha puzzle shows next.",
"createAccountNotice": "Things you need to know before you create an account:",
"createAccountConfirmEmail": "After your account being created, you need go to your email inbox to active your account to get permission to use all features.",
"createAccountNoAltAccounts": "Multiple or alternative accounts are banned from the Solar Network, that will violates our terms of services.",
"createAccountAgreeTerms": "I've read these terms and agree to the terms of service.",
"createAccountProfile": "Create your profile",
"createAccountToS": "Review Terms & Conditions",
"updateYourProfileDescription": "Adjust how you looks on the Solar Network.",
"realmsDescription": "Manage realms you've joined.",
"exploreDescription": "Explore contents on the Solar Network.",
"accountDescription": "Information about your account.",
"chatDescription": "Group Chats and Direct Messages",
"connectionServerDown": "Unable to Connect",
"appSettingsDescription": "Customize your app.",
"accountSettingsDescription": "Manage your preferences on the Solar Network.",
"walletDescription": "Your source point wallet.",
"relationshipsDescription": "Friends and connections.",
"notificationsDescription": "See what's happended related to you recently.",
"settingsFestivalFeatures": "Festival Limited Features",
"categoriesAndTags": "Categories & Tags",
"webArticlesStandDescription": "Explore external sites articles.",
"aboutDescription": "Learn more about the Solar Network.",
"abuseReportsDescription": "View and manage abuse reports.",
"stickerMarketplaceDescription": "Browse and add sticker packs from the Solar Network marketplace.",
"webFeedsDescription": "Browse and subscribe to web feeds from the Solar Network.",
"discoverRealmsDescription": "Discover new realms and join them.",
"postShuffleDescription": "Shuffle posts to see the posts randomly.",
"levelingDescription": "See your leveling progress and history.",
"notableDayToday": "{} is today!",
"authSessionLogout": "Logout Session",
"authSessionLogoutHint": "Are you sure you want to logout this session? This will terminate this specific login session.",
"filesDescription": "Manage your files on the Solar Network Drive.",
"postComposeDescription": "Compose a new post",
"searchPostsDescription": "Search posts by title, content, or else.",
"accountActivationAlert": "Activate your account",
"accountActivationAlertHint": "Unactivated account may leads to various of permission issues, activate your account by clicking the link we sent to your email inbox.",
"accountActivationResendHint": "Didn't see it? Try click the button below to resend one. If you need to update your email while your account was unactivated, feel free to contact our customer service.",
"accountActivationResend": "Resend",
"ipAddress": "IP Address",
"noFurtherData": "No further data",
"searchAnything": "Search Anything...",
"tapToViewAllNotifications": "Tap to view all notifications",
"mostRecent": "Most Recent",
"noNotificationsYet": "No notifications yet",
"recentChats": "Recent Chats",
"noFeaturedPostsAvailable": "No featured posts available",
"searchChatsAndPages": "Search chats and pages...",
"dashboard": "Dashboard",
"dashboardDescription": "All your data in one place.",
"postTagsCategories": "Post Tags and Categories",
"postTagsCategoriesDescription": "Browse posts by category and tags.",
"debugLogs": "Debug Logs",
"debugLogsDescription": "View debug logs for troubleshooting.",
"pinChatRoom": "Pin Chat Room",
"pinChatRoomDescription": "Pin this chat room to the top.",
"chatRoomPinned": "Chat room pinned successfully.",
"chatRoomUnpinned": "Chat room unpinned successfully.",
"pinnedChatRoom": "Pinned Rooms",
"settingsGroupedChatList": "Grouped Chat List",
"settingsNotifyWithHaptic": "Notification with Haptic Feedback",
"settingsDashSearchEngine": "Search Engine for web",
"settingsDashSearchEngineHelper": "Use %s as the placeholder for the query.",
"settingsDefaultScreen": "Default Screen",
"notableDayChristmas": "Christmas",
"notableDayNewYear": "New Year"
}

View File

@@ -1,5 +1,5 @@
PODS:
- Alamofire (5.10.2)
- Alamofire (5.11.0)
- connectivity_plus (0.0.1):
- Flutter
- croppy (0.0.1):
@@ -42,83 +42,83 @@ PODS:
- Flutter
- file_saver (0.0.1):
- Flutter
- Firebase/CoreOnly (12.4.0):
- FirebaseCore (~> 12.4.0)
- Firebase/Crashlytics (12.4.0):
- Firebase/CoreOnly (12.6.0):
- FirebaseCore (~> 12.6.0)
- Firebase/Crashlytics (12.6.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 12.4.0)
- Firebase/Messaging (12.4.0):
- FirebaseCrashlytics (~> 12.6.0)
- Firebase/Messaging (12.6.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0)
- firebase_analytics (12.0.4):
- FirebaseMessaging (~> 12.6.0)
- firebase_analytics (12.1.0):
- firebase_core
- FirebaseAnalytics (= 12.4.0)
- FirebaseAnalytics (= 12.6.0)
- Flutter
- firebase_core (4.2.1):
- Firebase/CoreOnly (= 12.4.0)
- firebase_core (4.3.0):
- Firebase/CoreOnly (= 12.6.0)
- Flutter
- firebase_crashlytics (5.0.5):
- Firebase/Crashlytics (= 12.4.0)
- firebase_crashlytics (5.0.6):
- Firebase/Crashlytics (= 12.6.0)
- firebase_core
- Flutter
- firebase_messaging (16.0.4):
- Firebase/Messaging (= 12.4.0)
- firebase_messaging (16.1.0):
- Firebase/Messaging (= 12.6.0)
- firebase_core
- Flutter
- FirebaseAnalytics (12.4.0):
- FirebaseAnalytics/Default (= 12.4.0)
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- FirebaseAnalytics (12.6.0):
- FirebaseAnalytics/Default (= 12.6.0)
- FirebaseCore (~> 12.6.0)
- FirebaseInstallations (~> 12.6.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.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- GoogleAppMeasurement/Default (= 12.4.0)
- FirebaseAnalytics/Default (12.6.0):
- FirebaseCore (~> 12.6.0)
- FirebaseInstallations (~> 12.6.0)
- GoogleAppMeasurement/Default (= 12.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseCore (12.4.0):
- FirebaseCoreInternal (~> 12.4.0)
- FirebaseCore (12.6.0):
- FirebaseCoreInternal (~> 12.6.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseCoreInternal (12.4.0):
- FirebaseCoreExtension (12.6.0):
- FirebaseCore (~> 12.6.0)
- FirebaseCoreInternal (12.6.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseCrashlytics (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- FirebaseRemoteConfigInterop (~> 12.4.0)
- FirebaseSessions (~> 12.4.0)
- FirebaseCrashlytics (12.6.0):
- FirebaseCore (~> 12.6.0)
- FirebaseInstallations (~> 12.6.0)
- FirebaseRemoteConfigInterop (~> 12.6.0)
- FirebaseSessions (~> 12.6.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (12.6.0):
- FirebaseCore (~> 12.6.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- FirebaseMessaging (12.6.0):
- FirebaseCore (~> 12.6.0)
- FirebaseInstallations (~> 12.6.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.4.0)
- FirebaseSessions (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseCoreExtension (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- FirebaseRemoteConfigInterop (12.6.0)
- FirebaseSessions (12.6.0):
- FirebaseCore (~> 12.6.0)
- FirebaseCoreExtension (~> 12.6.0)
- FirebaseInstallations (~> 12.6.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
@@ -140,8 +140,9 @@ PODS:
- Flutter
- flutter_native_splash (2.4.3):
- Flutter
- flutter_secure_storage (6.0.0):
- flutter_secure_storage_darwin (10.0.0):
- Flutter
- FlutterMacOS
- flutter_timezone (0.0.1):
- Flutter
- flutter_udid (0.0.1):
@@ -153,28 +154,28 @@ PODS:
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleAdsOnDeviceConversion (3.1.0):
- GoogleAdsOnDeviceConversion (3.2.0):
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Core (12.4.0):
- GoogleAppMeasurement/Core (12.6.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.4.0):
- GoogleAdsOnDeviceConversion (~> 3.1.0)
- GoogleAppMeasurement/Core (= 12.4.0)
- GoogleAppMeasurement/IdentitySupport (= 12.4.0)
- GoogleAppMeasurement/Default (12.6.0):
- GoogleAdsOnDeviceConversion (~> 3.2.0)
- GoogleAppMeasurement/Core (= 12.6.0)
- GoogleAppMeasurement/IdentitySupport (= 12.6.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.4.0):
- GoogleAppMeasurement/Core (= 12.4.0)
- GoogleAppMeasurement/IdentitySupport (12.6.0):
- GoogleAppMeasurement/Core (= 12.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
@@ -212,6 +213,8 @@ PODS:
- GoogleUtilities/Privacy
- image_picker_ios (0.0.1):
- Flutter
- in_app_review (2.0.0):
- Flutter
- irondash_engine_context (0.0.1):
- Flutter
- KeychainAccess (4.2.2)
@@ -273,6 +276,8 @@ PODS:
- SDWebImage (5.21.5):
- SDWebImage/Core (= 5.21.5)
- SDWebImage/Core (5.21.5)
- sensors_plus (0.0.1):
- Flutter
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -336,12 +341,13 @@ DEPENDENCIES:
- 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_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- Kingfisher (~> 8.0)
- KingfisherWebP
@@ -358,6 +364,7 @@ DEPENDENCIES:
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- record_ios (from `.symlinks/plugins/record_ios/ios`)
- sensors_plus (from `.symlinks/plugins/sensors_plus/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
@@ -431,8 +438,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_secure_storage_darwin:
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
flutter_timezone:
:path: ".symlinks/plugins/flutter_timezone/ios"
flutter_udid:
@@ -443,6 +450,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/gal/darwin"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_review:
:path: ".symlinks/plugins/in_app_review/ios"
irondash_engine_context:
:path: ".symlinks/plugins/irondash_engine_context/ios"
livekit_client:
@@ -471,6 +480,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
record_ios:
:path: ".symlinks/plugins/record_ios/ios"
sensors_plus:
:path: ".symlinks/plugins/sensors_plus/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@@ -491,7 +502,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
Alamofire: bd5e7b23a1a750975288482c1831d71e74415f86
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
@@ -499,36 +510,37 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
firebase_crashlytics: c039028126cb45e32f4c217aa392408b0963d081
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018
FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6
FirebaseCrashlytics: a6ece278a837c7e88de2d9b5da0a3542f2342395
FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2
FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5
FirebaseRemoteConfigInterop: 1e31ec72b89c9924367c59bfb5ec9ab60d1d6766
FirebaseSessions: ba7c7a7ca8696a8d540eb3fe3800fbe98c79786d
Firebase: a451a7b61536298fd5cbfe3a746fd40443a50679
firebase_analytics: 4f9cca09e65f6c2944a862c6dc86f6bed9fb769c
firebase_core: ba00a168e719694f38960502ceb560285603d073
firebase_crashlytics: 13f4b77e9ce2a84b1f8ea07f293db5b6213ce1cf
firebase_messaging: bf0e29321927edc02a563c984dbfa5b063864b15
FirebaseAnalytics: d0a97a0db6425e5a5d966340b87f92ca7b13a557
FirebaseCore: 0e38ad5d62d980a47a64b8e9301ffa311457be04
FirebaseCoreExtension: 032fd6f8509e591fda8cb76f6651f20d926b121f
FirebaseCoreInternal: 69bf1306a05b8ac43004f6cc1f804bb7b05b229e
FirebaseCrashlytics: 3d6248c50726ee7832aef0e53cb84c9e64d9fa7e
FirebaseInstallations: 631b38da2e11a83daa4bfb482f79d286a5dfa7ad
FirebaseMessaging: a61bc42dcab3f7a346d94bbb54dab2c9435b18b2
FirebaseRemoteConfigInterop: 3443b8cb8fffd76bb3e03b2a84bfd3db952fcda4
FirebaseSessions: 2e8f808347e665dff3e5843f275715f07045297d
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
GoogleAppMeasurement: 3bf40aff49a601af5da1c3345702fcb4991d35ee
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
Kingfisher: 23d18f54677d973b713e54ce6a8f5eef6e7056ba
@@ -552,6 +564,7 @@ SPEC CHECKSUMS:
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
sensors_plus: 6a11ed0c2e1d0bd0b20b4029d3bad27d96e0c65b
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418

View File

@@ -1085,8 +1085,8 @@
baseConfigurationReference = 31EA49B10397BD4145AD765E /* Pods-Solian Watch App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1136,8 +1136,8 @@
baseConfigurationReference = 2440CEDEAAD6D51FDA95FA62 /* Pods-Solian Watch App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1185,8 +1185,8 @@
baseConfigurationReference = 0ECC3D56D018DD87FC342699 /* Pods-Solian Watch App.profile.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1232,7 +1232,7 @@
73ACDFC42E3D0E6100B63535 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1274,7 +1274,7 @@
73ACDFC52E3D0E6100B63535 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1313,7 +1313,7 @@
73ACDFC62E3D0E6100B63535 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1353,7 +1353,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1398,7 +1398,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1440,7 +1440,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1482,7 +1482,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1524,7 +1524,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1563,7 +1563,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;

View File

@@ -1,334 +1 @@
{
"images" : [
{
"filename" : "Icon-App-20x20@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "Icon-App-20x20@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "Icon-App-29x29@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "Icon-App-29x29@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "Icon-App-38x38@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "38x38"
},
{
"filename" : "Icon-App-38x38@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "38x38"
},
{
"filename" : "Icon-App-40x40@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "Icon-App-40x40@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "Icon-App-60x60@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "Icon-App-60x60@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "Icon-App-64x64@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "64x64"
},
{
"filename" : "Icon-App-64x64@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "64x64"
},
{
"filename" : "Icon-App-68x68@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "68x68"
},
{
"filename" : "Icon-App-76x76@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "Icon-App-83.5x83.5@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "Icon-App-1024x1024@1x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "1x",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-20x20@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "20x20"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-20x20@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "20x20"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-29x29@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "29x29"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-29x29@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "29x29"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-38x38@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "38x38"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-38x38@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "38x38"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-40x40@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "40x40"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-40x40@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "40x40"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-60x60@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "60x60"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-60x60@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "60x60"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-64x64@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "64x64"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-64x64@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "64x64"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-68x68@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "68x68"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-76x76@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "76x76"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-83.5x83.5@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Icon-App-Dark-1024x1024@1x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "1x",
"size" : "1024x1024"
},
{
"filename" : "Icon-App-1024x1024@1x.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{"images":[{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@2x.png","scale":"2x","platform":"ios"},{"size":"20x20","idiom":"universal","filename":"Icon-App-20x20@3x.png","scale":"3x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@2x.png","scale":"2x","platform":"ios"},{"size":"29x29","idiom":"universal","filename":"Icon-App-29x29@3x.png","scale":"3x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@2x.png","scale":"2x","platform":"ios"},{"size":"38x38","idiom":"universal","filename":"Icon-App-38x38@3x.png","scale":"3x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@2x.png","scale":"2x","platform":"ios"},{"size":"40x40","idiom":"universal","filename":"Icon-App-40x40@3x.png","scale":"3x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@2x.png","scale":"2x","platform":"ios"},{"size":"60x60","idiom":"universal","filename":"Icon-App-60x60@3x.png","scale":"3x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@2x.png","scale":"2x","platform":"ios"},{"size":"64x64","idiom":"universal","filename":"Icon-App-64x64@3x.png","scale":"3x","platform":"ios"},{"size":"68x68","idiom":"universal","filename":"Icon-App-68x68@2x.png","scale":"2x","platform":"ios"},{"size":"76x76","idiom":"universal","filename":"Icon-App-76x76@2x.png","scale":"2x","platform":"ios"},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x","platform":"ios"},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-1024x1024@1x.png","scale":"1x","platform":"ios"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"20x20","idiom":"universal","filename":"Icon-App-Dark-20x20@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"29x29","idiom":"universal","filename":"Icon-App-Dark-29x29@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"38x38","idiom":"universal","filename":"Icon-App-Dark-38x38@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"40x40","idiom":"universal","filename":"Icon-App-Dark-40x40@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"60x60","idiom":"universal","filename":"Icon-App-Dark-60x60@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"64x64","idiom":"universal","filename":"Icon-App-Dark-64x64@3x.png","scale":"3x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"68x68","idiom":"universal","filename":"Icon-App-Dark-68x68@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"76x76","idiom":"universal","filename":"Icon-App-Dark-76x76@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"83.5x83.5","idiom":"universal","filename":"Icon-App-Dark-83.5x83.5@2x.png","scale":"2x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]},{"size":"1024x1024","idiom":"universal","filename":"Icon-App-Dark-1024x1024@1x.png","scale":"1x","platform":"ios","appearances":[{"appearance":"luminosity","value":"dark"}]}],"info":{"version":1,"author":"xcode"}}

View File

@@ -7,7 +7,7 @@
<key>BUNDLE_ID</key>
<string>dev.solsynth.solian</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -52,16 +52,15 @@
<key>CLIENT_ID</key>
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false />
<false/>
<key>LSRequiresIPhoneOS</key>
<true />
<true/>
<key>NSCalendarsUsageDescription</key>
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
<key>NSCameraUsageDescription</key>
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
<key>NSFaceIDUsageDescription</key>
<string>Allow the Solar Network verify your ownership of the logged in account and continue
your action quickly.</string>
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
@@ -78,7 +77,7 @@
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true />
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
@@ -91,7 +90,7 @@
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false />
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
@@ -108,4 +107,4 @@
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
</dict>
</plist>
</plist>

View File

@@ -5,16 +5,19 @@ import 'package:island/database/draft.dart';
import 'package:island/models/account.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/post.dart';
import 'package:island/models/realm.dart';
part 'drift_db.g.dart';
// Define the database
@DriftDatabase(tables: [ChatRooms, ChatMembers, ChatMessages, PostDrafts])
@DriftDatabase(
tables: [Realms, ChatRooms, ChatMembers, ChatMessages, PostDrafts],
)
class AppDatabase extends _$AppDatabase {
AppDatabase(super.e);
@override
int get schemaVersion => 9;
int get schemaVersion => 12;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -71,6 +74,35 @@ class AppDatabase extends _$AppDatabase {
'ALTER TABLE chat_members DROP COLUMN last_typed',
);
}
if (from < 10) {
// Add realms table and update chat_rooms foreign key
await m.createTable(realms);
// The realmId column in chat_rooms already exists, just need to ensure the foreign key constraint
}
if (from < 11) {
// Add isPinned column to chat_rooms table
await customStatement(
'ALTER TABLE chat_rooms ADD COLUMN is_pinned INTEGER DEFAULT 0',
);
}
if (from < 12) {
// Add new columns to realms table
await customStatement(
'ALTER TABLE realms ADD COLUMN slug TEXT NOT NULL DEFAULT \'\'',
);
await customStatement(
'ALTER TABLE realms ADD COLUMN verified_as TEXT NULL',
);
await customStatement(
'ALTER TABLE realms ADD COLUMN verified_at DATETIME NULL',
);
await customStatement(
'ALTER TABLE realms ADD COLUMN is_community INTEGER NOT NULL DEFAULT 0',
);
await customStatement(
'ALTER TABLE realms ADD COLUMN is_public INTEGER NOT NULL DEFAULT 0',
);
}
},
);
@@ -92,11 +124,10 @@ class AppDatabase extends _$AppDatabase {
// Migrate existing data if any
try {
final oldDrafts =
await customSelect(
'SELECT id, post, lastModified FROM post_drafts_old',
readsFrom: {postDrafts},
).get();
final oldDrafts = await customSelect(
'SELECT id, post, lastModified FROM post_drafts_old',
readsFrom: {postDrafts},
).get();
for (final row in oldDrafts) {
final postJson = row.read<String>('post');
@@ -150,9 +181,9 @@ class AppDatabase extends _$AppDatabase {
}
Future<int> updateMessageStatus(String id, MessageStatus status) {
return (update(chatMessages)..where(
(m) => m.id.equals(id),
)).write(ChatMessagesCompanion(status: Value(status)));
return (update(chatMessages)..where((m) => m.id.equals(id))).write(
ChatMessagesCompanion(status: Value(status)),
);
}
Future<int> deleteMessage(String id) {
@@ -176,29 +207,28 @@ class AppDatabase extends _$AppDatabase {
if (query.isNotEmpty) {
final searchTerm = '%$query%';
selectStatement =
selectStatement..where(
(m) =>
m.content.like(searchTerm) |
m.meta.like(searchTerm) |
m.attachments.like(searchTerm) |
m.type.like(searchTerm),
);
selectStatement = selectStatement
..where(
(m) =>
m.content.like(searchTerm) |
m.meta.like(searchTerm) |
m.attachments.like(searchTerm) |
m.type.like(searchTerm),
);
}
if (withAttachments == true) {
selectStatement =
selectStatement..where((m) => m.attachments.equals('[]').not());
selectStatement = selectStatement
..where((m) => m.attachments.equals('[]').not());
}
final messages =
await (selectStatement
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
.get();
final messageFutures =
messages
.map((msg) => companionToMessage(msg, fetchAccount: fetchAccount))
.toList();
final messageFutures = messages
.map((msg) => companionToMessage(msg, fetchAccount: fetchAccount))
.toList();
return await Future.wait(messageFutures);
}
@@ -234,9 +264,9 @@ class AppDatabase extends _$AppDatabase {
final data = jsonDecode(dbMessage.data);
SnChatMember? sender;
try {
final senderRow =
await (select(chatMembers)
..where((m) => m.id.equals(dbMessage.senderId))).getSingle();
final senderRow = await (select(
chatMembers,
)..where((m) => m.id.equals(dbMessage.senderId))).getSingle();
SnAccount senderAccount;
senderAccount = SnAccount.fromJson(senderRow.account);
@@ -335,6 +365,7 @@ class AppDatabase extends _$AppDatabase {
picture: Value(room.picture?.toJson()),
background: Value(room.background?.toJson()),
realmId: Value(room.realmId),
accountId: Value(room.accountId),
createdAt: Value(room.createdAt),
updatedAt: Value(room.updatedAt),
deletedAt: Value(room.deletedAt),
@@ -358,6 +389,25 @@ class AppDatabase extends _$AppDatabase {
);
}
RealmsCompanion companionFromRealm(SnRealm realm) {
return RealmsCompanion(
id: Value(realm.id),
slug: Value(realm.slug),
name: Value(realm.name),
description: Value(realm.description),
verifiedAs: Value(realm.verifiedAs),
verifiedAt: Value(realm.verifiedAt),
isCommunity: Value(realm.isCommunity),
isPublic: Value(realm.isPublic),
picture: Value(realm.picture?.toJson()),
background: Value(realm.background?.toJson()),
accountId: Value(realm.accountId),
createdAt: Value(realm.createdAt),
updatedAt: Value(realm.updatedAt),
deletedAt: Value(realm.deletedAt),
);
}
Future<void> saveChatRooms(
List<SnChatRoom> rooms, {
bool override = false,
@@ -373,22 +423,46 @@ class AppDatabase extends _$AppDatabase {
if (idsToRemove.isNotEmpty) {
final idsList = idsToRemove.toList();
// Remove messages
await (delete(chatMessages)
..where((t) => t.roomId.isIn(idsList))).go();
await (delete(
chatMessages,
)..where((t) => t.roomId.isIn(idsList))).go();
// Remove members
await (delete(chatMembers)
..where((t) => t.chatRoomId.isIn(idsList))).go();
await (delete(
chatMembers,
)..where((t) => t.chatRoomId.isIn(idsList))).go();
// Remove rooms
await (delete(chatRooms)..where((t) => t.id.isIn(idsList))).go();
}
}
// 2. Upsert remote rooms
// 2. Upsert realms first
final realmsToSave = rooms
.where((room) => room.realm != null)
.map((room) => room.realm!)
.toSet()
.toList();
await batch((batch) {
for (final realm in realmsToSave) {
batch.insert(
realms,
companionFromRealm(realm),
mode: InsertMode.insertOrReplace,
);
}
});
// 3. Upsert remote rooms
await batch((batch) async {
for (final room in rooms) {
// Preserve local isPinned status
final currentRoom = await (select(
chatRooms,
)..where((r) => r.id.equals(room.id))).getSingleOrNull();
final isPinned = currentRoom?.isPinned ?? false;
batch.insert(
chatRooms,
companionFromRoom(room),
companionFromRoom(room).copyWith(isPinned: Value(isPinned)),
mode: InsertMode.insertOrReplace,
);
for (final member in room.members ?? []) {
@@ -445,8 +519,9 @@ class AppDatabase extends _$AppDatabase {
}
Future<PostDraft?> getPostDraftById(String id) async {
return await (select(postDrafts)
..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
return await (select(
postDrafts,
)..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
}
Future<void> saveMember(SnChatMember member) async {
@@ -463,4 +538,16 @@ class AppDatabase extends _$AppDatabase {
// Then save the message
return await saveMessage(messageToCompanion(message));
}
Future<void> toggleChatRoomPinned(String roomId) async {
final room = await (select(
chatRooms,
)..where((r) => r.id.equals(roomId))).getSingleOrNull();
if (room != null) {
final newPinnedStatus = !(room.isPinned ?? false);
await (update(chatRooms)..where((r) => r.id.equals(roomId))).write(
ChatRoomsCompanion(isPinned: Value(newPinnedStatus)),
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,26 @@ class ListMapConverter
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
}
class Realms extends Table {
TextColumn get id => text()();
TextColumn get slug => text()();
TextColumn get name => text().nullable()();
TextColumn get description => text().nullable()();
TextColumn get verifiedAs => text().nullable()();
DateTimeColumn get verifiedAt => dateTime().nullable()();
BoolColumn get isCommunity => boolean()();
BoolColumn get isPublic => boolean()();
TextColumn get picture => text().map(const MapConverter()).nullable()();
TextColumn get background => text().map(const MapConverter()).nullable()();
TextColumn get accountId => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get updatedAt => dateTime()();
DateTimeColumn get deletedAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {id};
}
class ChatRooms extends Table {
TextColumn get id => text()();
TextColumn get name => text().nullable()();
@@ -47,8 +67,10 @@ class ChatRooms extends Table {
boolean().nullable().withDefault(const Constant(false))();
TextColumn get picture => text().map(const MapConverter()).nullable()();
TextColumn get background => text().map(const MapConverter()).nullable()();
TextColumn get realmId => text().nullable()();
TextColumn get realmId => text().references(Realms, #id).nullable()();
TextColumn get accountId => text().nullable()();
BoolColumn get isPinned =>
boolean().nullable().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get updatedAt => dateTime()();
DateTimeColumn get deletedAt => dateTime().nullable()();
@@ -91,10 +113,9 @@ class ChatMessages extends Table {
TextColumn get type => text().withDefault(const Constant('text'))();
TextColumn get meta =>
text().map(const MapConverter()).withDefault(const Constant('{}'))();
TextColumn get membersMentioned =>
text()
.map(const ListStringConverter())
.withDefault(const Constant('[]'))();
TextColumn get membersMentioned => text()
.map(const ListStringConverter())
.withDefault(const Constant('[]'))();
DateTimeColumn get editedAt => dateTime().nullable()();
TextColumn get attachments =>
text().map(const ListMapConverter()).withDefault(const Constant('[]'))();

View File

@@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:image_picker_android/image_picker_android.dart';
import 'package:island/talker.dart';
import 'package:island/firebase_options.dart';
@@ -53,7 +54,8 @@ void main() async {
if (!kIsWeb && (Platform.isLinux || Platform.isMacOS || Platform.isWindows)) {
talker.info("[SplashScreen] Initializing desktop window manager...");
await protocolHandler.register('myprotocol');
await protocolHandler.register('solian');
await hotKeyManager.unregisterAll();
talker.info("[SplashScreen] Desktop window manager is ready!");
}

View File

@@ -10,7 +10,8 @@ sealed class SnNotableDay with _$SnNotableDay {
required DateTime date,
required String localName,
required String globalName,
required String countryCode,
required String? countryCode,
required String? localizableKey,
required List<int> holidays,
}) = _SnNotableDay;

View File

@@ -15,7 +15,7 @@ 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;
DateTime get date; String get localName; String get globalName; String? get countryCode; String? get localizableKey; 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)
@@ -28,16 +28,16 @@ $SnNotableDayCopyWith<SnNotableDay> get copyWith => _$SnNotableDayCopyWithImpl<S
@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));
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)&&(identical(other.localizableKey, localizableKey) || other.localizableKey == localizableKey)&&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));
int get hashCode => Object.hash(runtimeType,date,localName,globalName,countryCode,localizableKey,const DeepCollectionEquality().hash(holidays));
@override
String toString() {
return 'SnNotableDay(date: $date, localName: $localName, globalName: $globalName, countryCode: $countryCode, holidays: $holidays)';
return 'SnNotableDay(date: $date, localName: $localName, globalName: $globalName, countryCode: $countryCode, localizableKey: $localizableKey, holidays: $holidays)';
}
@@ -48,7 +48,7 @@ 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
DateTime date, String localName, String globalName, String? countryCode, String? localizableKey, List<int> holidays
});
@@ -65,13 +65,14 @@ class _$SnNotableDayCopyWithImpl<$Res>
/// 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,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? date = null,Object? localName = null,Object? globalName = null,Object? countryCode = freezed,Object? localizableKey = freezed,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 String,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String?,localizableKey: freezed == localizableKey ? _self.localizableKey : localizableKey // ignore: cast_nullable_to_non_nullable
as String?,holidays: null == holidays ? _self.holidays : holidays // ignore: cast_nullable_to_non_nullable
as List<int>,
));
}
@@ -154,10 +155,10 @@ return $default(_that);case _:
/// }
/// ```
@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;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime date, String localName, String globalName, String? countryCode, String? localizableKey, 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 $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_that.localizableKey,_that.holidays);case _:
return orElse();
}
@@ -175,10 +176,10 @@ return $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_t
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime date, String localName, String globalName, String countryCode, List<int> holidays) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime date, String localName, String globalName, String? countryCode, String? localizableKey, List<int> holidays) $default,) {final _that = this;
switch (_that) {
case _SnNotableDay():
return $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_that.holidays);}
return $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_that.localizableKey,_that.holidays);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -192,10 +193,10 @@ return $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_t
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime date, String localName, String globalName, String countryCode, List<int> holidays)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime date, String localName, String globalName, String? countryCode, String? localizableKey, 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 $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_that.localizableKey,_that.holidays);case _:
return null;
}
@@ -207,13 +208,14 @@ return $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_t
@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;
const _SnNotableDay({required this.date, required this.localName, required this.globalName, required this.countryCode, required this.localizableKey, 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;
@override final String? countryCode;
@override final String? localizableKey;
final List<int> _holidays;
@override List<int> get holidays {
if (_holidays is EqualUnmodifiableListView) return _holidays;
@@ -235,16 +237,16 @@ 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));
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)&&(identical(other.localizableKey, localizableKey) || other.localizableKey == localizableKey)&&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));
int get hashCode => Object.hash(runtimeType,date,localName,globalName,countryCode,localizableKey,const DeepCollectionEquality().hash(_holidays));
@override
String toString() {
return 'SnNotableDay(date: $date, localName: $localName, globalName: $globalName, countryCode: $countryCode, holidays: $holidays)';
return 'SnNotableDay(date: $date, localName: $localName, globalName: $globalName, countryCode: $countryCode, localizableKey: $localizableKey, holidays: $holidays)';
}
@@ -255,7 +257,7 @@ abstract mixin class _$SnNotableDayCopyWith<$Res> implements $SnNotableDayCopyWi
factory _$SnNotableDayCopyWith(_SnNotableDay value, $Res Function(_SnNotableDay) _then) = __$SnNotableDayCopyWithImpl;
@override @useResult
$Res call({
DateTime date, String localName, String globalName, String countryCode, List<int> holidays
DateTime date, String localName, String globalName, String? countryCode, String? localizableKey, List<int> holidays
});
@@ -272,13 +274,14 @@ class __$SnNotableDayCopyWithImpl<$Res>
/// 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,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? date = null,Object? localName = null,Object? globalName = null,Object? countryCode = freezed,Object? localizableKey = freezed,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 String,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String?,localizableKey: freezed == localizableKey ? _self.localizableKey : localizableKey // ignore: cast_nullable_to_non_nullable
as String?,holidays: null == holidays ? _self._holidays : holidays // ignore: cast_nullable_to_non_nullable
as List<int>,
));
}

View File

@@ -11,7 +11,8 @@ _SnNotableDay _$SnNotableDayFromJson(Map<String, dynamic> json) =>
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,
countryCode: json['country_code'] as String?,
localizableKey: json['localizable_key'] as String?,
holidays: (json['holidays'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
@@ -23,6 +24,7 @@ Map<String, dynamic> _$SnNotableDayToJson(_SnNotableDay instance) =>
'local_name': instance.localName,
'global_name': instance.globalName,
'country_code': instance.countryCode,
'localizable_key': instance.localizableKey,
'holidays': instance.holidays,
};

View File

@@ -24,6 +24,8 @@ sealed class SnChatRoom with _$SnChatRoom {
required DateTime updatedAt,
required DateTime? deletedAt,
required List<SnChatMember>? members,
// Frontend data
@Default(false) bool isPinned,
}) = _SnChatRoom;
factory SnChatRoom.fromJson(Map<String, dynamic> json) =>

View File

@@ -15,7 +15,8 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnChatRoom {
String get id; String? get name; String? get description; int get type; bool get isPublic; bool get isCommunity; SnCloudFile? get picture; SnCloudFile? get background; String? get realmId; String? get accountId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnChatMember>? get members;
String get id; String? get name; String? get description; int get type; bool get isPublic; bool get isCommunity; SnCloudFile? get picture; SnCloudFile? get background; String? get realmId; String? get accountId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnChatMember>? get members;// Frontend data
bool get isPinned;
/// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +29,16 @@ $SnChatRoomCopyWith<SnChatRoom> get copyWith => _$SnChatRoomCopyWithImpl<SnChatR
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.members, members));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.members, members)&&(identical(other.isPinned, isPinned) || other.isPinned == isPinned));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,isCommunity,picture,background,realmId,accountId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(members));
int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,isCommunity,picture,background,realmId,accountId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(members),isPinned);
@override
String toString() {
return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, isCommunity: $isCommunity, picture: $picture, background: $background, realmId: $realmId, accountId: $accountId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)';
return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, isCommunity: $isCommunity, picture: $picture, background: $background, realmId: $realmId, accountId: $accountId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members, isPinned: $isPinned)';
}
@@ -48,7 +49,7 @@ abstract mixin class $SnChatRoomCopyWith<$Res> {
factory $SnChatRoomCopyWith(SnChatRoom value, $Res Function(SnChatRoom) _then) = _$SnChatRoomCopyWithImpl;
@useResult
$Res call({
String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members
String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members, bool isPinned
});
@@ -65,7 +66,7 @@ class _$SnChatRoomCopyWithImpl<$Res>
/// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? isCommunity = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? accountId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? isCommunity = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? accountId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,Object? isPinned = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@@ -82,7 +83,8 @@ as SnRealm?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore
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?,members: freezed == members ? _self.members : members // ignore: cast_nullable_to_non_nullable
as List<SnChatMember>?,
as List<SnChatMember>?,isPinned: null == isPinned ? _self.isPinned : isPinned // ignore: cast_nullable_to_non_nullable
as bool,
));
}
/// Create a copy of SnChatRoom
@@ -200,10 +202,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members, bool isPinned)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnChatRoom() when $default != null:
return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,_that.isCommunity,_that.picture,_that.background,_that.realmId,_that.accountId,_that.realm,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.members);case _:
return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,_that.isCommunity,_that.picture,_that.background,_that.realmId,_that.accountId,_that.realm,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.members,_that.isPinned);case _:
return orElse();
}
@@ -221,10 +223,10 @@ return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members, bool isPinned) $default,) {final _that = this;
switch (_that) {
case _SnChatRoom():
return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,_that.isCommunity,_that.picture,_that.background,_that.realmId,_that.accountId,_that.realm,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.members);}
return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,_that.isCommunity,_that.picture,_that.background,_that.realmId,_that.accountId,_that.realm,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.members,_that.isPinned);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -238,10 +240,10 @@ return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members, bool isPinned)? $default,) {final _that = this;
switch (_that) {
case _SnChatRoom() when $default != null:
return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,_that.isCommunity,_that.picture,_that.background,_that.realmId,_that.accountId,_that.realm,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.members);case _:
return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,_that.isCommunity,_that.picture,_that.background,_that.realmId,_that.accountId,_that.realm,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.members,_that.isPinned);case _:
return null;
}
@@ -253,7 +255,7 @@ return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,
@JsonSerializable()
class _SnChatRoom implements SnChatRoom {
const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, this.isPublic = false, this.isCommunity = false, required this.picture, required this.background, required this.realmId, required this.accountId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final List<SnChatMember>? members}): _members = members;
const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, this.isPublic = false, this.isCommunity = false, required this.picture, required this.background, required this.realmId, required this.accountId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final List<SnChatMember>? members, this.isPinned = false}): _members = members;
factory _SnChatRoom.fromJson(Map<String, dynamic> json) => _$SnChatRoomFromJson(json);
@override final String id;
@@ -279,6 +281,8 @@ class _SnChatRoom implements SnChatRoom {
return EqualUnmodifiableListView(value);
}
// Frontend data
@override@JsonKey() final bool isPinned;
/// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values.
@@ -293,16 +297,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._members, _members));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._members, _members)&&(identical(other.isPinned, isPinned) || other.isPinned == isPinned));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,isCommunity,picture,background,realmId,accountId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_members));
int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,isCommunity,picture,background,realmId,accountId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_members),isPinned);
@override
String toString() {
return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, isCommunity: $isCommunity, picture: $picture, background: $background, realmId: $realmId, accountId: $accountId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)';
return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, isCommunity: $isCommunity, picture: $picture, background: $background, realmId: $realmId, accountId: $accountId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members, isPinned: $isPinned)';
}
@@ -313,7 +317,7 @@ abstract mixin class _$SnChatRoomCopyWith<$Res> implements $SnChatRoomCopyWith<$
factory _$SnChatRoomCopyWith(_SnChatRoom value, $Res Function(_SnChatRoom) _then) = __$SnChatRoomCopyWithImpl;
@override @useResult
$Res call({
String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members
String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members, bool isPinned
});
@@ -330,7 +334,7 @@ class __$SnChatRoomCopyWithImpl<$Res>
/// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? isCommunity = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? accountId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? isCommunity = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? accountId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,Object? isPinned = null,}) {
return _then(_SnChatRoom(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@@ -347,7 +351,8 @@ as SnRealm?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore
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?,members: freezed == members ? _self._members : members // ignore: cast_nullable_to_non_nullable
as List<SnChatMember>?,
as List<SnChatMember>?,isPinned: null == isPinned ? _self.isPinned : isPinned // ignore: cast_nullable_to_non_nullable
as bool,
));
}

View File

@@ -32,6 +32,7 @@ _SnChatRoom _$SnChatRoomFromJson(Map<String, dynamic> json) => _SnChatRoom(
members: (json['members'] as List<dynamic>?)
?.map((e) => SnChatMember.fromJson(e as Map<String, dynamic>))
.toList(),
isPinned: json['is_pinned'] as bool? ?? false,
);
Map<String, dynamic> _$SnChatRoomToJson(_SnChatRoom instance) =>
@@ -51,6 +52,7 @@ Map<String, dynamic> _$SnChatRoomToJson(_SnChatRoom instance) =>
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'members': instance.members?.map((e) => e.toJson()).toList(),
'is_pinned': instance.isPinned,
};
_SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) =>

16
lib/models/fortune.dart Normal file
View File

@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'fortune.g.dart';
part 'fortune.freezed.dart';
@freezed
sealed class SnFortuneSaying with _$SnFortuneSaying {
const factory SnFortuneSaying({
required String content,
required String source,
required String language,
}) = _SnFortuneSaying;
factory SnFortuneSaying.fromJson(Map<String, dynamic> json) =>
_$SnFortuneSayingFromJson(json);
}

View File

@@ -0,0 +1,277 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'fortune.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnFortuneSaying {
String get content; String get source; String get language;
/// Create a copy of SnFortuneSaying
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnFortuneSayingCopyWith<SnFortuneSaying> get copyWith => _$SnFortuneSayingCopyWithImpl<SnFortuneSaying>(this as SnFortuneSaying, _$identity);
/// Serializes this SnFortuneSaying to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnFortuneSaying&&(identical(other.content, content) || other.content == content)&&(identical(other.source, source) || other.source == source)&&(identical(other.language, language) || other.language == language));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,content,source,language);
@override
String toString() {
return 'SnFortuneSaying(content: $content, source: $source, language: $language)';
}
}
/// @nodoc
abstract mixin class $SnFortuneSayingCopyWith<$Res> {
factory $SnFortuneSayingCopyWith(SnFortuneSaying value, $Res Function(SnFortuneSaying) _then) = _$SnFortuneSayingCopyWithImpl;
@useResult
$Res call({
String content, String source, String language
});
}
/// @nodoc
class _$SnFortuneSayingCopyWithImpl<$Res>
implements $SnFortuneSayingCopyWith<$Res> {
_$SnFortuneSayingCopyWithImpl(this._self, this._then);
final SnFortuneSaying _self;
final $Res Function(SnFortuneSaying) _then;
/// Create a copy of SnFortuneSaying
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? content = null,Object? source = null,Object? language = null,}) {
return _then(_self.copyWith(
content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String,source: null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable
as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [SnFortuneSaying].
extension SnFortuneSayingPatterns on SnFortuneSaying {
/// 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( _SnFortuneSaying value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnFortuneSaying() 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( _SnFortuneSaying value) $default,){
final _that = this;
switch (_that) {
case _SnFortuneSaying():
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( _SnFortuneSaying value)? $default,){
final _that = this;
switch (_that) {
case _SnFortuneSaying() 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 content, String source, String language)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnFortuneSaying() when $default != null:
return $default(_that.content,_that.source,_that.language);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 content, String source, String language) $default,) {final _that = this;
switch (_that) {
case _SnFortuneSaying():
return $default(_that.content,_that.source,_that.language);}
}
/// 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 content, String source, String language)? $default,) {final _that = this;
switch (_that) {
case _SnFortuneSaying() when $default != null:
return $default(_that.content,_that.source,_that.language);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnFortuneSaying implements SnFortuneSaying {
const _SnFortuneSaying({required this.content, required this.source, required this.language});
factory _SnFortuneSaying.fromJson(Map<String, dynamic> json) => _$SnFortuneSayingFromJson(json);
@override final String content;
@override final String source;
@override final String language;
/// Create a copy of SnFortuneSaying
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnFortuneSayingCopyWith<_SnFortuneSaying> get copyWith => __$SnFortuneSayingCopyWithImpl<_SnFortuneSaying>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnFortuneSayingToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnFortuneSaying&&(identical(other.content, content) || other.content == content)&&(identical(other.source, source) || other.source == source)&&(identical(other.language, language) || other.language == language));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,content,source,language);
@override
String toString() {
return 'SnFortuneSaying(content: $content, source: $source, language: $language)';
}
}
/// @nodoc
abstract mixin class _$SnFortuneSayingCopyWith<$Res> implements $SnFortuneSayingCopyWith<$Res> {
factory _$SnFortuneSayingCopyWith(_SnFortuneSaying value, $Res Function(_SnFortuneSaying) _then) = __$SnFortuneSayingCopyWithImpl;
@override @useResult
$Res call({
String content, String source, String language
});
}
/// @nodoc
class __$SnFortuneSayingCopyWithImpl<$Res>
implements _$SnFortuneSayingCopyWith<$Res> {
__$SnFortuneSayingCopyWithImpl(this._self, this._then);
final _SnFortuneSaying _self;
final $Res Function(_SnFortuneSaying) _then;
/// Create a copy of SnFortuneSaying
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? content = null,Object? source = null,Object? language = null,}) {
return _then(_SnFortuneSaying(
content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String,source: null == source ? _self.source : source // ignore: cast_nullable_to_non_nullable
as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on

21
lib/models/fortune.g.dart Normal file
View File

@@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'fortune.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnFortuneSaying _$SnFortuneSayingFromJson(Map<String, dynamic> json) =>
_SnFortuneSaying(
content: json['content'] as String,
source: json['source'] as String,
language: json['language'] as String,
);
Map<String, dynamic> _$SnFortuneSayingToJson(_SnFortuneSaying instance) =>
<String, dynamic>{
'content': instance.content,
'source': instance.source,
'language': instance.language,
};

View File

@@ -74,15 +74,15 @@ sealed class SnPublisherStats with _$SnPublisherStats {
}
@freezed
sealed class SnSubscriptionStatus with _$SnSubscriptionStatus {
const factory SnSubscriptionStatus({
required bool isSubscribed,
sealed class SnPublisherSubscription with _$SnPublisherSubscription {
const factory SnPublisherSubscription({
required String accountId,
required String publisherId,
required String publisherName,
}) = _SnSubscriptionStatus;
required SnPublisher publisher,
}) = _SnPublisherSubscription;
factory SnSubscriptionStatus.fromJson(Map<String, dynamic> json) =>
_$SnSubscriptionStatusFromJson(json);
factory SnPublisherSubscription.fromJson(Map<String, dynamic> json) =>
_$SnPublisherSubscriptionFromJson(json);
}
@freezed
@@ -92,8 +92,9 @@ sealed class ReactInfo with _$ReactInfo {
static String getTranslationKey(String templateKey) {
final parts = templateKey.split('_');
final camelCase =
parts.map((p) => p[0].toUpperCase() + p.substring(1)).join();
final camelCase = parts
.map((p) => p[0].toUpperCase() + p.substring(1))
.join();
return 'reaction$camelCase';
}
}

View File

@@ -856,72 +856,81 @@ as int,
/// @nodoc
mixin _$SnSubscriptionStatus {
mixin _$SnPublisherSubscription {
bool get isSubscribed; String get publisherId; String get publisherName;
/// Create a copy of SnSubscriptionStatus
String get accountId; String get publisherId; SnPublisher get publisher;
/// Create a copy of SnPublisherSubscription
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnSubscriptionStatusCopyWith<SnSubscriptionStatus> get copyWith => _$SnSubscriptionStatusCopyWithImpl<SnSubscriptionStatus>(this as SnSubscriptionStatus, _$identity);
$SnPublisherSubscriptionCopyWith<SnPublisherSubscription> get copyWith => _$SnPublisherSubscriptionCopyWithImpl<SnPublisherSubscription>(this as SnPublisherSubscription, _$identity);
/// Serializes this SnSubscriptionStatus to a JSON map.
/// Serializes this SnPublisherSubscription to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSubscriptionStatus&&(identical(other.isSubscribed, isSubscribed) || other.isSubscribed == isSubscribed)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisherName, publisherName) || other.publisherName == publisherName));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisherSubscription&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,isSubscribed,publisherId,publisherName);
int get hashCode => Object.hash(runtimeType,accountId,publisherId,publisher);
@override
String toString() {
return 'SnSubscriptionStatus(isSubscribed: $isSubscribed, publisherId: $publisherId, publisherName: $publisherName)';
return 'SnPublisherSubscription(accountId: $accountId, publisherId: $publisherId, publisher: $publisher)';
}
}
/// @nodoc
abstract mixin class $SnSubscriptionStatusCopyWith<$Res> {
factory $SnSubscriptionStatusCopyWith(SnSubscriptionStatus value, $Res Function(SnSubscriptionStatus) _then) = _$SnSubscriptionStatusCopyWithImpl;
abstract mixin class $SnPublisherSubscriptionCopyWith<$Res> {
factory $SnPublisherSubscriptionCopyWith(SnPublisherSubscription value, $Res Function(SnPublisherSubscription) _then) = _$SnPublisherSubscriptionCopyWithImpl;
@useResult
$Res call({
bool isSubscribed, String publisherId, String publisherName
String accountId, String publisherId, SnPublisher publisher
});
$SnPublisherCopyWith<$Res> get publisher;
}
/// @nodoc
class _$SnSubscriptionStatusCopyWithImpl<$Res>
implements $SnSubscriptionStatusCopyWith<$Res> {
_$SnSubscriptionStatusCopyWithImpl(this._self, this._then);
class _$SnPublisherSubscriptionCopyWithImpl<$Res>
implements $SnPublisherSubscriptionCopyWith<$Res> {
_$SnPublisherSubscriptionCopyWithImpl(this._self, this._then);
final SnSubscriptionStatus _self;
final $Res Function(SnSubscriptionStatus) _then;
final SnPublisherSubscription _self;
final $Res Function(SnPublisherSubscription) _then;
/// Create a copy of SnSubscriptionStatus
/// Create a copy of SnPublisherSubscription
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isSubscribed = null,Object? publisherId = null,Object? publisherName = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? accountId = null,Object? publisherId = null,Object? publisher = null,}) {
return _then(_self.copyWith(
isSubscribed: null == isSubscribed ? _self.isSubscribed : isSubscribed // ignore: cast_nullable_to_non_nullable
as bool,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,publisherName: null == publisherName ? _self.publisherName : publisherName // ignore: cast_nullable_to_non_nullable
as String,
accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as SnPublisher,
));
}
/// Create a copy of SnPublisherSubscription
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<$Res> get publisher {
return $SnPublisherCopyWith<$Res>(_self.publisher, (value) {
return _then(_self.copyWith(publisher: value));
});
}
}
/// Adds pattern-matching-related methods to [SnSubscriptionStatus].
extension SnSubscriptionStatusPatterns on SnSubscriptionStatus {
/// Adds pattern-matching-related methods to [SnPublisherSubscription].
extension SnPublisherSubscriptionPatterns on SnPublisherSubscription {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
@@ -934,10 +943,10 @@ extension SnSubscriptionStatusPatterns on SnSubscriptionStatus {
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnSubscriptionStatus value)? $default,{required TResult orElse(),}){
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPublisherSubscription value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnSubscriptionStatus() when $default != null:
case _SnPublisherSubscription() when $default != null:
return $default(_that);case _:
return orElse();
@@ -956,10 +965,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnSubscriptionStatus value) $default,){
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPublisherSubscription value) $default,){
final _that = this;
switch (_that) {
case _SnSubscriptionStatus():
case _SnPublisherSubscription():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
@@ -974,10 +983,10 @@ return $default(_that);}
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnSubscriptionStatus value)? $default,){
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPublisherSubscription value)? $default,){
final _that = this;
switch (_that) {
case _SnSubscriptionStatus() when $default != null:
case _SnPublisherSubscription() when $default != null:
return $default(_that);case _:
return null;
@@ -995,10 +1004,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isSubscribed, String publisherId, String publisherName)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String accountId, String publisherId, SnPublisher publisher)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnSubscriptionStatus() when $default != null:
return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);case _:
case _SnPublisherSubscription() when $default != null:
return $default(_that.accountId,_that.publisherId,_that.publisher);case _:
return orElse();
}
@@ -1016,10 +1025,10 @@ return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);case _
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isSubscribed, String publisherId, String publisherName) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String accountId, String publisherId, SnPublisher publisher) $default,) {final _that = this;
switch (_that) {
case _SnSubscriptionStatus():
return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);}
case _SnPublisherSubscription():
return $default(_that.accountId,_that.publisherId,_that.publisher);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -1033,10 +1042,10 @@ return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isSubscribed, String publisherId, String publisherName)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String accountId, String publisherId, SnPublisher publisher)? $default,) {final _that = this;
switch (_that) {
case _SnSubscriptionStatus() when $default != null:
return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);case _:
case _SnPublisherSubscription() when $default != null:
return $default(_that.accountId,_that.publisherId,_that.publisher);case _:
return null;
}
@@ -1047,74 +1056,83 @@ return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);case _
/// @nodoc
@JsonSerializable()
class _SnSubscriptionStatus implements SnSubscriptionStatus {
const _SnSubscriptionStatus({required this.isSubscribed, required this.publisherId, required this.publisherName});
factory _SnSubscriptionStatus.fromJson(Map<String, dynamic> json) => _$SnSubscriptionStatusFromJson(json);
class _SnPublisherSubscription implements SnPublisherSubscription {
const _SnPublisherSubscription({required this.accountId, required this.publisherId, required this.publisher});
factory _SnPublisherSubscription.fromJson(Map<String, dynamic> json) => _$SnPublisherSubscriptionFromJson(json);
@override final bool isSubscribed;
@override final String accountId;
@override final String publisherId;
@override final String publisherName;
@override final SnPublisher publisher;
/// Create a copy of SnSubscriptionStatus
/// Create a copy of SnPublisherSubscription
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnSubscriptionStatusCopyWith<_SnSubscriptionStatus> get copyWith => __$SnSubscriptionStatusCopyWithImpl<_SnSubscriptionStatus>(this, _$identity);
_$SnPublisherSubscriptionCopyWith<_SnPublisherSubscription> get copyWith => __$SnPublisherSubscriptionCopyWithImpl<_SnPublisherSubscription>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnSubscriptionStatusToJson(this, );
return _$SnPublisherSubscriptionToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSubscriptionStatus&&(identical(other.isSubscribed, isSubscribed) || other.isSubscribed == isSubscribed)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisherName, publisherName) || other.publisherName == publisherName));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisherSubscription&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,isSubscribed,publisherId,publisherName);
int get hashCode => Object.hash(runtimeType,accountId,publisherId,publisher);
@override
String toString() {
return 'SnSubscriptionStatus(isSubscribed: $isSubscribed, publisherId: $publisherId, publisherName: $publisherName)';
return 'SnPublisherSubscription(accountId: $accountId, publisherId: $publisherId, publisher: $publisher)';
}
}
/// @nodoc
abstract mixin class _$SnSubscriptionStatusCopyWith<$Res> implements $SnSubscriptionStatusCopyWith<$Res> {
factory _$SnSubscriptionStatusCopyWith(_SnSubscriptionStatus value, $Res Function(_SnSubscriptionStatus) _then) = __$SnSubscriptionStatusCopyWithImpl;
abstract mixin class _$SnPublisherSubscriptionCopyWith<$Res> implements $SnPublisherSubscriptionCopyWith<$Res> {
factory _$SnPublisherSubscriptionCopyWith(_SnPublisherSubscription value, $Res Function(_SnPublisherSubscription) _then) = __$SnPublisherSubscriptionCopyWithImpl;
@override @useResult
$Res call({
bool isSubscribed, String publisherId, String publisherName
String accountId, String publisherId, SnPublisher publisher
});
@override $SnPublisherCopyWith<$Res> get publisher;
}
/// @nodoc
class __$SnSubscriptionStatusCopyWithImpl<$Res>
implements _$SnSubscriptionStatusCopyWith<$Res> {
__$SnSubscriptionStatusCopyWithImpl(this._self, this._then);
class __$SnPublisherSubscriptionCopyWithImpl<$Res>
implements _$SnPublisherSubscriptionCopyWith<$Res> {
__$SnPublisherSubscriptionCopyWithImpl(this._self, this._then);
final _SnSubscriptionStatus _self;
final $Res Function(_SnSubscriptionStatus) _then;
final _SnPublisherSubscription _self;
final $Res Function(_SnPublisherSubscription) _then;
/// Create a copy of SnSubscriptionStatus
/// Create a copy of SnPublisherSubscription
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isSubscribed = null,Object? publisherId = null,Object? publisherName = null,}) {
return _then(_SnSubscriptionStatus(
isSubscribed: null == isSubscribed ? _self.isSubscribed : isSubscribed // ignore: cast_nullable_to_non_nullable
as bool,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,publisherName: null == publisherName ? _self.publisherName : publisherName // ignore: cast_nullable_to_non_nullable
as String,
@override @pragma('vm:prefer-inline') $Res call({Object? accountId = null,Object? publisherId = null,Object? publisher = null,}) {
return _then(_SnPublisherSubscription(
accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as SnPublisher,
));
}
/// Create a copy of SnPublisherSubscription
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<$Res> get publisher {
return $SnPublisherCopyWith<$Res>(_self.publisher, (value) {
return _then(_self.copyWith(publisher: value));
});
}
}
/// @nodoc

View File

@@ -158,20 +158,20 @@ Map<String, dynamic> _$SnPublisherStatsToJson(_SnPublisherStats instance) =>
'downvote_received': instance.downvoteReceived,
};
_SnSubscriptionStatus _$SnSubscriptionStatusFromJson(
_SnPublisherSubscription _$SnPublisherSubscriptionFromJson(
Map<String, dynamic> json,
) => _SnSubscriptionStatus(
isSubscribed: json['is_subscribed'] as bool,
) => _SnPublisherSubscription(
accountId: json['account_id'] as String,
publisherId: json['publisher_id'] as String,
publisherName: json['publisher_name'] as String,
publisher: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
);
Map<String, dynamic> _$SnSubscriptionStatusToJson(
_SnSubscriptionStatus instance,
Map<String, dynamic> _$SnPublisherSubscriptionToJson(
_SnPublisherSubscription instance,
) => <String, dynamic>{
'is_subscribed': instance.isSubscribed,
'account_id': instance.accountId,
'publisher_id': instance.publisherId,
'publisher_name': instance.publisherName,
'publisher': instance.publisher.toJson(),
};
_SnPostEmbedView _$SnPostEmbedViewFromJson(Map<String, dynamic> json) =>

View File

@@ -1,6 +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/models/post_tag.dart';
import 'package:island/utils/text.dart';
part 'post_category.freezed.dart';
@@ -29,3 +30,21 @@ sealed class SnPostCategory with _$SnPostCategory {
return name ?? slug;
}
}
@freezed
sealed class SnCategorySubscription with _$SnCategorySubscription {
const factory SnCategorySubscription({
required String id,
required String accountId,
required String? categoryId,
required SnPostCategory? category,
required String? tagId,
required SnPostTag? tag,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnCategorySubscription;
factory SnCategorySubscription.fromJson(Map<String, dynamic> json) =>
_$SnCategorySubscriptionFromJson(json);
}

View File

@@ -286,4 +286,333 @@ as int,
}
/// @nodoc
mixin _$SnCategorySubscription {
String get id; String get accountId; String? get categoryId; SnPostCategory? get category; String? get tagId; SnPostTag? get tag; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnCategorySubscription
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnCategorySubscriptionCopyWith<SnCategorySubscription> get copyWith => _$SnCategorySubscriptionCopyWithImpl<SnCategorySubscription>(this as SnCategorySubscription, _$identity);
/// Serializes this SnCategorySubscription to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCategorySubscription&&(identical(other.id, id) || other.id == id)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.categoryId, categoryId) || other.categoryId == categoryId)&&(identical(other.category, category) || other.category == category)&&(identical(other.tagId, tagId) || other.tagId == tagId)&&(identical(other.tag, tag) || other.tag == tag)&&(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,accountId,categoryId,category,tagId,tag,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnCategorySubscription(id: $id, accountId: $accountId, categoryId: $categoryId, category: $category, tagId: $tagId, tag: $tag, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnCategorySubscriptionCopyWith<$Res> {
factory $SnCategorySubscriptionCopyWith(SnCategorySubscription value, $Res Function(SnCategorySubscription) _then) = _$SnCategorySubscriptionCopyWithImpl;
@useResult
$Res call({
String id, String accountId, String? categoryId, SnPostCategory? category, String? tagId, SnPostTag? tag, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnPostCategoryCopyWith<$Res>? get category;$SnPostTagCopyWith<$Res>? get tag;
}
/// @nodoc
class _$SnCategorySubscriptionCopyWithImpl<$Res>
implements $SnCategorySubscriptionCopyWith<$Res> {
_$SnCategorySubscriptionCopyWithImpl(this._self, this._then);
final SnCategorySubscription _self;
final $Res Function(SnCategorySubscription) _then;
/// Create a copy of SnCategorySubscription
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? accountId = null,Object? categoryId = freezed,Object? category = freezed,Object? tagId = freezed,Object? tag = freezed,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,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,categoryId: freezed == categoryId ? _self.categoryId : categoryId // ignore: cast_nullable_to_non_nullable
as String?,category: freezed == category ? _self.category : category // ignore: cast_nullable_to_non_nullable
as SnPostCategory?,tagId: freezed == tagId ? _self.tagId : tagId // ignore: cast_nullable_to_non_nullable
as String?,tag: freezed == tag ? _self.tag : tag // ignore: cast_nullable_to_non_nullable
as SnPostTag?,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 SnCategorySubscription
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostCategoryCopyWith<$Res>? get category {
if (_self.category == null) {
return null;
}
return $SnPostCategoryCopyWith<$Res>(_self.category!, (value) {
return _then(_self.copyWith(category: value));
});
}/// Create a copy of SnCategorySubscription
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostTagCopyWith<$Res>? get tag {
if (_self.tag == null) {
return null;
}
return $SnPostTagCopyWith<$Res>(_self.tag!, (value) {
return _then(_self.copyWith(tag: value));
});
}
}
/// Adds pattern-matching-related methods to [SnCategorySubscription].
extension SnCategorySubscriptionPatterns on SnCategorySubscription {
/// 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( _SnCategorySubscription value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnCategorySubscription() 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( _SnCategorySubscription value) $default,){
final _that = this;
switch (_that) {
case _SnCategorySubscription():
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( _SnCategorySubscription value)? $default,){
final _that = this;
switch (_that) {
case _SnCategorySubscription() 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 id, String accountId, String? categoryId, SnPostCategory? category, String? tagId, SnPostTag? tag, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnCategorySubscription() when $default != null:
return $default(_that.id,_that.accountId,_that.categoryId,_that.category,_that.tagId,_that.tag,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String accountId, String? categoryId, SnPostCategory? category, String? tagId, SnPostTag? tag, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnCategorySubscription():
return $default(_that.id,_that.accountId,_that.categoryId,_that.category,_that.tagId,_that.tag,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String accountId, String? categoryId, SnPostCategory? category, String? tagId, SnPostTag? tag, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnCategorySubscription() when $default != null:
return $default(_that.id,_that.accountId,_that.categoryId,_that.category,_that.tagId,_that.tag,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnCategorySubscription implements SnCategorySubscription {
const _SnCategorySubscription({required this.id, required this.accountId, required this.categoryId, required this.category, required this.tagId, required this.tag, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnCategorySubscription.fromJson(Map<String, dynamic> json) => _$SnCategorySubscriptionFromJson(json);
@override final String id;
@override final String accountId;
@override final String? categoryId;
@override final SnPostCategory? category;
@override final String? tagId;
@override final SnPostTag? tag;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnCategorySubscription
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnCategorySubscriptionCopyWith<_SnCategorySubscription> get copyWith => __$SnCategorySubscriptionCopyWithImpl<_SnCategorySubscription>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnCategorySubscriptionToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCategorySubscription&&(identical(other.id, id) || other.id == id)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.categoryId, categoryId) || other.categoryId == categoryId)&&(identical(other.category, category) || other.category == category)&&(identical(other.tagId, tagId) || other.tagId == tagId)&&(identical(other.tag, tag) || other.tag == tag)&&(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,accountId,categoryId,category,tagId,tag,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnCategorySubscription(id: $id, accountId: $accountId, categoryId: $categoryId, category: $category, tagId: $tagId, tag: $tag, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnCategorySubscriptionCopyWith<$Res> implements $SnCategorySubscriptionCopyWith<$Res> {
factory _$SnCategorySubscriptionCopyWith(_SnCategorySubscription value, $Res Function(_SnCategorySubscription) _then) = __$SnCategorySubscriptionCopyWithImpl;
@override @useResult
$Res call({
String id, String accountId, String? categoryId, SnPostCategory? category, String? tagId, SnPostTag? tag, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnPostCategoryCopyWith<$Res>? get category;@override $SnPostTagCopyWith<$Res>? get tag;
}
/// @nodoc
class __$SnCategorySubscriptionCopyWithImpl<$Res>
implements _$SnCategorySubscriptionCopyWith<$Res> {
__$SnCategorySubscriptionCopyWithImpl(this._self, this._then);
final _SnCategorySubscription _self;
final $Res Function(_SnCategorySubscription) _then;
/// Create a copy of SnCategorySubscription
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? accountId = null,Object? categoryId = freezed,Object? category = freezed,Object? tagId = freezed,Object? tag = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnCategorySubscription(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,categoryId: freezed == categoryId ? _self.categoryId : categoryId // ignore: cast_nullable_to_non_nullable
as String?,category: freezed == category ? _self.category : category // ignore: cast_nullable_to_non_nullable
as SnPostCategory?,tagId: freezed == tagId ? _self.tagId : tagId // ignore: cast_nullable_to_non_nullable
as String?,tag: freezed == tag ? _self.tag : tag // ignore: cast_nullable_to_non_nullable
as SnPostTag?,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 SnCategorySubscription
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostCategoryCopyWith<$Res>? get category {
if (_self.category == null) {
return null;
}
return $SnPostCategoryCopyWith<$Res>(_self.category!, (value) {
return _then(_self.copyWith(category: value));
});
}/// Create a copy of SnCategorySubscription
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostTagCopyWith<$Res>? get tag {
if (_self.tag == null) {
return null;
}
return $SnPostTagCopyWith<$Res>(_self.tag!, (value) {
return _then(_self.copyWith(tag: value));
});
}
}
// dart format on

View File

@@ -27,3 +27,37 @@ Map<String, dynamic> _$SnPostCategoryToJson(_SnPostCategory instance) =>
'posts': instance.posts.map((e) => e.toJson()).toList(),
'usage': instance.usage,
};
_SnCategorySubscription _$SnCategorySubscriptionFromJson(
Map<String, dynamic> json,
) => _SnCategorySubscription(
id: json['id'] as String,
accountId: json['account_id'] as String,
categoryId: json['category_id'] as String?,
category: json['category'] == null
? null
: SnPostCategory.fromJson(json['category'] as Map<String, dynamic>),
tagId: json['tag_id'] as String?,
tag: json['tag'] == null
? null
: SnPostTag.fromJson(json['tag'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnCategorySubscriptionToJson(
_SnCategorySubscription instance,
) => <String, dynamic>{
'id': instance.id,
'account_id': instance.accountId,
'category_id': instance.categoryId,
'category': instance.category?.toJson(),
'tag_id': instance.tagId,
'tag': instance.tag?.toJson(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@@ -3,6 +3,28 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'publication_site.freezed.dart';
part 'publication_site.g.dart';
@freezed
sealed class SnPublicationSiteNavItems with _$SnPublicationSiteNavItems {
const factory SnPublicationSiteNavItems({
required String label,
required String href,
}) = _SnPublicationSiteNavItems;
factory SnPublicationSiteNavItems.fromJson(Map<String, dynamic> json) =>
_$SnPublicationSiteNavItemsFromJson(json);
}
@freezed
sealed class SnPublicationSiteConfig with _$SnPublicationSiteConfig {
const factory SnPublicationSiteConfig({
String? styleOverride,
List<SnPublicationSiteNavItems>? navItems,
}) = _SnPublicationSiteConfig;
factory SnPublicationSiteConfig.fromJson(Map<String, dynamic> json) =>
_$SnPublicationSiteConfigFromJson(json);
}
@freezed
sealed class SnPublicationSite with _$SnPublicationSite {
const factory SnPublicationSite({
@@ -16,6 +38,7 @@ sealed class SnPublicationSite with _$SnPublicationSite {
required DateTime createdAt,
required DateTime updatedAt,
required List<SnPublicationPage> pages,
required SnPublicationSiteConfig config,
}) = _SnPublicationSite;
factory SnPublicationSite.fromJson(Map<String, dynamic> json) =>

View File

@@ -12,10 +12,538 @@ part of 'publication_site.dart';
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPublicationSiteNavItems {
String get label; String get href;
/// Create a copy of SnPublicationSiteNavItems
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnPublicationSiteNavItemsCopyWith<SnPublicationSiteNavItems> get copyWith => _$SnPublicationSiteNavItemsCopyWithImpl<SnPublicationSiteNavItems>(this as SnPublicationSiteNavItems, _$identity);
/// Serializes this SnPublicationSiteNavItems to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublicationSiteNavItems&&(identical(other.label, label) || other.label == label)&&(identical(other.href, href) || other.href == href));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,label,href);
@override
String toString() {
return 'SnPublicationSiteNavItems(label: $label, href: $href)';
}
}
/// @nodoc
abstract mixin class $SnPublicationSiteNavItemsCopyWith<$Res> {
factory $SnPublicationSiteNavItemsCopyWith(SnPublicationSiteNavItems value, $Res Function(SnPublicationSiteNavItems) _then) = _$SnPublicationSiteNavItemsCopyWithImpl;
@useResult
$Res call({
String label, String href
});
}
/// @nodoc
class _$SnPublicationSiteNavItemsCopyWithImpl<$Res>
implements $SnPublicationSiteNavItemsCopyWith<$Res> {
_$SnPublicationSiteNavItemsCopyWithImpl(this._self, this._then);
final SnPublicationSiteNavItems _self;
final $Res Function(SnPublicationSiteNavItems) _then;
/// Create a copy of SnPublicationSiteNavItems
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? label = null,Object? href = null,}) {
return _then(_self.copyWith(
label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String,href: null == href ? _self.href : href // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [SnPublicationSiteNavItems].
extension SnPublicationSiteNavItemsPatterns on SnPublicationSiteNavItems {
/// 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( _SnPublicationSiteNavItems value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnPublicationSiteNavItems() 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( _SnPublicationSiteNavItems value) $default,){
final _that = this;
switch (_that) {
case _SnPublicationSiteNavItems():
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( _SnPublicationSiteNavItems value)? $default,){
final _that = this;
switch (_that) {
case _SnPublicationSiteNavItems() 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 label, String href)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPublicationSiteNavItems() when $default != null:
return $default(_that.label,_that.href);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 label, String href) $default,) {final _that = this;
switch (_that) {
case _SnPublicationSiteNavItems():
return $default(_that.label,_that.href);}
}
/// 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 label, String href)? $default,) {final _that = this;
switch (_that) {
case _SnPublicationSiteNavItems() when $default != null:
return $default(_that.label,_that.href);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnPublicationSiteNavItems implements SnPublicationSiteNavItems {
const _SnPublicationSiteNavItems({required this.label, required this.href});
factory _SnPublicationSiteNavItems.fromJson(Map<String, dynamic> json) => _$SnPublicationSiteNavItemsFromJson(json);
@override final String label;
@override final String href;
/// Create a copy of SnPublicationSiteNavItems
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnPublicationSiteNavItemsCopyWith<_SnPublicationSiteNavItems> get copyWith => __$SnPublicationSiteNavItemsCopyWithImpl<_SnPublicationSiteNavItems>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnPublicationSiteNavItemsToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublicationSiteNavItems&&(identical(other.label, label) || other.label == label)&&(identical(other.href, href) || other.href == href));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,label,href);
@override
String toString() {
return 'SnPublicationSiteNavItems(label: $label, href: $href)';
}
}
/// @nodoc
abstract mixin class _$SnPublicationSiteNavItemsCopyWith<$Res> implements $SnPublicationSiteNavItemsCopyWith<$Res> {
factory _$SnPublicationSiteNavItemsCopyWith(_SnPublicationSiteNavItems value, $Res Function(_SnPublicationSiteNavItems) _then) = __$SnPublicationSiteNavItemsCopyWithImpl;
@override @useResult
$Res call({
String label, String href
});
}
/// @nodoc
class __$SnPublicationSiteNavItemsCopyWithImpl<$Res>
implements _$SnPublicationSiteNavItemsCopyWith<$Res> {
__$SnPublicationSiteNavItemsCopyWithImpl(this._self, this._then);
final _SnPublicationSiteNavItems _self;
final $Res Function(_SnPublicationSiteNavItems) _then;
/// Create a copy of SnPublicationSiteNavItems
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? label = null,Object? href = null,}) {
return _then(_SnPublicationSiteNavItems(
label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String,href: null == href ? _self.href : href // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
mixin _$SnPublicationSiteConfig {
String? get styleOverride; List<SnPublicationSiteNavItems>? get navItems;
/// Create a copy of SnPublicationSiteConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnPublicationSiteConfigCopyWith<SnPublicationSiteConfig> get copyWith => _$SnPublicationSiteConfigCopyWithImpl<SnPublicationSiteConfig>(this as SnPublicationSiteConfig, _$identity);
/// Serializes this SnPublicationSiteConfig to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublicationSiteConfig&&(identical(other.styleOverride, styleOverride) || other.styleOverride == styleOverride)&&const DeepCollectionEquality().equals(other.navItems, navItems));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,styleOverride,const DeepCollectionEquality().hash(navItems));
@override
String toString() {
return 'SnPublicationSiteConfig(styleOverride: $styleOverride, navItems: $navItems)';
}
}
/// @nodoc
abstract mixin class $SnPublicationSiteConfigCopyWith<$Res> {
factory $SnPublicationSiteConfigCopyWith(SnPublicationSiteConfig value, $Res Function(SnPublicationSiteConfig) _then) = _$SnPublicationSiteConfigCopyWithImpl;
@useResult
$Res call({
String? styleOverride, List<SnPublicationSiteNavItems>? navItems
});
}
/// @nodoc
class _$SnPublicationSiteConfigCopyWithImpl<$Res>
implements $SnPublicationSiteConfigCopyWith<$Res> {
_$SnPublicationSiteConfigCopyWithImpl(this._self, this._then);
final SnPublicationSiteConfig _self;
final $Res Function(SnPublicationSiteConfig) _then;
/// Create a copy of SnPublicationSiteConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? styleOverride = freezed,Object? navItems = freezed,}) {
return _then(_self.copyWith(
styleOverride: freezed == styleOverride ? _self.styleOverride : styleOverride // ignore: cast_nullable_to_non_nullable
as String?,navItems: freezed == navItems ? _self.navItems : navItems // ignore: cast_nullable_to_non_nullable
as List<SnPublicationSiteNavItems>?,
));
}
}
/// Adds pattern-matching-related methods to [SnPublicationSiteConfig].
extension SnPublicationSiteConfigPatterns on SnPublicationSiteConfig {
/// 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( _SnPublicationSiteConfig value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnPublicationSiteConfig() 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( _SnPublicationSiteConfig value) $default,){
final _that = this;
switch (_that) {
case _SnPublicationSiteConfig():
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( _SnPublicationSiteConfig value)? $default,){
final _that = this;
switch (_that) {
case _SnPublicationSiteConfig() 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? styleOverride, List<SnPublicationSiteNavItems>? navItems)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPublicationSiteConfig() when $default != null:
return $default(_that.styleOverride,_that.navItems);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? styleOverride, List<SnPublicationSiteNavItems>? navItems) $default,) {final _that = this;
switch (_that) {
case _SnPublicationSiteConfig():
return $default(_that.styleOverride,_that.navItems);}
}
/// 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? styleOverride, List<SnPublicationSiteNavItems>? navItems)? $default,) {final _that = this;
switch (_that) {
case _SnPublicationSiteConfig() when $default != null:
return $default(_that.styleOverride,_that.navItems);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnPublicationSiteConfig implements SnPublicationSiteConfig {
const _SnPublicationSiteConfig({this.styleOverride, final List<SnPublicationSiteNavItems>? navItems}): _navItems = navItems;
factory _SnPublicationSiteConfig.fromJson(Map<String, dynamic> json) => _$SnPublicationSiteConfigFromJson(json);
@override final String? styleOverride;
final List<SnPublicationSiteNavItems>? _navItems;
@override List<SnPublicationSiteNavItems>? get navItems {
final value = _navItems;
if (value == null) return null;
if (_navItems is EqualUnmodifiableListView) return _navItems;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
/// Create a copy of SnPublicationSiteConfig
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnPublicationSiteConfigCopyWith<_SnPublicationSiteConfig> get copyWith => __$SnPublicationSiteConfigCopyWithImpl<_SnPublicationSiteConfig>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnPublicationSiteConfigToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublicationSiteConfig&&(identical(other.styleOverride, styleOverride) || other.styleOverride == styleOverride)&&const DeepCollectionEquality().equals(other._navItems, _navItems));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,styleOverride,const DeepCollectionEquality().hash(_navItems));
@override
String toString() {
return 'SnPublicationSiteConfig(styleOverride: $styleOverride, navItems: $navItems)';
}
}
/// @nodoc
abstract mixin class _$SnPublicationSiteConfigCopyWith<$Res> implements $SnPublicationSiteConfigCopyWith<$Res> {
factory _$SnPublicationSiteConfigCopyWith(_SnPublicationSiteConfig value, $Res Function(_SnPublicationSiteConfig) _then) = __$SnPublicationSiteConfigCopyWithImpl;
@override @useResult
$Res call({
String? styleOverride, List<SnPublicationSiteNavItems>? navItems
});
}
/// @nodoc
class __$SnPublicationSiteConfigCopyWithImpl<$Res>
implements _$SnPublicationSiteConfigCopyWith<$Res> {
__$SnPublicationSiteConfigCopyWithImpl(this._self, this._then);
final _SnPublicationSiteConfig _self;
final $Res Function(_SnPublicationSiteConfig) _then;
/// Create a copy of SnPublicationSiteConfig
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? styleOverride = freezed,Object? navItems = freezed,}) {
return _then(_SnPublicationSiteConfig(
styleOverride: freezed == styleOverride ? _self.styleOverride : styleOverride // ignore: cast_nullable_to_non_nullable
as String?,navItems: freezed == navItems ? _self._navItems : navItems // ignore: cast_nullable_to_non_nullable
as List<SnPublicationSiteNavItems>?,
));
}
}
/// @nodoc
mixin _$SnPublicationSite {
String get id; String get slug; String get name; String? get description; int? get mode; String get publisherId; String get accountId; DateTime get createdAt; DateTime get updatedAt; List<SnPublicationPage> get pages;
String get id; String get slug; String get name; String? get description; int? get mode; String get publisherId; String get accountId; DateTime get createdAt; DateTime get updatedAt; List<SnPublicationPage> get pages; SnPublicationSiteConfig get config;
/// Create a copy of SnPublicationSite
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +556,16 @@ $SnPublicationSiteCopyWith<SnPublicationSite> get copyWith => _$SnPublicationSit
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublicationSite&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other.pages, pages));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublicationSite&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other.pages, pages)&&(identical(other.config, config) || other.config == config));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,description,mode,publisherId,accountId,createdAt,updatedAt,const DeepCollectionEquality().hash(pages));
int get hashCode => Object.hash(runtimeType,id,slug,name,description,mode,publisherId,accountId,createdAt,updatedAt,const DeepCollectionEquality().hash(pages),config);
@override
String toString() {
return 'SnPublicationSite(id: $id, slug: $slug, name: $name, description: $description, mode: $mode, publisherId: $publisherId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, pages: $pages)';
return 'SnPublicationSite(id: $id, slug: $slug, name: $name, description: $description, mode: $mode, publisherId: $publisherId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, pages: $pages, config: $config)';
}
@@ -48,11 +576,11 @@ abstract mixin class $SnPublicationSiteCopyWith<$Res> {
factory $SnPublicationSiteCopyWith(SnPublicationSite value, $Res Function(SnPublicationSite) _then) = _$SnPublicationSiteCopyWithImpl;
@useResult
$Res call({
String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages
String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages, SnPublicationSiteConfig config
});
$SnPublicationSiteConfigCopyWith<$Res> get config;
}
/// @nodoc
@@ -65,7 +593,7 @@ class _$SnPublicationSiteCopyWithImpl<$Res>
/// Create a copy of SnPublicationSite
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? mode = freezed,Object? publisherId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? pages = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? mode = freezed,Object? publisherId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? pages = null,Object? config = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
@@ -77,10 +605,20 @@ as String,accountId: null == accountId ? _self.accountId : accountId // ignore:
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,pages: null == pages ? _self.pages : pages // ignore: cast_nullable_to_non_nullable
as List<SnPublicationPage>,
as List<SnPublicationPage>,config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
as SnPublicationSiteConfig,
));
}
/// Create a copy of SnPublicationSite
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPublicationSiteConfigCopyWith<$Res> get config {
return $SnPublicationSiteConfigCopyWith<$Res>(_self.config, (value) {
return _then(_self.copyWith(config: value));
});
}
}
@@ -159,10 +697,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages, SnPublicationSiteConfig config)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPublicationSite() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages);case _:
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages,_that.config);case _:
return orElse();
}
@@ -180,10 +718,10 @@ return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_tha
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages, SnPublicationSiteConfig config) $default,) {final _that = this;
switch (_that) {
case _SnPublicationSite():
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages);}
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages,_that.config);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -197,10 +735,10 @@ return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_tha
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages, SnPublicationSiteConfig config)? $default,) {final _that = this;
switch (_that) {
case _SnPublicationSite() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages);case _:
return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_that.publisherId,_that.accountId,_that.createdAt,_that.updatedAt,_that.pages,_that.config);case _:
return null;
}
@@ -212,7 +750,7 @@ return $default(_that.id,_that.slug,_that.name,_that.description,_that.mode,_tha
@JsonSerializable()
class _SnPublicationSite implements SnPublicationSite {
const _SnPublicationSite({required this.id, required this.slug, required this.name, this.description, this.mode, required this.publisherId, required this.accountId, required this.createdAt, required this.updatedAt, required final List<SnPublicationPage> pages}): _pages = pages;
const _SnPublicationSite({required this.id, required this.slug, required this.name, this.description, this.mode, required this.publisherId, required this.accountId, required this.createdAt, required this.updatedAt, required final List<SnPublicationPage> pages, required this.config}): _pages = pages;
factory _SnPublicationSite.fromJson(Map<String, dynamic> json) => _$SnPublicationSiteFromJson(json);
@override final String id;
@@ -231,6 +769,7 @@ class _SnPublicationSite implements SnPublicationSite {
return EqualUnmodifiableListView(_pages);
}
@override final SnPublicationSiteConfig config;
/// Create a copy of SnPublicationSite
/// with the given fields replaced by the non-null parameter values.
@@ -245,16 +784,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublicationSite&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other._pages, _pages));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublicationSite&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other._pages, _pages)&&(identical(other.config, config) || other.config == config));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,description,mode,publisherId,accountId,createdAt,updatedAt,const DeepCollectionEquality().hash(_pages));
int get hashCode => Object.hash(runtimeType,id,slug,name,description,mode,publisherId,accountId,createdAt,updatedAt,const DeepCollectionEquality().hash(_pages),config);
@override
String toString() {
return 'SnPublicationSite(id: $id, slug: $slug, name: $name, description: $description, mode: $mode, publisherId: $publisherId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, pages: $pages)';
return 'SnPublicationSite(id: $id, slug: $slug, name: $name, description: $description, mode: $mode, publisherId: $publisherId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, pages: $pages, config: $config)';
}
@@ -265,11 +804,11 @@ abstract mixin class _$SnPublicationSiteCopyWith<$Res> implements $SnPublication
factory _$SnPublicationSiteCopyWith(_SnPublicationSite value, $Res Function(_SnPublicationSite) _then) = __$SnPublicationSiteCopyWithImpl;
@override @useResult
$Res call({
String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages
String id, String slug, String name, String? description, int? mode, String publisherId, String accountId, DateTime createdAt, DateTime updatedAt, List<SnPublicationPage> pages, SnPublicationSiteConfig config
});
@override $SnPublicationSiteConfigCopyWith<$Res> get config;
}
/// @nodoc
@@ -282,7 +821,7 @@ class __$SnPublicationSiteCopyWithImpl<$Res>
/// Create a copy of SnPublicationSite
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? mode = freezed,Object? publisherId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? pages = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? mode = freezed,Object? publisherId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? pages = null,Object? config = null,}) {
return _then(_SnPublicationSite(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
@@ -294,11 +833,21 @@ as String,accountId: null == accountId ? _self.accountId : accountId // ignore:
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,pages: null == pages ? _self._pages : pages // ignore: cast_nullable_to_non_nullable
as List<SnPublicationPage>,
as List<SnPublicationPage>,config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
as SnPublicationSiteConfig,
));
}
/// Create a copy of SnPublicationSite
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPublicationSiteConfigCopyWith<$Res> get config {
return $SnPublicationSiteConfigCopyWith<$Res>(_self.config, (value) {
return _then(_self.copyWith(config: value));
});
}
}

View File

@@ -6,6 +6,35 @@ part of 'publication_site.dart';
// JsonSerializableGenerator
// **************************************************************************
_SnPublicationSiteNavItems _$SnPublicationSiteNavItemsFromJson(
Map<String, dynamic> json,
) => _SnPublicationSiteNavItems(
label: json['label'] as String,
href: json['href'] as String,
);
Map<String, dynamic> _$SnPublicationSiteNavItemsToJson(
_SnPublicationSiteNavItems instance,
) => <String, dynamic>{'label': instance.label, 'href': instance.href};
_SnPublicationSiteConfig _$SnPublicationSiteConfigFromJson(
Map<String, dynamic> json,
) => _SnPublicationSiteConfig(
styleOverride: json['style_override'] as String?,
navItems: (json['nav_items'] as List<dynamic>?)
?.map(
(e) => SnPublicationSiteNavItems.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$SnPublicationSiteConfigToJson(
_SnPublicationSiteConfig instance,
) => <String, dynamic>{
'style_override': instance.styleOverride,
'nav_items': instance.navItems?.map((e) => e.toJson()).toList(),
};
_SnPublicationSite _$SnPublicationSiteFromJson(Map<String, dynamic> json) =>
_SnPublicationSite(
id: json['id'] as String,
@@ -20,6 +49,9 @@ _SnPublicationSite _$SnPublicationSiteFromJson(Map<String, dynamic> json) =>
pages: (json['pages'] as List<dynamic>)
.map((e) => SnPublicationPage.fromJson(e as Map<String, dynamic>))
.toList(),
config: SnPublicationSiteConfig.fromJson(
json['config'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$SnPublicationSiteToJson(_SnPublicationSite instance) =>
@@ -34,6 +66,7 @@ Map<String, dynamic> _$SnPublicationSiteToJson(_SnPublicationSite instance) =>
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'pages': instance.pages.map((e) => e.toJson()).toList(),
'config': instance.config.toJson(),
};
_SnPublicationPage _$SnPublicationPageFromJson(Map<String, dynamic> json) =>

231
lib/models/route_item.dart Normal file
View File

@@ -0,0 +1,231 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:material_symbols_icons/symbols.dart';
part 'route_item.freezed.dart';
@freezed
sealed class RouteItem with _$RouteItem {
const factory RouteItem({
required String name,
required String path,
required String description,
@Default([]) List<String> searchableAliases,
required IconData icon,
}) = _RouteItem;
}
final List<RouteItem> kAvailableRoutes = [
RouteItem(
name: 'dashboard'.tr(),
path: '/',
description: 'dashboardDescription'.tr(),
searchableAliases: ['dashboard', 'home'],
icon: Symbols.home,
),
RouteItem(
name: 'explore'.tr(),
path: '/explore',
description: 'exploreDescription'.tr(),
searchableAliases: ['explore', 'discover'],
icon: Symbols.explore,
),
RouteItem(
name: 'searchPosts'.tr(),
path: '/posts/search',
description: 'searchPostsDescription'.tr(),
searchableAliases: ['search', 'posts'],
icon: Symbols.search,
),
RouteItem(
name: 'postShuffle'.tr(),
path: '/posts/shuffle',
description: 'postShuffleDescription'.tr(),
searchableAliases: ['shuffle', 'random', 'posts'],
icon: Symbols.shuffle,
),
RouteItem(
name: 'postTagsCategories'.tr(),
path: '/posts/categories',
description: 'postTagsCategoriesDescription'.tr(),
searchableAliases: ['tags', 'categories', 'posts'],
icon: Symbols.category,
),
RouteItem(
name: 'discoverRealms'.tr(),
path: '/discovery/realms',
description: 'discoverRealmsDescription'.tr(),
searchableAliases: ['realms', 'groups', 'communities'],
icon: Symbols.public,
),
RouteItem(
name: 'chat'.tr(),
path: '/chat',
description: 'chatDescription'.tr(),
searchableAliases: ['chat', 'messages', 'conversations', 'dm'],
icon: Symbols.chat,
),
RouteItem(
name: 'realms'.tr(),
path: '/realms',
description: 'realmsDescription'.tr(),
searchableAliases: ['realms', 'groups', 'communities'],
icon: Symbols.group,
),
RouteItem(
name: 'account'.tr(),
path: '/account',
description: 'accountDescription'.tr(),
searchableAliases: ['account', 'me', 'profile', 'user'],
icon: Symbols.person,
),
RouteItem(
name: 'stickerMarketplace'.tr(),
path: '/stickers',
description: 'stickerMarketplaceDescription'.tr(),
searchableAliases: ['stickers', 'marketplace', 'emojis', 'emojis'],
icon: Symbols.emoji_emotions,
),
RouteItem(
name: 'webFeeds'.tr(),
path: '/feeds',
description: 'webFeedsDescription'.tr(),
searchableAliases: ['feeds', 'web feeds', 'rss', 'news'],
icon: Symbols.feed,
),
RouteItem(
name: 'wallet'.tr(),
path: '/account/wallet',
description: 'walletDescription'.tr(),
searchableAliases: [
'wallet',
'balance',
'money',
'source points',
'gold points',
'nsp',
'shd',
],
icon: Symbols.account_balance_wallet,
),
RouteItem(
name: 'relationships'.tr(),
path: '/account/relationships',
description: 'relationshipsDescription'.tr(),
searchableAliases: ['relationships', 'friends', 'block list', 'blocks'],
icon: Symbols.people,
),
RouteItem(
name: 'updateYourProfile'.tr(),
path: '/account/me/update',
description: 'updateYourProfileDescription'.tr(),
searchableAliases: ['profile', 'update', 'edit', 'my profile'],
icon: Symbols.edit,
),
RouteItem(
name: 'leveling'.tr(),
path: '/account/me/leveling',
description: 'levelingDescription'.tr(),
searchableAliases: [
'leveling',
'level',
'levels',
'subscriptions',
'social credits',
],
icon: Symbols.trending_up,
),
RouteItem(
name: 'accountSettings'.tr(),
path: '/account/me/settings',
description: 'accountSettingsDescription'.tr(),
searchableAliases: [
'settings',
'preferences',
'account',
'account settings',
],
icon: Symbols.settings,
),
RouteItem(
name: 'abuseReports'.tr(),
path: '/safety/reports/me',
description: 'abuseReportsDescription'.tr(),
searchableAliases: ['reports', 'abuse', 'safety'],
icon: Symbols.report,
),
RouteItem(
name: 'files'.tr(),
path: '/files',
description: 'filesDescription'.tr(),
searchableAliases: ['files', 'folders', 'storage', 'drive', 'cloud'],
icon: Symbols.folder,
),
RouteItem(
name: 'aiThought'.tr(),
path: '/thought',
description: 'aiThoughtTitle'.tr(),
searchableAliases: ['thought', 'ai', 'ai thought'],
icon: Symbols.psychology,
),
RouteItem(
name: 'creatorHub'.tr(),
path: '/creators',
description: 'creatorHubDescription'.tr(),
searchableAliases: ['creators', 'hub', 'creator hub', 'creators hub'],
icon: Symbols.create,
),
RouteItem(
name: 'developerPortal'.tr(),
path: '/developers',
description: 'developerPortalDescription'.tr(),
searchableAliases: [
'developers',
'dev',
'developer',
'developer hub',
'developers hub',
],
icon: Symbols.code,
),
RouteItem(
name: 'debugLogs'.tr(),
path: '/logs',
description: 'debugLogsDescription'.tr(),
searchableAliases: ['logs', 'debug', 'debug logs'],
icon: Symbols.bug_report,
),
RouteItem(
name: 'webArticlesStand'.tr(),
path: '/feeds/articles',
description: 'webArticlesStandDescription'.tr(),
searchableAliases: ['articles', 'stand', 'feed', 'web feed'],
icon: Symbols.article,
),
RouteItem(
name: 'appSettings'.tr(),
path: '/settings',
description: 'appSettingsDescription'.tr(),
searchableAliases: ['settings', 'preferences', 'app', 'app settings'],
icon: Symbols.settings,
),
RouteItem(
name: 'about'.tr(),
path: '/about',
description: 'about'.tr(),
searchableAliases: ['about', 'info'],
icon: Symbols.info,
),
];
@freezed
sealed class SpecialAction with _$SpecialAction {
const factory SpecialAction({
required String name,
required String description,
required IconData icon,
required VoidCallback action,
@Default([]) List<String> searchableAliases,
}) = _SpecialAction;
}

View File

@@ -0,0 +1,552 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'route_item.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$RouteItem {
String get name; String get path; String get description; List<String> get searchableAliases; IconData get icon;
/// Create a copy of RouteItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$RouteItemCopyWith<RouteItem> get copyWith => _$RouteItemCopyWithImpl<RouteItem>(this as RouteItem, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RouteItem&&(identical(other.name, name) || other.name == name)&&(identical(other.path, path) || other.path == path)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.searchableAliases, searchableAliases)&&(identical(other.icon, icon) || other.icon == icon));
}
@override
int get hashCode => Object.hash(runtimeType,name,path,description,const DeepCollectionEquality().hash(searchableAliases),icon);
@override
String toString() {
return 'RouteItem(name: $name, path: $path, description: $description, searchableAliases: $searchableAliases, icon: $icon)';
}
}
/// @nodoc
abstract mixin class $RouteItemCopyWith<$Res> {
factory $RouteItemCopyWith(RouteItem value, $Res Function(RouteItem) _then) = _$RouteItemCopyWithImpl;
@useResult
$Res call({
String name, String path, String description, List<String> searchableAliases, IconData icon
});
}
/// @nodoc
class _$RouteItemCopyWithImpl<$Res>
implements $RouteItemCopyWith<$Res> {
_$RouteItemCopyWithImpl(this._self, this._then);
final RouteItem _self;
final $Res Function(RouteItem) _then;
/// Create a copy of RouteItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? path = null,Object? description = null,Object? searchableAliases = null,Object? icon = null,}) {
return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,path: null == path ? _self.path : path // ignore: cast_nullable_to_non_nullable
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String,searchableAliases: null == searchableAliases ? _self.searchableAliases : searchableAliases // ignore: cast_nullable_to_non_nullable
as List<String>,icon: null == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
as IconData,
));
}
}
/// Adds pattern-matching-related methods to [RouteItem].
extension RouteItemPatterns on RouteItem {
/// 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( _RouteItem value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _RouteItem() 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( _RouteItem value) $default,){
final _that = this;
switch (_that) {
case _RouteItem():
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( _RouteItem value)? $default,){
final _that = this;
switch (_that) {
case _RouteItem() 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 name, String path, String description, List<String> searchableAliases, IconData icon)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _RouteItem() when $default != null:
return $default(_that.name,_that.path,_that.description,_that.searchableAliases,_that.icon);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 name, String path, String description, List<String> searchableAliases, IconData icon) $default,) {final _that = this;
switch (_that) {
case _RouteItem():
return $default(_that.name,_that.path,_that.description,_that.searchableAliases,_that.icon);}
}
/// 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 name, String path, String description, List<String> searchableAliases, IconData icon)? $default,) {final _that = this;
switch (_that) {
case _RouteItem() when $default != null:
return $default(_that.name,_that.path,_that.description,_that.searchableAliases,_that.icon);case _:
return null;
}
}
}
/// @nodoc
class _RouteItem implements RouteItem {
const _RouteItem({required this.name, required this.path, required this.description, final List<String> searchableAliases = const [], required this.icon}): _searchableAliases = searchableAliases;
@override final String name;
@override final String path;
@override final String description;
final List<String> _searchableAliases;
@override@JsonKey() List<String> get searchableAliases {
if (_searchableAliases is EqualUnmodifiableListView) return _searchableAliases;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_searchableAliases);
}
@override final IconData icon;
/// Create a copy of RouteItem
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$RouteItemCopyWith<_RouteItem> get copyWith => __$RouteItemCopyWithImpl<_RouteItem>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RouteItem&&(identical(other.name, name) || other.name == name)&&(identical(other.path, path) || other.path == path)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._searchableAliases, _searchableAliases)&&(identical(other.icon, icon) || other.icon == icon));
}
@override
int get hashCode => Object.hash(runtimeType,name,path,description,const DeepCollectionEquality().hash(_searchableAliases),icon);
@override
String toString() {
return 'RouteItem(name: $name, path: $path, description: $description, searchableAliases: $searchableAliases, icon: $icon)';
}
}
/// @nodoc
abstract mixin class _$RouteItemCopyWith<$Res> implements $RouteItemCopyWith<$Res> {
factory _$RouteItemCopyWith(_RouteItem value, $Res Function(_RouteItem) _then) = __$RouteItemCopyWithImpl;
@override @useResult
$Res call({
String name, String path, String description, List<String> searchableAliases, IconData icon
});
}
/// @nodoc
class __$RouteItemCopyWithImpl<$Res>
implements _$RouteItemCopyWith<$Res> {
__$RouteItemCopyWithImpl(this._self, this._then);
final _RouteItem _self;
final $Res Function(_RouteItem) _then;
/// Create a copy of RouteItem
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? path = null,Object? description = null,Object? searchableAliases = null,Object? icon = null,}) {
return _then(_RouteItem(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,path: null == path ? _self.path : path // ignore: cast_nullable_to_non_nullable
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String,searchableAliases: null == searchableAliases ? _self._searchableAliases : searchableAliases // ignore: cast_nullable_to_non_nullable
as List<String>,icon: null == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
as IconData,
));
}
}
/// @nodoc
mixin _$SpecialAction {
String get name; String get description; IconData get icon; VoidCallback get action; List<String> get searchableAliases;
/// Create a copy of SpecialAction
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SpecialActionCopyWith<SpecialAction> get copyWith => _$SpecialActionCopyWithImpl<SpecialAction>(this as SpecialAction, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SpecialAction&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.action, action) || other.action == action)&&const DeepCollectionEquality().equals(other.searchableAliases, searchableAliases));
}
@override
int get hashCode => Object.hash(runtimeType,name,description,icon,action,const DeepCollectionEquality().hash(searchableAliases));
@override
String toString() {
return 'SpecialAction(name: $name, description: $description, icon: $icon, action: $action, searchableAliases: $searchableAliases)';
}
}
/// @nodoc
abstract mixin class $SpecialActionCopyWith<$Res> {
factory $SpecialActionCopyWith(SpecialAction value, $Res Function(SpecialAction) _then) = _$SpecialActionCopyWithImpl;
@useResult
$Res call({
String name, String description, IconData icon, VoidCallback action, List<String> searchableAliases
});
}
/// @nodoc
class _$SpecialActionCopyWithImpl<$Res>
implements $SpecialActionCopyWith<$Res> {
_$SpecialActionCopyWithImpl(this._self, this._then);
final SpecialAction _self;
final $Res Function(SpecialAction) _then;
/// Create a copy of SpecialAction
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? description = null,Object? icon = null,Object? action = null,Object? searchableAliases = null,}) {
return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String,icon: null == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
as IconData,action: null == action ? _self.action : action // ignore: cast_nullable_to_non_nullable
as VoidCallback,searchableAliases: null == searchableAliases ? _self.searchableAliases : searchableAliases // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
/// Adds pattern-matching-related methods to [SpecialAction].
extension SpecialActionPatterns on SpecialAction {
/// 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( _SpecialAction value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SpecialAction() 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( _SpecialAction value) $default,){
final _that = this;
switch (_that) {
case _SpecialAction():
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( _SpecialAction value)? $default,){
final _that = this;
switch (_that) {
case _SpecialAction() 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 name, String description, IconData icon, VoidCallback action, List<String> searchableAliases)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SpecialAction() when $default != null:
return $default(_that.name,_that.description,_that.icon,_that.action,_that.searchableAliases);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 name, String description, IconData icon, VoidCallback action, List<String> searchableAliases) $default,) {final _that = this;
switch (_that) {
case _SpecialAction():
return $default(_that.name,_that.description,_that.icon,_that.action,_that.searchableAliases);}
}
/// 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 name, String description, IconData icon, VoidCallback action, List<String> searchableAliases)? $default,) {final _that = this;
switch (_that) {
case _SpecialAction() when $default != null:
return $default(_that.name,_that.description,_that.icon,_that.action,_that.searchableAliases);case _:
return null;
}
}
}
/// @nodoc
class _SpecialAction implements SpecialAction {
const _SpecialAction({required this.name, required this.description, required this.icon, required this.action, final List<String> searchableAliases = const []}): _searchableAliases = searchableAliases;
@override final String name;
@override final String description;
@override final IconData icon;
@override final VoidCallback action;
final List<String> _searchableAliases;
@override@JsonKey() List<String> get searchableAliases {
if (_searchableAliases is EqualUnmodifiableListView) return _searchableAliases;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_searchableAliases);
}
/// Create a copy of SpecialAction
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SpecialActionCopyWith<_SpecialAction> get copyWith => __$SpecialActionCopyWithImpl<_SpecialAction>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SpecialAction&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.action, action) || other.action == action)&&const DeepCollectionEquality().equals(other._searchableAliases, _searchableAliases));
}
@override
int get hashCode => Object.hash(runtimeType,name,description,icon,action,const DeepCollectionEquality().hash(_searchableAliases));
@override
String toString() {
return 'SpecialAction(name: $name, description: $description, icon: $icon, action: $action, searchableAliases: $searchableAliases)';
}
}
/// @nodoc
abstract mixin class _$SpecialActionCopyWith<$Res> implements $SpecialActionCopyWith<$Res> {
factory _$SpecialActionCopyWith(_SpecialAction value, $Res Function(_SpecialAction) _then) = __$SpecialActionCopyWithImpl;
@override @useResult
$Res call({
String name, String description, IconData icon, VoidCallback action, List<String> searchableAliases
});
}
/// @nodoc
class __$SpecialActionCopyWithImpl<$Res>
implements _$SpecialActionCopyWith<$Res> {
__$SpecialActionCopyWithImpl(this._self, this._then);
final _SpecialAction _self;
final $Res Function(_SpecialAction) _then;
/// Create a copy of SpecialAction
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? description = null,Object? icon = null,Object? action = null,Object? searchableAliases = null,}) {
return _then(_SpecialAction(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String,icon: null == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
as IconData,action: null == action ? _self.action : action // ignore: cast_nullable_to_non_nullable
as VoidCallback,searchableAliases: null == searchableAliases ? _self._searchableAliases : searchableAliases // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
// dart format on

View File

@@ -16,6 +16,8 @@ import 'package:island/talker.dart';
part 'call.g.dart';
part 'call.freezed.dart';
enum ViewMode { grid, stage }
String formatDuration(Duration duration) {
String negativeSign = duration.isNegative ? '-' : '';
String twoDigits(int n) => n.toString().padLeft(2, "0");
@@ -33,6 +35,7 @@ sealed class CallState with _$CallState {
required bool isScreenSharing,
required bool isSpeakerphone,
@Default(Duration(seconds: 0)) Duration duration,
@Default(ViewMode.grid) ViewMode viewMode,
String? error,
}) = _CallState;
}
@@ -84,6 +87,7 @@ class CallNotifier extends _$CallNotifier {
isCameraEnabled: false,
isScreenSharing: false,
isSpeakerphone: true,
viewMode: ViewMode.grid,
);
}
@@ -258,8 +262,8 @@ class CallNotifier extends _$CallNotifier {
duration: Duration(
milliseconds:
(DateTime.now().millisecondsSinceEpoch -
(ongoingCall?.createdAt.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch)),
(ongoingCall?.createdAt.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch)),
),
);
});
@@ -418,6 +422,14 @@ class CallNotifier extends _$CallNotifier {
return participantsVolumes[live.remoteParticipant.sid] ?? 1;
}
void toggleViewMode() {
state = state.copyWith(
viewMode: state.viewMode == ViewMode.grid
? ViewMode.stage
: ViewMode.grid,
);
}
void dispose() {
state = state.copyWith(
error: null,

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$CallState implements DiagnosticableTreeMixin {
bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; bool get isSpeakerphone; Duration get duration; String? get error;
bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; bool get isSpeakerphone; Duration get duration; ViewMode get viewMode; String? get error;
/// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -26,21 +26,21 @@ $CallStateCopyWith<CallState> get copyWith => _$CallStateCopyWithImpl<CallState>
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'CallState'))
..add(DiagnosticsProperty('isConnected', isConnected))..add(DiagnosticsProperty('isMicrophoneEnabled', isMicrophoneEnabled))..add(DiagnosticsProperty('isCameraEnabled', isCameraEnabled))..add(DiagnosticsProperty('isScreenSharing', isScreenSharing))..add(DiagnosticsProperty('isSpeakerphone', isSpeakerphone))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('error', error));
..add(DiagnosticsProperty('isConnected', isConnected))..add(DiagnosticsProperty('isMicrophoneEnabled', isMicrophoneEnabled))..add(DiagnosticsProperty('isCameraEnabled', isCameraEnabled))..add(DiagnosticsProperty('isScreenSharing', isScreenSharing))..add(DiagnosticsProperty('isSpeakerphone', isSpeakerphone))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('viewMode', viewMode))..add(DiagnosticsProperty('error', error));
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.isSpeakerphone, isSpeakerphone) || other.isSpeakerphone == isSpeakerphone)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error));
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.isSpeakerphone, isSpeakerphone) || other.isSpeakerphone == isSpeakerphone)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.viewMode, viewMode) || other.viewMode == viewMode)&&(identical(other.error, error) || other.error == error));
}
@override
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,error);
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,viewMode,error);
@override
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, error: $error)';
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, viewMode: $viewMode, error: $error)';
}
@@ -51,7 +51,7 @@ abstract mixin class $CallStateCopyWith<$Res> {
factory $CallStateCopyWith(CallState value, $Res Function(CallState) _then) = _$CallStateCopyWithImpl;
@useResult
$Res call({
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, ViewMode viewMode, String? error
});
@@ -68,7 +68,7 @@ class _$CallStateCopyWithImpl<$Res>
/// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? isSpeakerphone = null,Object? duration = null,Object? error = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? isSpeakerphone = null,Object? duration = null,Object? viewMode = null,Object? error = freezed,}) {
return _then(_self.copyWith(
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable
@@ -76,7 +76,8 @@ as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCam
as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable
as bool,isSpeakerphone: null == isSpeakerphone ? _self.isSpeakerphone : isSpeakerphone // ignore: cast_nullable_to_non_nullable
as bool,duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
as Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as Duration,viewMode: null == viewMode ? _self.viewMode : viewMode // ignore: cast_nullable_to_non_nullable
as ViewMode,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as String?,
));
}
@@ -159,10 +160,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, ViewMode viewMode, String? error)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _CallState() when $default != null:
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.error);case _:
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.viewMode,_that.error);case _:
return orElse();
}
@@ -180,10 +181,10 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, ViewMode viewMode, String? error) $default,) {final _that = this;
switch (_that) {
case _CallState():
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.error);}
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.viewMode,_that.error);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -197,10 +198,10 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, ViewMode viewMode, String? error)? $default,) {final _that = this;
switch (_that) {
case _CallState() when $default != null:
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.error);case _:
return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnabled,_that.isScreenSharing,_that.isSpeakerphone,_that.duration,_that.viewMode,_that.error);case _:
return null;
}
@@ -212,7 +213,7 @@ return $default(_that.isConnected,_that.isMicrophoneEnabled,_that.isCameraEnable
class _CallState with DiagnosticableTreeMixin implements CallState {
const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, required this.isSpeakerphone, this.duration = const Duration(seconds: 0), this.error});
const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, required this.isSpeakerphone, this.duration = const Duration(seconds: 0), this.viewMode = ViewMode.grid, this.error});
@override final bool isConnected;
@@ -221,6 +222,7 @@ class _CallState with DiagnosticableTreeMixin implements CallState {
@override final bool isScreenSharing;
@override final bool isSpeakerphone;
@override@JsonKey() final Duration duration;
@override@JsonKey() final ViewMode viewMode;
@override final String? error;
/// Create a copy of CallState
@@ -234,21 +236,21 @@ _$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallSt
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'CallState'))
..add(DiagnosticsProperty('isConnected', isConnected))..add(DiagnosticsProperty('isMicrophoneEnabled', isMicrophoneEnabled))..add(DiagnosticsProperty('isCameraEnabled', isCameraEnabled))..add(DiagnosticsProperty('isScreenSharing', isScreenSharing))..add(DiagnosticsProperty('isSpeakerphone', isSpeakerphone))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('error', error));
..add(DiagnosticsProperty('isConnected', isConnected))..add(DiagnosticsProperty('isMicrophoneEnabled', isMicrophoneEnabled))..add(DiagnosticsProperty('isCameraEnabled', isCameraEnabled))..add(DiagnosticsProperty('isScreenSharing', isScreenSharing))..add(DiagnosticsProperty('isSpeakerphone', isSpeakerphone))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('viewMode', viewMode))..add(DiagnosticsProperty('error', error));
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.isSpeakerphone, isSpeakerphone) || other.isSpeakerphone == isSpeakerphone)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.isSpeakerphone, isSpeakerphone) || other.isSpeakerphone == isSpeakerphone)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.viewMode, viewMode) || other.viewMode == viewMode)&&(identical(other.error, error) || other.error == error));
}
@override
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,error);
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,isSpeakerphone,duration,viewMode,error);
@override
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, error: $error)';
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, isSpeakerphone: $isSpeakerphone, duration: $duration, viewMode: $viewMode, error: $error)';
}
@@ -259,7 +261,7 @@ abstract mixin class _$CallStateCopyWith<$Res> implements $CallStateCopyWith<$Re
factory _$CallStateCopyWith(_CallState value, $Res Function(_CallState) _then) = __$CallStateCopyWithImpl;
@override @useResult
$Res call({
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, String? error
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, bool isSpeakerphone, Duration duration, ViewMode viewMode, String? error
});
@@ -276,7 +278,7 @@ class __$CallStateCopyWithImpl<$Res>
/// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? isSpeakerphone = null,Object? duration = null,Object? error = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? isSpeakerphone = null,Object? duration = null,Object? viewMode = null,Object? error = freezed,}) {
return _then(_CallState(
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable
@@ -284,7 +286,8 @@ as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCam
as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable
as bool,isSpeakerphone: null == isSpeakerphone ? _self.isSpeakerphone : isSpeakerphone // ignore: cast_nullable_to_non_nullable
as bool,duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
as Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as Duration,viewMode: null == viewMode ? _self.viewMode : viewMode // ignore: cast_nullable_to_non_nullable
as ViewMode,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as String?,
));
}

View File

@@ -41,7 +41,7 @@ final class CallNotifierProvider
}
}
String _$callNotifierHash() => r'ef4e3e9c9d411cf9dce1ceb456a3b866b2c87db3';
String _$callNotifierHash() => r'40bd884d3918b8e817329589c921774ab3c62ea2';
abstract class _$CallNotifier extends $Notifier<CallState> {
CallState build();

View File

@@ -4,6 +4,7 @@ import 'package:island/database/drift_db.dart';
import 'package:island/models/account.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/file.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/database.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
@@ -46,99 +47,14 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier {
try {
final localRoomsData = await db.select(db.chatRooms).get();
final localRealmsData = await db.select(db.realms).get();
if (localRoomsData.isNotEmpty) {
final localRooms = await Future.wait(
localRoomsData.map((row) async {
final membersRows =
await (db.select(db.chatMembers)
..where((m) => m.chatRoomId.equals(row.id))).get();
final members =
membersRows.map((mRow) {
final account = SnAccount.fromJson(mRow.account);
return SnChatMember(
id: mRow.id,
chatRoomId: mRow.chatRoomId,
accountId: mRow.accountId,
account: account,
nick: mRow.nick,
notify: mRow.notify,
joinedAt: mRow.joinedAt,
breakUntil: mRow.breakUntil,
timeoutUntil: mRow.timeoutUntil,
status: null,
createdAt: mRow.createdAt,
updatedAt: mRow.updatedAt,
deletedAt: mRow.deletedAt,
chatRoom: null,
);
}).toList();
return SnChatRoom(
id: row.id,
name: row.name,
description: row.description,
type: row.type,
isPublic: row.isPublic!,
isCommunity: row.isCommunity!,
picture:
row.picture != null
? SnCloudFile.fromJson(row.picture!)
: null,
background:
row.background != null
? SnCloudFile.fromJson(row.background!)
: null,
realmId: row.realmId,
accountId: row.accountId,
realm: null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
deletedAt: row.deletedAt,
members: members,
);
}),
);
// Background sync
Future(() async {
try {
final client = ref.read(apiClientProvider);
final resp = await client.get('/sphere/chat');
final remoteRooms =
resp.data
.map((e) => SnChatRoom.fromJson(e))
.cast<SnChatRoom>()
.toList();
await db.saveChatRooms(remoteRooms, override: true);
// Update state with fresh data
state = AsyncData(await _buildRoomsFromDb(db));
} catch (_) {}
}).ignore();
return localRooms;
}
} catch (_) {}
// Fallback to API
final client = ref.watch(apiClientProvider);
final resp = await client.get('/sphere/chat');
final rooms =
resp.data
.map((e) => SnChatRoom.fromJson(e))
.cast<SnChatRoom>()
.toList();
await db.saveChatRooms(rooms, override: true);
return rooms;
}
Future<List<SnChatRoom>> _buildRoomsFromDb(AppDatabase db) async {
final localRoomsData = await db.select(db.chatRooms).get();
return Future.wait(
localRoomsData.map((row) async {
final membersRows =
await (db.select(db.chatMembers)
..where((m) => m.chatRoomId.equals(row.id))).get();
final members =
membersRows.map((mRow) {
final membersRows = await (db.select(
db.chatMembers,
)..where((m) => m.chatRoomId.equals(row.id))).get();
final members = membersRows.map((mRow) {
final account = SnAccount.fromJson(mRow.account);
return SnChatMember(
id: mRow.id,
@@ -157,6 +73,148 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier {
chatRoom: null,
);
}).toList();
return SnChatRoom(
id: row.id,
name: row.name,
description: row.description,
type: row.type,
isPublic: row.isPublic!,
isCommunity: row.isCommunity!,
picture: row.picture != null
? SnCloudFile.fromJson(row.picture!)
: null,
background: row.background != null
? SnCloudFile.fromJson(row.background!)
: null,
realmId: row.realmId,
accountId: row.accountId,
realm: localRealmsData
.where((e) => e.id == row.realmId)
.map((e) => _buildRealmFromTableEntry(e))
.firstOrNull,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
deletedAt: row.deletedAt,
members: members,
isPinned: row.isPinned ?? false,
);
}),
);
// Background sync
Future(() async {
try {
final client = ref.read(apiClientProvider);
final resp = await client.get('/sphere/chat');
final remoteRooms = resp.data
.map((e) => SnChatRoom.fromJson(e))
.cast<SnChatRoom>()
.toList();
await db.saveChatRooms(remoteRooms, override: true);
// Update state with fresh data
state = AsyncData(await _buildRoomsFromDb(db));
} catch (_) {}
}).ignore();
return localRooms;
}
} catch (_) {}
// Fallback to API
final client = ref.watch(apiClientProvider);
final resp = await client.get('/sphere/chat');
final rooms = resp.data
.map((e) => SnChatRoom.fromJson(e))
.cast<SnChatRoom>()
.toList();
await db.saveChatRooms(rooms, override: true);
return rooms;
}
SnRealm _buildRealmFromTableEntry(Realm localRealm) {
return SnRealm(
id: localRealm.id,
slug: localRealm.slug,
name: localRealm.name ?? localRealm.slug,
description: localRealm.description ?? '',
verifiedAs: localRealm.verifiedAs,
verifiedAt: localRealm.verifiedAt,
isCommunity: localRealm.isCommunity,
isPublic: localRealm.isPublic,
picture: localRealm.picture != null
? SnCloudFile.fromJson(localRealm.picture!)
: null,
background: localRealm.background != null
? SnCloudFile.fromJson(localRealm.background!)
: null,
accountId: localRealm.accountId ?? '',
createdAt: localRealm.createdAt,
updatedAt: localRealm.updatedAt,
deletedAt: localRealm.deletedAt,
);
}
Future<List<SnChatRoom>> _buildRoomsFromDb(AppDatabase db) async {
final localRoomsData = await db.select(db.chatRooms).get();
return Future.wait(
localRoomsData.map((row) async {
final membersRows = await (db.select(
db.chatMembers,
)..where((m) => m.chatRoomId.equals(row.id))).get();
final members = membersRows.map((mRow) {
final account = SnAccount.fromJson(mRow.account);
return SnChatMember(
id: mRow.id,
chatRoomId: mRow.chatRoomId,
accountId: mRow.accountId,
account: account,
nick: mRow.nick,
notify: mRow.notify,
joinedAt: mRow.joinedAt,
breakUntil: mRow.breakUntil,
timeoutUntil: mRow.timeoutUntil,
status: null,
createdAt: mRow.createdAt,
updatedAt: mRow.updatedAt,
deletedAt: mRow.deletedAt,
chatRoom: null,
);
}).toList();
// Load realm if it exists
SnRealm? realm;
if (row.realmId != null) {
try {
final realmRow = await (db.select(
db.realms,
)..where((r) => r.id.equals(row.realmId!))).getSingleOrNull();
if (realmRow != null) {
realm = SnRealm(
id: realmRow.id,
slug: '', // Not stored in DB
name: realmRow.name ?? '',
description: realmRow.description ?? '',
verifiedAs: null, // Not stored in DB
verifiedAt: null, // Not stored in DB
isCommunity: false, // Not stored in DB
isPublic: true, // Not stored in DB
picture: realmRow.picture != null
? SnCloudFile.fromJson(realmRow.picture!)
: null,
background: realmRow.background != null
? SnCloudFile.fromJson(realmRow.background!)
: null,
accountId: realmRow.accountId ?? '',
createdAt: realmRow.createdAt,
updatedAt: realmRow.updatedAt,
deletedAt: realmRow.deletedAt,
);
}
} catch (_) {
// Realm not found, keep as null
}
}
return SnChatRoom(
id: row.id,
name: row.name,
@@ -164,19 +222,20 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier {
type: row.type,
isPublic: row.isPublic!,
isCommunity: row.isCommunity!,
picture:
row.picture != null ? SnCloudFile.fromJson(row.picture!) : null,
background:
row.background != null
? SnCloudFile.fromJson(row.background!)
: null,
picture: row.picture != null
? SnCloudFile.fromJson(row.picture!)
: null,
background: row.background != null
? SnCloudFile.fromJson(row.background!)
: null,
realmId: row.realmId,
accountId: row.accountId,
realm: null,
realm: realm,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
deletedAt: row.deletedAt,
members: members,
isPinned: row.isPinned ?? false,
);
}),
);
@@ -192,35 +251,34 @@ class ChatRoomNotifier extends _$ChatRoomNotifier {
try {
// Try to get from local database first
final localRoomData =
await (db.select(db.chatRooms)
..where((r) => r.id.equals(identifier))).getSingleOrNull();
final localRoomData = await (db.select(
db.chatRooms,
)..where((r) => r.id.equals(identifier))).getSingleOrNull();
if (localRoomData != null) {
// Fetch members for this room
final membersRows =
await (db.select(db.chatMembers)
..where((m) => m.chatRoomId.equals(localRoomData.id))).get();
final members =
membersRows.map((mRow) {
final account = SnAccount.fromJson(mRow.account);
return SnChatMember(
id: mRow.id,
chatRoomId: mRow.chatRoomId,
accountId: mRow.accountId,
account: account,
nick: mRow.nick,
notify: mRow.notify,
joinedAt: mRow.joinedAt,
breakUntil: mRow.breakUntil,
timeoutUntil: mRow.timeoutUntil,
status: null,
createdAt: mRow.createdAt,
updatedAt: mRow.updatedAt,
deletedAt: mRow.deletedAt,
chatRoom: null,
);
}).toList();
final membersRows = await (db.select(
db.chatMembers,
)..where((m) => m.chatRoomId.equals(localRoomData.id))).get();
final members = membersRows.map((mRow) {
final account = SnAccount.fromJson(mRow.account);
return SnChatMember(
id: mRow.id,
chatRoomId: mRow.chatRoomId,
accountId: mRow.accountId,
account: account,
nick: mRow.nick,
notify: mRow.notify,
joinedAt: mRow.joinedAt,
breakUntil: mRow.breakUntil,
timeoutUntil: mRow.timeoutUntil,
status: null,
createdAt: mRow.createdAt,
updatedAt: mRow.updatedAt,
deletedAt: mRow.deletedAt,
chatRoom: null,
);
}).toList();
final localRoom = SnChatRoom(
id: localRoomData.id,
@@ -229,14 +287,12 @@ class ChatRoomNotifier extends _$ChatRoomNotifier {
type: localRoomData.type,
isPublic: localRoomData.isPublic!,
isCommunity: localRoomData.isCommunity!,
picture:
localRoomData.picture != null
? SnCloudFile.fromJson(localRoomData.picture!)
: null,
background:
localRoomData.background != null
? SnCloudFile.fromJson(localRoomData.background!)
: null,
picture: localRoomData.picture != null
? SnCloudFile.fromJson(localRoomData.picture!)
: null,
background: localRoomData.background != null
? SnCloudFile.fromJson(localRoomData.background!)
: null,
realmId: localRoomData.realmId,
accountId: localRoomData.accountId,
realm: null,

View File

@@ -34,7 +34,7 @@ final class ChatRoomJoinedNotifierProvider
}
String _$chatRoomJoinedNotifierHash() =>
r'c8092225ba0d9c08b2b5bca6f800f1877303b4ff';
r'e69955be56ef2c04a8062a8a65925e0a23bfcbaa';
abstract class _$ChatRoomJoinedNotifier
extends $AsyncNotifier<List<SnChatRoom>> {

View File

@@ -93,7 +93,6 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
// Set up periodic subscribe timer (every 5 minutes)
_periodicSubscribeTimer = Timer.periodic(const Duration(minutes: 5), (_) {
final wsState = ref.read(websocketStateProvider.notifier);
wsState.sendMessage(
jsonEncode(
WebSocketPacket(

View File

@@ -59,7 +59,7 @@ final class ChatSubscribeNotifierProvider
}
String _$chatSubscribeNotifierHash() =>
r'2b9fae96eb1f96a514a074985e5efa1c13d10aa4';
r'1aa164429aaab1628b5edbae11e33b0860abdcdc';
final class ChatSubscribeNotifierFamily extends $Family
with

View File

@@ -24,6 +24,8 @@ import "package:island/screens/account/profile.dart";
part 'messages_notifier.g.dart';
const Set<String> kSilentMessageTypes = {'messages.update.links'};
@riverpod
class MessagesNotifier extends _$MessagesNotifier {
late Dio _apiClient;
@@ -48,6 +50,9 @@ class MessagesNotifier extends _$MessagesNotifier {
late Future<SnAccount?> Function(String) _fetchAccount;
// Disposal handling
bool _disposed = false;
@override
FutureOr<List<LocalChatMessage>> build(String roomId) async {
_apiClient = ref.watch(apiClientProvider);
@@ -76,10 +81,17 @@ class MessagesNotifier extends _$MessagesNotifier {
talker.log('MessagesNotifier built for room $roomId');
// Set up disposal handling
ref.onDispose(() {
_disposed = true;
talker.log('MessagesNotifier disposed for room $roomId');
});
// Only setup sync and lifecycle listeners if user is a member
if (identity != null) {
ref.listen(appLifecycleStateProvider, (_, next) {
next.whenData((state) {
if (_disposed) return; // Check disposal before accessing ref
if (state == AppLifecycleState.paused) {
_lastPauseTime = DateTime.now();
talker.log('App paused, recording time');
@@ -88,7 +100,9 @@ class MessagesNotifier extends _$MessagesNotifier {
final diff = DateTime.now().difference(_lastPauseTime!);
if (diff > const Duration(minutes: 1)) {
talker.log('App resumed after >1 min, syncing messages');
syncMessages();
if (!_disposed) {
syncMessages(); // Check disposal before calling syncMessages
}
} else {
talker.log('App resumed within 1 min, skipping sync');
}
@@ -167,15 +181,15 @@ class MessagesNotifier extends _$MessagesNotifier {
List<LocalChatMessage> filteredMessages = dbMessages;
if (withLinks == true) {
filteredMessages =
filteredMessages.where((msg) => _hasLink(msg)).toList();
filteredMessages = filteredMessages
.where((msg) => _hasLink(msg))
.toList();
}
if (withAttachments == true) {
filteredMessages =
filteredMessages
.where((msg) => msg.toRemoteMessage().attachments.isNotEmpty)
.toList();
filteredMessages = filteredMessages
.where((msg) => msg.toRemoteMessage().attachments.isNotEmpty)
.toList();
}
final dbLocalMessages = filteredMessages;
@@ -190,8 +204,9 @@ class MessagesNotifier extends _$MessagesNotifier {
}
if (offset == 0) {
final pendingForRoom =
_pendingMessages.values.where((msg) => msg.roomId == roomId).toList();
final pendingForRoom = _pendingMessages.values
.where((msg) => msg.roomId == roomId)
.toList();
final allMessages = [...pendingForRoom, ...uniqueMessages];
_sortMessages(allMessages); // Use the helper function
@@ -239,8 +254,9 @@ class MessagesNotifier extends _$MessagesNotifier {
}
if (offset == 0) {
final pendingForRoom =
_pendingMessages.values.where((msg) => msg.roomId == roomId).toList();
final pendingForRoom = _pendingMessages.values
.where((msg) => msg.roomId == roomId)
.toList();
final allMessages = [...pendingForRoom, ...uniqueMessages];
_sortMessages(allMessages);
@@ -284,14 +300,13 @@ class MessagesNotifier extends _$MessagesNotifier {
final List<dynamic> data = response.data;
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
final messages =
data.map((json) {
final remoteMessage = SnChatMessage.fromJson(json);
return LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
}).toList();
final messages = data.map((json) {
final remoteMessage = SnChatMessage.fromJson(json);
return LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
}).toList();
for (final message in messages) {
await _database.saveMessageWithSender(message);
@@ -319,20 +334,24 @@ class MessagesNotifier extends _$MessagesNotifier {
_allRemoteMessagesFetched = false;
talker.log('Starting message sync');
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true));
// Use Future.microtask to set syncing state, but check disposal to avoid errors
Future.microtask(() {
if (!_disposed) {
ref.read(chatSyncingProvider.notifier).set(true);
}
});
try {
final dbMessages = await _database.getMessagesForRoom(
_room.id,
offset: 0,
limit: 1,
);
final lastMessage =
dbMessages.isEmpty
? null
: await _database.companionToMessage(
dbMessages.first,
fetchAccount: _fetchAccount,
);
final lastMessage = dbMessages.isEmpty
? null
: await _database.companionToMessage(
dbMessages.first,
fetchAccount: _fetchAccount,
);
if (lastMessage == null) {
talker.log('No local messages, fetching from network');
@@ -347,8 +366,10 @@ class MessagesNotifier extends _$MessagesNotifier {
// Sync with pagination support using timestamp-based cursor
int? totalMessages;
int syncedCount = 0;
int lastSyncTimestamp =
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch;
int lastSyncTimestamp = lastMessage
.toRemoteMessage()
.updatedAt
.millisecondsSinceEpoch;
do {
final resp = await _apiClient.post(
@@ -395,6 +416,7 @@ class MessagesNotifier extends _$MessagesNotifier {
showErrorAlert(err);
} finally {
talker.log('Finished message sync');
// Always reset global syncing state, regardless of disposal
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(false));
_isSyncing = false;
}
@@ -492,7 +514,9 @@ class MessagesNotifier extends _$MessagesNotifier {
if (!_hasMore || state is AsyncLoading) return;
talker.log('Loading more messages');
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true));
if (!_disposed) {
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true));
}
try {
final currentMessages = state.value ?? [];
final offset = currentMessages.length;
@@ -515,6 +539,7 @@ class MessagesNotifier extends _$MessagesNotifier {
);
showErrorAlert(err);
} finally {
// Always reset global syncing state, regardless of disposal
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(false));
}
}
@@ -559,18 +584,17 @@ class MessagesNotifier extends _$MessagesNotifier {
try {
var cloudAttachments = List.empty(growable: true);
for (var idx = 0; idx < attachments.length; idx++) {
final cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: attachments[idx],
onProgress: (progress, _) {
_fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0;
onProgress?.call(
localMessage.id,
_fileUploadProgress[localMessage.id] ?? {},
);
},
).future;
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: attachments[idx],
onProgress: (progress, _) {
_fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0;
onProgress?.call(
localMessage.id,
_fileUploadProgress[localMessage.id] ?? {},
);
},
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
@@ -606,22 +630,20 @@ class MessagesNotifier extends _$MessagesNotifier {
final currentMessages = state.value ?? [];
if (editingTo != null) {
final newMessages =
currentMessages
.where((m) => m.id != localMessage.id) // remove pending message
.map(
(m) => m.id == editingTo.id ? updatedMessage : m,
) // update original message
.toList();
final newMessages = currentMessages
.where((m) => m.id != localMessage.id) // remove pending message
.map(
(m) => m.id == editingTo.id ? updatedMessage : m,
) // update original message
.toList();
state = AsyncValue.data(newMessages);
} else {
final newMessages =
currentMessages.map((m) {
if (m.id == localMessage.id) {
return updatedMessage;
}
return m;
}).toList();
final newMessages = currentMessages.map((m) {
if (m.id == localMessage.id) {
return updatedMessage;
}
return m;
}).toList();
state = AsyncValue.data(newMessages);
}
talker.log('Message with nonce $nonce sent successfully');
@@ -638,13 +660,12 @@ class MessagesNotifier extends _$MessagesNotifier {
localMessage.id,
MessageStatus.failed,
);
final newMessages =
(state.value ?? []).map((m) {
if (m.id == localMessage.id) {
return m..status = MessageStatus.failed;
}
return m;
}).toList();
final newMessages = (state.value ?? []).map((m) {
if (m.id == localMessage.id) {
return m..status = MessageStatus.failed;
}
return m;
}).toList();
state = AsyncValue.data(newMessages);
showErrorAlert(e);
}
@@ -686,13 +707,12 @@ class MessagesNotifier extends _$MessagesNotifier {
await _database.deleteMessage(pendingMessageId);
await _database.saveMessageWithSender(updatedMessage);
final newMessages =
(state.value ?? []).map((m) {
if (m.id == pendingMessageId) {
return updatedMessage;
}
return m;
}).toList();
final newMessages = (state.value ?? []).map((m) {
if (m.id == pendingMessageId) {
return updatedMessage;
}
return m;
}).toList();
state = AsyncValue.data(newMessages);
} catch (e, stackTrace) {
talker.log(
@@ -707,13 +727,12 @@ class MessagesNotifier extends _$MessagesNotifier {
pendingMessageId,
MessageStatus.failed,
);
final newMessages =
(state.value ?? []).map((m) {
if (m.id == pendingMessageId) {
return m..status = MessageStatus.failed;
}
return m;
}).toList();
final newMessages = (state.value ?? []).map((m) {
if (m.id == pendingMessageId) {
return m..status = MessageStatus.failed;
}
return m;
}).toList();
state = AsyncValue.data(_sortMessages(newMessages));
showErrorAlert(e);
}
@@ -730,6 +749,8 @@ class MessagesNotifier extends _$MessagesNotifier {
talker.log('Received new message ${remoteMessage.id}');
final isSilentMessage = kSilentMessageTypes.contains(remoteMessage.type);
final localMessage = LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
@@ -741,23 +762,25 @@ class MessagesNotifier extends _$MessagesNotifier {
);
}
await _database.saveMessageWithSender(localMessage);
if (!isSilentMessage) {
await _database.saveMessageWithSender(localMessage);
final currentMessages = state.value ?? [];
final existingIndex = currentMessages.indexWhere(
(m) =>
m.id == localMessage.id ||
(localMessage.nonce != null && m.nonce == localMessage.nonce),
);
if (existingIndex >= 0) {
final newList = [...currentMessages];
newList[existingIndex] = localMessage;
state = AsyncValue.data(_sortMessages(newList));
} else {
state = AsyncValue.data(
_sortMessages([localMessage, ...currentMessages]),
final currentMessages = state.value ?? [];
final existingIndex = currentMessages.indexWhere(
(m) =>
m.id == localMessage.id ||
(localMessage.nonce != null && m.nonce == localMessage.nonce),
);
if (existingIndex >= 0) {
final newList = [...currentMessages];
newList[existingIndex] = localMessage;
state = AsyncValue.data(_sortMessages(newList));
} else {
state = AsyncValue.data(
_sortMessages([localMessage, ...currentMessages]),
);
}
}
switch (remoteMessage.type) {
@@ -783,15 +806,44 @@ class MessagesNotifier extends _$MessagesNotifier {
talker.log('Received message update ${remoteMessage.id}');
final targetId = remoteMessage.meta['message_id'] ?? remoteMessage.id;
final updatedMessage = LocalChatMessage.fromRemoteMessage(
remoteMessage.copyWith(
id: targetId,
meta: Map.of(remoteMessage.meta)..remove('message_id'),
type: 'text',
LocalChatMessage updatedMessage;
if (remoteMessage.type == 'messages.update.links') {
// For link updates, merge meta with existing message instead of creating new one
final existingMessage = await fetchMessageById(targetId);
if (existingMessage == null) {
talker.log('Cannot update links for non-existent message $targetId');
return;
}
final existingRemote = existingMessage.toRemoteMessage();
final mergedMeta = Map<String, dynamic>.of(existingRemote.meta);
mergedMeta.addAll(remoteMessage.meta);
mergedMeta.remove('message_id'); // Remove the target message ID from meta
final updatedRemote = existingRemote.copyWith(
meta: mergedMeta,
editedAt: remoteMessage.createdAt,
),
MessageStatus.sent,
);
);
updatedMessage = LocalChatMessage.fromRemoteMessage(
updatedRemote,
existingMessage.status,
);
} else {
// For regular updates, create new message as before
updatedMessage = LocalChatMessage.fromRemoteMessage(
remoteMessage.copyWith(
id: targetId,
meta: Map.of(remoteMessage.meta)..remove('message_id'),
type: 'text',
editedAt: remoteMessage.createdAt,
),
MessageStatus.sent,
);
}
await _database.updateMessage(_database.messageToCompanion(updatedMessage));
final currentMessages = state.value ?? [];
@@ -865,8 +917,9 @@ class MessagesNotifier extends _$MessagesNotifier {
await _database.deleteMessage(messageId);
final currentMessages = state.value ?? [];
final newMessages =
currentMessages.where((m) => m.id != messageId).toList();
final newMessages = currentMessages
.where((m) => m.id != messageId)
.toList();
state = AsyncValue.data(newMessages);
return;
}
@@ -969,9 +1022,9 @@ class MessagesNotifier extends _$MessagesNotifier {
Future<LocalChatMessage?> fetchMessageById(String messageId) async {
talker.log('Fetching message by id $messageId');
try {
final localMessage =
await (_database.select(_database.chatMessages)
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
final localMessage = await (_database.select(
_database.chatMessages,
)..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
if (localMessage != null) {
return _database.companionToMessage(
localMessage,
@@ -1005,7 +1058,9 @@ class MessagesNotifier extends _$MessagesNotifier {
_isJumping = true;
// Clear flashing messages when starting a new jump
ref.read(flashingMessagesProvider.notifier).state = {};
if (!_disposed) {
ref.read(flashingMessagesProvider.notifier).state = {};
}
try {
talker.log('Fetching message $messageId');
@@ -1047,8 +1102,9 @@ class MessagesNotifier extends _$MessagesNotifier {
// Calculate offset to position target message in the middle of the loaded chunk
const chunkSize = 100; // Load 100 messages around the target
final offset =
(newerCount - chunkSize ~/ 2).clamp(0, double.infinity).toInt();
final offset = (newerCount - chunkSize ~/ 2)
.clamp(0, double.infinity)
.toInt();
talker.log(
'Calculated offset $offset for target message (newer: $newerCount, chunk: $chunkSize)',
);
@@ -1060,8 +1116,9 @@ class MessagesNotifier extends _$MessagesNotifier {
// Check if loaded messages are already in current state
final currentIds = currentMessages.map((m) => m.id).toSet();
final newMessages =
loadedMessages.where((m) => !currentIds.contains(m.id)).toList();
final newMessages = loadedMessages
.where((m) => !currentIds.contains(m.id))
.toList();
talker.log(
'Loaded ${loadedMessages.length} messages, ${newMessages.length} are new',
);

View File

@@ -50,7 +50,7 @@ final class MessagesNotifierProvider
}
}
String _$messagesNotifierHash() => r'2f3f19cb99357184e82d66e74a31863fcfc48856';
String _$messagesNotifierHash() => r'c7e2cd7f5b8673af88f5076814393dbfbd0d43c5';
final class MessagesNotifierFamily extends $Family
with

View File

@@ -22,10 +22,9 @@ const kAppColorSchemeStoreKey = 'app_color_scheme';
const kAppCustomColorsStoreKey = 'app_custom_colors';
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 kAppFestivalFeatures = 'app_feastival_features';
const kAppWindowSize = 'app_window_size';
const kAppWindowOpacity = 'app_window_opacity';
const kAppCardTransparent = 'app_card_transparent';
@@ -33,31 +32,20 @@ const kAppEnterToSend = 'app_enter_to_send';
const kAppDefaultPoolId = 'app_default_pool_id';
const kAppMessageDisplayStyle = 'app_message_display_style';
const kAppThemeMode = 'app_theme_mode';
const kMaterialYouToggleStoreKey = 'app_theme_material_you';
const kAppDisableAnimation = 'app_disable_animation';
const kAppFabPosition = 'app_fab_position';
const kAppGroupedChatList = 'app_grouped_chat_list';
const kFeaturedPostsCollapsedId =
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
const kAppFirstLaunchAt = 'app_first_launch_at';
const kAppAskedReview = 'app_asked_review';
const kAppDashSearchEngine = 'app_dash_search_engine';
const kAppDefaultScreen = 'app_default_screen';
const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none,
'settingsImageQualityLow': FilterQuality.low,
'settingsImageQualityMedium': FilterQuality.medium,
'settingsImageQualityHigh': FilterQuality.high,
};
// Will be overrided by the ProviderScope
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError();
});
final imageQualityProvider = Provider<FilterQuality>((ref) {
final prefs = ref.watch(sharedPreferencesProvider);
return kImageQualityLevel.values.elementAtOrNull(
prefs.getInt('app_image_quality') ?? 3,
) ??
FilterQuality.high;
});
final serverUrlProvider = Provider<String>((ref) {
final prefs = ref.watch(sharedPreferencesProvider);
return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
@@ -81,13 +69,13 @@ sealed class ThemeColors with _$ThemeColors {
@freezed
sealed class AppSettings with _$AppSettings {
const factory AppSettings({
required bool autoTranslate,
required bool dataSavingMode,
required bool soundEffects,
required bool aprilFoolFeatures,
required bool festivalFeatures,
required bool enterToSend,
required bool appBarTransparent,
required bool showBackgroundImage,
required bool notifyWithHaptic,
required String? customFonts,
required int? appColorScheme, // The color stored via the int type
required ThemeColors? customColors,
@@ -97,9 +85,12 @@ sealed class AppSettings with _$AppSettings {
required String? defaultPoolId,
required String messageDisplayStyle,
required String? themeMode,
required bool useMaterial3,
required bool disableAnimation,
required String fabPosition,
required bool groupedChatList,
required String? firstLaunchAt,
required bool askedReview,
required String? dashSearchEngine,
required String? defaultScreen,
}) = _AppSettings;
}
@@ -109,13 +100,13 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
AppSettings build() {
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,
festivalFeatures: prefs.getBool(kAppFestivalFeatures) ?? true,
enterToSend: prefs.getBool(kAppEnterToSend) ?? true,
appBarTransparent: prefs.getBool(kAppbarTransparentStoreKey) ?? false,
showBackgroundImage: prefs.getBool(kAppShowBackgroundImage) ?? true,
notifyWithHaptic: prefs.getBool(kAppNotifyWithHaptic) ?? true,
customFonts: prefs.getString(kAppCustomFonts),
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
customColors: _getThemeColorsFromPrefs(prefs),
@@ -125,9 +116,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
defaultPoolId: prefs.getString(kAppDefaultPoolId),
messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble',
themeMode: prefs.getString(kAppThemeMode) ?? 'system',
useMaterial3: prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
disableAnimation: prefs.getBool(kAppDisableAnimation) ?? false,
fabPosition: prefs.getString(kAppFabPosition) ?? 'center',
groupedChatList: prefs.getBool(kAppGroupedChatList) ?? false,
askedReview: prefs.getBool(kAppAskedReview) ?? false,
firstLaunchAt: prefs.getString(kAppFirstLaunchAt),
dashSearchEngine: prefs.getString(kAppDashSearchEngine),
defaultScreen: prefs.getString(kAppDefaultScreen),
);
}
@@ -170,12 +164,6 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
state = state.copyWith(defaultPoolId: value);
}
void setAutoTranslate(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppAutoTranslate, value);
state = state.copyWith(autoTranslate: value);
}
void setDataSavingMode(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppDataSavingMode, value);
@@ -188,10 +176,10 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
state = state.copyWith(soundEffects: value);
}
void setAprilFoolFeatures(bool value) {
void setFeativalFeatures(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppAprilFoolFeatures, value);
state = state.copyWith(aprilFoolFeatures: value);
prefs.setBool(kAppFestivalFeatures, value);
state = state.copyWith(festivalFeatures: value);
}
void setEnterToSend(bool value) {
@@ -212,12 +200,24 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
state = state.copyWith(showBackgroundImage: value);
}
void setNotifyWithHaptic(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppNotifyWithHaptic, value);
state = state.copyWith(notifyWithHaptic: value);
}
void setCustomFonts(String? value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setString(kAppCustomFonts, value ?? '');
state = state.copyWith(customFonts: value);
}
void setDefaultScreen(String? value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setString(kAppDefaultScreen, value ?? 'dashboard');
state = state.copyWith(defaultScreen: value);
}
void setAppColorScheme(int? value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setInt(kAppColorSchemeStoreKey, value ?? 0);
@@ -263,12 +263,6 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
state = state.copyWith(cardTransparency: value);
}
void setUseMaterial3(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kMaterialYouToggleStoreKey, value);
state = state.copyWith(useMaterial3: value);
}
void setCustomColors(ThemeColors? value) {
final prefs = ref.read(sharedPreferencesProvider);
if (value != null) {
@@ -286,10 +280,36 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
state = state.copyWith(disableAnimation: value);
}
void setFabPosition(String value) {
void setGroupedChatList(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setString(kAppFabPosition, value);
state = state.copyWith(fabPosition: value);
prefs.setBool(kAppGroupedChatList, value);
state = state.copyWith(groupedChatList: value);
}
void setFirstLaunchAt(String? value) {
final prefs = ref.read(sharedPreferencesProvider);
if (value != null) {
prefs.setString(kAppFirstLaunchAt, value);
} else {
prefs.remove(kAppFirstLaunchAt);
}
state = state.copyWith(firstLaunchAt: value);
}
void setAskedReview(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppAskedReview, value);
state = state.copyWith(askedReview: value);
}
void setDashSearchEngine(String? value) {
final prefs = ref.read(sharedPreferencesProvider);
if (value != null) {
prefs.setString(kAppDashSearchEngine, value);
} else {
prefs.remove(kAppDashSearchEngine);
}
state = state.copyWith(dashSearchEngine: value);
}
}

View File

@@ -286,11 +286,11 @@ as int?,
/// @nodoc
mixin _$AppSettings {
bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type
bool get dataSavingMode; bool get soundEffects; bool get festivalFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; bool get notifyWithHaptic; String? get customFonts; int? get appColorScheme;// The color stored via the int type
ThemeColors? get customColors; Size? get windowSize;// The window size for desktop platforms
double get windowOpacity;// The window opacity for desktop platforms
double get cardTransparency;// The card background opacity
String? get defaultPoolId; String get messageDisplayStyle; String? get themeMode; bool get useMaterial3; bool get disableAnimation; String get fabPosition;
String? get defaultPoolId; String get messageDisplayStyle; String? get themeMode; bool get disableAnimation; bool get groupedChatList; String? get firstLaunchAt; bool get askedReview; String? get dashSearchEngine; String? get defaultScreen;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -301,16 +301,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.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.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.useMaterial3, useMaterial3) || other.useMaterial3 == useMaterial3)&&(identical(other.disableAnimation, disableAnimation) || other.disableAnimation == disableAnimation)&&(identical(other.fabPosition, fabPosition) || other.fabPosition == fabPosition));
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.festivalFeatures, festivalFeatures) || other.festivalFeatures == festivalFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.notifyWithHaptic, notifyWithHaptic) || other.notifyWithHaptic == notifyWithHaptic)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.disableAnimation, disableAnimation) || other.disableAnimation == disableAnimation)&&(identical(other.groupedChatList, groupedChatList) || other.groupedChatList == groupedChatList)&&(identical(other.firstLaunchAt, firstLaunchAt) || other.firstLaunchAt == firstLaunchAt)&&(identical(other.askedReview, askedReview) || other.askedReview == askedReview)&&(identical(other.dashSearchEngine, dashSearchEngine) || other.dashSearchEngine == dashSearchEngine)&&(identical(other.defaultScreen, defaultScreen) || other.defaultScreen == defaultScreen));
}
@override
int get hashCode => Object.hashAll([runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3,disableAnimation,fabPosition]);
int get hashCode => Object.hashAll([runtimeType,dataSavingMode,soundEffects,festivalFeatures,enterToSend,appBarTransparent,showBackgroundImage,notifyWithHaptic,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,disableAnimation,groupedChatList,firstLaunchAt,askedReview,dashSearchEngine,defaultScreen]);
@override
String toString() {
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3, disableAnimation: $disableAnimation, fabPosition: $fabPosition)';
return 'AppSettings(dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, festivalFeatures: $festivalFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, notifyWithHaptic: $notifyWithHaptic, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, disableAnimation: $disableAnimation, groupedChatList: $groupedChatList, firstLaunchAt: $firstLaunchAt, askedReview: $askedReview, dashSearchEngine: $dashSearchEngine, defaultScreen: $defaultScreen)';
}
@@ -321,7 +321,7 @@ abstract mixin class $AppSettingsCopyWith<$Res> {
factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
@useResult
$Res call({
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation, String fabPosition
bool dataSavingMode, bool soundEffects, bool festivalFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, bool notifyWithHaptic, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool disableAnimation, bool groupedChatList, String? firstLaunchAt, bool askedReview, String? dashSearchEngine, String? defaultScreen
});
@@ -338,15 +338,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? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,Object? disableAnimation = null,Object? fabPosition = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? dataSavingMode = null,Object? soundEffects = null,Object? festivalFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? notifyWithHaptic = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? disableAnimation = null,Object? groupedChatList = null,Object? firstLaunchAt = freezed,Object? askedReview = null,Object? dashSearchEngine = freezed,Object? defaultScreen = 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
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,festivalFeatures: null == festivalFeatures ? _self.festivalFeatures : festivalFeatures // 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,notifyWithHaptic: null == notifyWithHaptic ? _self.notifyWithHaptic : notifyWithHaptic // 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?,customColors: freezed == customColors ? _self.customColors : customColors // ignore: cast_nullable_to_non_nullable
@@ -356,10 +356,13 @@ as double,cardTransparency: null == cardTransparency ? _self.cardTransparency :
as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
as String,themeMode: freezed == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable
as String?,useMaterial3: null == useMaterial3 ? _self.useMaterial3 : useMaterial3 // ignore: cast_nullable_to_non_nullable
as bool,disableAnimation: null == disableAnimation ? _self.disableAnimation : disableAnimation // ignore: cast_nullable_to_non_nullable
as bool,fabPosition: null == fabPosition ? _self.fabPosition : fabPosition // ignore: cast_nullable_to_non_nullable
as String,
as String?,disableAnimation: null == disableAnimation ? _self.disableAnimation : disableAnimation // ignore: cast_nullable_to_non_nullable
as bool,groupedChatList: null == groupedChatList ? _self.groupedChatList : groupedChatList // ignore: cast_nullable_to_non_nullable
as bool,firstLaunchAt: freezed == firstLaunchAt ? _self.firstLaunchAt : firstLaunchAt // ignore: cast_nullable_to_non_nullable
as String?,askedReview: null == askedReview ? _self.askedReview : askedReview // ignore: cast_nullable_to_non_nullable
as bool,dashSearchEngine: freezed == dashSearchEngine ? _self.dashSearchEngine : dashSearchEngine // ignore: cast_nullable_to_non_nullable
as String?,defaultScreen: freezed == defaultScreen ? _self.defaultScreen : defaultScreen // ignore: cast_nullable_to_non_nullable
as String?,
));
}
/// Create a copy of AppSettings
@@ -453,10 +456,10 @@ return $default(_that);case _:
/// }
/// ```
@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, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation, String fabPosition)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool dataSavingMode, bool soundEffects, bool festivalFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, bool notifyWithHaptic, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool disableAnimation, bool groupedChatList, String? firstLaunchAt, bool askedReview, String? dashSearchEngine, String? defaultScreen)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AppSettings() when $default != null:
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3,_that.disableAnimation,_that.fabPosition);case _:
return $default(_that.dataSavingMode,_that.soundEffects,_that.festivalFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.notifyWithHaptic,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.disableAnimation,_that.groupedChatList,_that.firstLaunchAt,_that.askedReview,_that.dashSearchEngine,_that.defaultScreen);case _:
return orElse();
}
@@ -474,10 +477,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
/// }
/// ```
@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, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation, String fabPosition) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool dataSavingMode, bool soundEffects, bool festivalFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, bool notifyWithHaptic, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool disableAnimation, bool groupedChatList, String? firstLaunchAt, bool askedReview, String? dashSearchEngine, String? defaultScreen) $default,) {final _that = this;
switch (_that) {
case _AppSettings():
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3,_that.disableAnimation,_that.fabPosition);}
return $default(_that.dataSavingMode,_that.soundEffects,_that.festivalFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.notifyWithHaptic,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.disableAnimation,_that.groupedChatList,_that.firstLaunchAt,_that.askedReview,_that.dashSearchEngine,_that.defaultScreen);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -491,10 +494,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
/// }
/// ```
@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, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation, String fabPosition)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool dataSavingMode, bool soundEffects, bool festivalFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, bool notifyWithHaptic, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool disableAnimation, bool groupedChatList, String? firstLaunchAt, bool askedReview, String? dashSearchEngine, String? defaultScreen)? $default,) {final _that = this;
switch (_that) {
case _AppSettings() when $default != null:
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3,_that.disableAnimation,_that.fabPosition);case _:
return $default(_that.dataSavingMode,_that.soundEffects,_that.festivalFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.notifyWithHaptic,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.disableAnimation,_that.groupedChatList,_that.firstLaunchAt,_that.askedReview,_that.dashSearchEngine,_that.defaultScreen);case _:
return null;
}
@@ -506,16 +509,16 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
class _AppSettings implements AppSettings {
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.customColors, required this.windowSize, required this.windowOpacity, required this.cardTransparency, required this.defaultPoolId, required this.messageDisplayStyle, required this.themeMode, required this.useMaterial3, required this.disableAnimation, required this.fabPosition});
const _AppSettings({required this.dataSavingMode, required this.soundEffects, required this.festivalFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.notifyWithHaptic, required this.customFonts, required this.appColorScheme, required this.customColors, required this.windowSize, required this.windowOpacity, required this.cardTransparency, required this.defaultPoolId, required this.messageDisplayStyle, required this.themeMode, required this.disableAnimation, required this.groupedChatList, required this.firstLaunchAt, required this.askedReview, required this.dashSearchEngine, required this.defaultScreen});
@override final bool autoTranslate;
@override final bool dataSavingMode;
@override final bool soundEffects;
@override final bool aprilFoolFeatures;
@override final bool festivalFeatures;
@override final bool enterToSend;
@override final bool appBarTransparent;
@override final bool showBackgroundImage;
@override final bool notifyWithHaptic;
@override final String? customFonts;
@override final int? appColorScheme;
// The color stored via the int type
@@ -529,9 +532,12 @@ class _AppSettings implements AppSettings {
@override final String? defaultPoolId;
@override final String messageDisplayStyle;
@override final String? themeMode;
@override final bool useMaterial3;
@override final bool disableAnimation;
@override final String fabPosition;
@override final bool groupedChatList;
@override final String? firstLaunchAt;
@override final bool askedReview;
@override final String? dashSearchEngine;
@override final String? defaultScreen;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@@ -543,16 +549,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.useMaterial3, useMaterial3) || other.useMaterial3 == useMaterial3)&&(identical(other.disableAnimation, disableAnimation) || other.disableAnimation == disableAnimation)&&(identical(other.fabPosition, fabPosition) || other.fabPosition == fabPosition));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.festivalFeatures, festivalFeatures) || other.festivalFeatures == festivalFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.notifyWithHaptic, notifyWithHaptic) || other.notifyWithHaptic == notifyWithHaptic)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.disableAnimation, disableAnimation) || other.disableAnimation == disableAnimation)&&(identical(other.groupedChatList, groupedChatList) || other.groupedChatList == groupedChatList)&&(identical(other.firstLaunchAt, firstLaunchAt) || other.firstLaunchAt == firstLaunchAt)&&(identical(other.askedReview, askedReview) || other.askedReview == askedReview)&&(identical(other.dashSearchEngine, dashSearchEngine) || other.dashSearchEngine == dashSearchEngine)&&(identical(other.defaultScreen, defaultScreen) || other.defaultScreen == defaultScreen));
}
@override
int get hashCode => Object.hashAll([runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3,disableAnimation,fabPosition]);
int get hashCode => Object.hashAll([runtimeType,dataSavingMode,soundEffects,festivalFeatures,enterToSend,appBarTransparent,showBackgroundImage,notifyWithHaptic,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,disableAnimation,groupedChatList,firstLaunchAt,askedReview,dashSearchEngine,defaultScreen]);
@override
String toString() {
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3, disableAnimation: $disableAnimation, fabPosition: $fabPosition)';
return 'AppSettings(dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, festivalFeatures: $festivalFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, notifyWithHaptic: $notifyWithHaptic, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, disableAnimation: $disableAnimation, groupedChatList: $groupedChatList, firstLaunchAt: $firstLaunchAt, askedReview: $askedReview, dashSearchEngine: $dashSearchEngine, defaultScreen: $defaultScreen)';
}
@@ -563,7 +569,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
@override @useResult
$Res call({
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation, String fabPosition
bool dataSavingMode, bool soundEffects, bool festivalFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, bool notifyWithHaptic, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool disableAnimation, bool groupedChatList, String? firstLaunchAt, bool askedReview, String? dashSearchEngine, String? defaultScreen
});
@@ -580,15 +586,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? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,Object? disableAnimation = null,Object? fabPosition = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? dataSavingMode = null,Object? soundEffects = null,Object? festivalFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? notifyWithHaptic = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? disableAnimation = null,Object? groupedChatList = null,Object? firstLaunchAt = freezed,Object? askedReview = null,Object? dashSearchEngine = freezed,Object? defaultScreen = 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
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,festivalFeatures: null == festivalFeatures ? _self.festivalFeatures : festivalFeatures // 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,notifyWithHaptic: null == notifyWithHaptic ? _self.notifyWithHaptic : notifyWithHaptic // 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?,customColors: freezed == customColors ? _self.customColors : customColors // ignore: cast_nullable_to_non_nullable
@@ -598,10 +604,13 @@ as double,cardTransparency: null == cardTransparency ? _self.cardTransparency :
as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
as String,themeMode: freezed == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable
as String?,useMaterial3: null == useMaterial3 ? _self.useMaterial3 : useMaterial3 // ignore: cast_nullable_to_non_nullable
as bool,disableAnimation: null == disableAnimation ? _self.disableAnimation : disableAnimation // ignore: cast_nullable_to_non_nullable
as bool,fabPosition: null == fabPosition ? _self.fabPosition : fabPosition // ignore: cast_nullable_to_non_nullable
as String,
as String?,disableAnimation: null == disableAnimation ? _self.disableAnimation : disableAnimation // ignore: cast_nullable_to_non_nullable
as bool,groupedChatList: null == groupedChatList ? _self.groupedChatList : groupedChatList // ignore: cast_nullable_to_non_nullable
as bool,firstLaunchAt: freezed == firstLaunchAt ? _self.firstLaunchAt : firstLaunchAt // ignore: cast_nullable_to_non_nullable
as String?,askedReview: null == askedReview ? _self.askedReview : askedReview // ignore: cast_nullable_to_non_nullable
as bool,dashSearchEngine: freezed == dashSearchEngine ? _self.dashSearchEngine : dashSearchEngine // ignore: cast_nullable_to_non_nullable
as String?,defaultScreen: freezed == defaultScreen ? _self.defaultScreen : defaultScreen // ignore: cast_nullable_to_non_nullable
as String?,
));
}

View File

@@ -65,7 +65,7 @@ final class AppSettingsNotifierProvider
}
String _$appSettingsNotifierHash() =>
r'22b695f2023e3251db3296858acd701f7211d757';
r'6592261baf8182fe78d3e58e2fd9bb53d3287736';
abstract class _$AppSettingsNotifier extends $Notifier<AppSettings> {
AppSettings build();

View File

@@ -5,6 +5,7 @@ import 'dart:io';
import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:dio/dio.dart';
import 'package:image_picker/image_picker.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -16,6 +17,36 @@ import 'package:island/talker.dart';
import 'config.dart';
part 'network.g.dart';
// Network status enum to track different states
enum NetworkStatus { online, notReady, maintenance, offline }
// Provider for network status using Riverpod v3 annotation
@riverpod
class NetworkStatusNotifier extends _$NetworkStatusNotifier {
@override
NetworkStatus build() {
return NetworkStatus.online;
}
void setOnline() {
state = NetworkStatus.online;
}
void setMaintenance() {
state = NetworkStatus.maintenance;
}
void setOffline() {
state = NetworkStatus.offline;
}
void setNotReady() {
state = NetworkStatus.notReady;
}
}
final imagePickerProvider = Provider((ref) => ImagePicker());
final userAgentProvider = FutureProvider<String>((ref) async {
@@ -80,24 +111,58 @@ final apiClientProvider = Provider<Dio>((ref) {
dio.interceptors.addAll([
InterceptorsWrapper(
onRequest: (
RequestOptions options,
RequestInterceptorHandler handler,
) async {
try {
final token = await getToken(ref.watch(tokenProvider));
if (token != null) {
options.headers['Authorization'] = 'AtField $token';
}
} catch (err) {
// ignore
}
onRequest:
(RequestOptions options, RequestInterceptorHandler handler) async {
try {
final token = await getToken(ref.watch(tokenProvider));
if (token != null) {
options.headers['Authorization'] = 'AtField $token';
}
} catch (err) {
// ignore
}
final userAgent = ref.read(userAgentProvider);
if (userAgent.value != null) {
options.headers['User-Agent'] = userAgent.value;
final userAgent = ref.read(userAgentProvider);
if (userAgent.value != null) {
options.headers['User-Agent'] = userAgent.value;
}
return handler.next(options);
},
onResponse: (response, handler) {
// Check for 503 status code (Service Unavailable/Maintenance)
if (response.statusCode == 503) {
final networkStatusNotifier = ref.read(
networkStatusProvider.notifier,
);
if (response.headers.value('X-NotReady') != null) {
networkStatusNotifier.setNotReady();
} else {
networkStatusNotifier.setMaintenance();
}
} else if (response.statusCode != null &&
response.statusCode! >= 200 &&
response.statusCode! < 300) {
// Set online status for successful responses
final networkStatusNotifier = ref.read(
networkStatusProvider.notifier,
);
networkStatusNotifier.setOnline();
}
return handler.next(options);
return handler.next(response);
},
onError: (error, handler) {
// Handle network errors and set offline status
if (error.response?.statusCode == 503) {
final networkStatusNotifier = ref.read(
networkStatusProvider.notifier,
);
if (error.response?.headers.value('X-NotReady') != null) {
networkStatusNotifier.setNotReady();
} else {
networkStatusNotifier.setMaintenance();
}
}
return handler.next(error);
},
),
TalkerDioLogger(

64
lib/pods/network.g.dart Normal file
View File

@@ -0,0 +1,64 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'network.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(NetworkStatusNotifier)
const networkStatusProvider = NetworkStatusNotifierProvider._();
final class NetworkStatusNotifierProvider
extends $NotifierProvider<NetworkStatusNotifier, NetworkStatus> {
const NetworkStatusNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'networkStatusProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$networkStatusNotifierHash();
@$internal
@override
NetworkStatusNotifier create() => NetworkStatusNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(NetworkStatus value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<NetworkStatus>(value),
);
}
}
String _$networkStatusNotifierHash() =>
r'6f08e3067fa5265432f28f64e10775e3039506c3';
abstract class _$NetworkStatusNotifier extends $Notifier<NetworkStatus> {
NetworkStatus build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<NetworkStatus, NetworkStatus>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<NetworkStatus, NetworkStatus>,
NetworkStatus,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -8,6 +8,10 @@ abstract class PaginationController<T> {
bool get fetchedAll;
bool get isLoading;
bool get hasMore;
set hasMore(bool value);
String? get cursor;
set cursor(String? value);
FutureOr<List<T>> fetch();
@@ -31,19 +35,31 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
int get fetchedCount => state.value?.length ?? 0;
@override
bool get fetchedAll => totalCount != null && fetchedCount >= totalCount!;
bool get fetchedAll =>
!hasMore || (totalCount != null && fetchedCount >= totalCount!);
@override
bool isLoading = false;
@override
FutureOr<List<T>> build() async => fetch();
bool hasMore = true;
@override
String? cursor;
@override
FutureOr<List<T>> build() async {
cursor = null;
return fetch();
}
@override
Future<void> refresh() async {
isLoading = true;
totalCount = null;
state = AsyncData<List<T>>([]);
hasMore = true;
cursor = null;
state = AsyncLoading<List<T>>();
final newState = await AsyncValue.guard<List<T>>(() async {
return await fetch();
@@ -55,6 +71,7 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
@override
Future<void> fetchFurther() async {
if (fetchedAll) return;
if (isLoading) return;
isLoading = true;
state = AsyncLoading<List<T>>();
@@ -77,7 +94,9 @@ mixin AsyncPaginationFilter<F, T> on AsyncPaginationController<T>
// Reset the data
isLoading = true;
totalCount = null;
state = AsyncData<List<T>>([]);
hasMore = true;
cursor = null;
state = AsyncLoading<List<T>>();
currentFilter = filter;
final newState = await AsyncValue.guard<List<T>>(() async {

View File

@@ -10,6 +10,7 @@ part 'post_list.freezed.dart';
sealed class PostListQuery with _$PostListQuery {
const factory PostListQuery({
String? pubName,
List<String>? publishers,
String? realm,
int? type,
List<String>? categories,
@@ -61,35 +62,98 @@ class PostListNotifier extends AsyncNotifier<List<SnPost>>
Future<List<SnPost>> fetch() async {
final client = ref.read(apiClientProvider);
final queryParams = {
'offset': fetchedCount,
'take': pageSize,
'replies': currentFilter.includeReplies,
'orderDesc': currentFilter.orderDesc,
if (currentFilter.shuffle) 'shuffle': currentFilter.shuffle,
if (currentFilter.pubName != null) 'pub': currentFilter.pubName,
if (currentFilter.realm != null) 'realm': currentFilter.realm,
if (currentFilter.type != null) 'type': currentFilter.type,
if (currentFilter.tags != null) 'tags': currentFilter.tags,
if (currentFilter.categories != null)
'categories': currentFilter.categories,
if (currentFilter.pinned != null) 'pinned': currentFilter.pinned,
if (currentFilter.order != null) 'order': currentFilter.order,
if (currentFilter.periodStart != null)
'periodStart': currentFilter.periodStart,
if (currentFilter.periodEnd != null) 'periodEnd': currentFilter.periodEnd,
if (currentFilter.queryTerm != null) 'query': currentFilter.queryTerm,
if (currentFilter.mediaOnly != null) 'media': currentFilter.mediaOnly,
};
// Handle multiple publishers by making separate requests and combining results
if (currentFilter.publishers != null &&
currentFilter.publishers!.isNotEmpty) {
final allPosts = <SnPost>[];
var totalPostsCount = 0;
final response = await client.get(
'/sphere/posts',
queryParameters: queryParams,
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
return response.data
.map((json) => SnPost.fromJson(json))
.cast<SnPost>()
.toList();
for (final publisherName in currentFilter.publishers!) {
final queryParams = {
'offset': fetchedCount,
'take': pageSize,
'replies': currentFilter.includeReplies,
'orderDesc': currentFilter.orderDesc,
if (currentFilter.shuffle) 'shuffle': currentFilter.shuffle,
'pub': publisherName,
if (currentFilter.realm != null) 'realm': currentFilter.realm,
if (currentFilter.type != null) 'type': currentFilter.type,
if (currentFilter.tags != null) 'tags': currentFilter.tags,
if (currentFilter.categories != null)
'categories': currentFilter.categories,
if (currentFilter.pinned != null) 'pinned': currentFilter.pinned,
if (currentFilter.order != null) 'order': currentFilter.order,
if (currentFilter.periodStart != null)
'periodStart': currentFilter.periodStart,
if (currentFilter.periodEnd != null)
'periodEnd': currentFilter.periodEnd,
if (currentFilter.queryTerm != null) 'query': currentFilter.queryTerm,
if (currentFilter.mediaOnly != null) 'media': currentFilter.mediaOnly,
};
final response = await client.get(
'/sphere/posts',
queryParameters: queryParams,
);
final posts = response.data
.map((json) => SnPost.fromJson(json))
.cast<SnPost>()
.toList();
allPosts.addAll(posts);
totalPostsCount += int.parse(response.headers.value('X-Total') ?? '0');
}
// Sort combined results by creation date (newest first)
allPosts.sort(
(a, b) => (b.createdAt ?? DateTime.now()).compareTo(
a.createdAt ?? DateTime.now(),
),
);
// Apply pagination to combined results
final startIndex = fetchedCount;
final endIndex = (fetchedCount + pageSize).clamp(0, allPosts.length);
final paginatedPosts = startIndex < allPosts.length
? allPosts.sublist(startIndex, endIndex)
: <SnPost>[];
totalCount = totalPostsCount;
return paginatedPosts;
} else {
// Single publisher or no publisher filter
final queryParams = {
'offset': fetchedCount,
'take': pageSize,
'replies': currentFilter.includeReplies,
'orderDesc': currentFilter.orderDesc,
if (currentFilter.shuffle) 'shuffle': currentFilter.shuffle,
if (currentFilter.pubName != null) 'pub': currentFilter.pubName,
if (currentFilter.realm != null) 'realm': currentFilter.realm,
if (currentFilter.type != null) 'type': currentFilter.type,
if (currentFilter.tags != null) 'tags': currentFilter.tags,
if (currentFilter.categories != null)
'categories': currentFilter.categories,
if (currentFilter.pinned != null) 'pinned': currentFilter.pinned,
if (currentFilter.order != null) 'order': currentFilter.order,
if (currentFilter.periodStart != null)
'periodStart': currentFilter.periodStart,
if (currentFilter.periodEnd != null)
'periodEnd': currentFilter.periodEnd,
if (currentFilter.queryTerm != null) 'query': currentFilter.queryTerm,
if (currentFilter.mediaOnly != null) 'media': currentFilter.mediaOnly,
};
final response = await client.get(
'/sphere/posts',
queryParameters: queryParams,
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
return response.data
.map((json) => SnPost.fromJson(json))
.cast<SnPost>()
.toList();
}
}
}

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$PostListQuery {
String? get pubName; String? get realm; int? get type; List<String>? get categories; List<String>? get tags; bool? get pinned; bool get shuffle; bool? get includeReplies; bool? get mediaOnly; String? get queryTerm; String? get order; int? get periodStart; int? get periodEnd; bool get orderDesc;
String? get pubName; List<String>? get publishers; String? get realm; int? get type; List<String>? get categories; List<String>? get tags; bool? get pinned; bool get shuffle; bool? get includeReplies; bool? get mediaOnly; String? get queryTerm; String? get order; int? get periodStart; int? get periodEnd; bool get orderDesc;
/// Create a copy of PostListQuery
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $PostListQueryCopyWith<PostListQuery> get copyWith => _$PostListQueryCopyWithImp
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is PostListQuery&&(identical(other.pubName, pubName) || other.pubName == pubName)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.pinned, pinned) || other.pinned == pinned)&&(identical(other.shuffle, shuffle) || other.shuffle == shuffle)&&(identical(other.includeReplies, includeReplies) || other.includeReplies == includeReplies)&&(identical(other.mediaOnly, mediaOnly) || other.mediaOnly == mediaOnly)&&(identical(other.queryTerm, queryTerm) || other.queryTerm == queryTerm)&&(identical(other.order, order) || other.order == order)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&(identical(other.orderDesc, orderDesc) || other.orderDesc == orderDesc));
return identical(this, other) || (other.runtimeType == runtimeType&&other is PostListQuery&&(identical(other.pubName, pubName) || other.pubName == pubName)&&const DeepCollectionEquality().equals(other.publishers, publishers)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.pinned, pinned) || other.pinned == pinned)&&(identical(other.shuffle, shuffle) || other.shuffle == shuffle)&&(identical(other.includeReplies, includeReplies) || other.includeReplies == includeReplies)&&(identical(other.mediaOnly, mediaOnly) || other.mediaOnly == mediaOnly)&&(identical(other.queryTerm, queryTerm) || other.queryTerm == queryTerm)&&(identical(other.order, order) || other.order == order)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&(identical(other.orderDesc, orderDesc) || other.orderDesc == orderDesc));
}
@override
int get hashCode => Object.hash(runtimeType,pubName,realm,type,const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(tags),pinned,shuffle,includeReplies,mediaOnly,queryTerm,order,periodStart,periodEnd,orderDesc);
int get hashCode => Object.hash(runtimeType,pubName,const DeepCollectionEquality().hash(publishers),realm,type,const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(tags),pinned,shuffle,includeReplies,mediaOnly,queryTerm,order,periodStart,periodEnd,orderDesc);
@override
String toString() {
return 'PostListQuery(pubName: $pubName, realm: $realm, type: $type, categories: $categories, tags: $tags, pinned: $pinned, shuffle: $shuffle, includeReplies: $includeReplies, mediaOnly: $mediaOnly, queryTerm: $queryTerm, order: $order, periodStart: $periodStart, periodEnd: $periodEnd, orderDesc: $orderDesc)';
return 'PostListQuery(pubName: $pubName, publishers: $publishers, realm: $realm, type: $type, categories: $categories, tags: $tags, pinned: $pinned, shuffle: $shuffle, includeReplies: $includeReplies, mediaOnly: $mediaOnly, queryTerm: $queryTerm, order: $order, periodStart: $periodStart, periodEnd: $periodEnd, orderDesc: $orderDesc)';
}
@@ -45,7 +45,7 @@ abstract mixin class $PostListQueryCopyWith<$Res> {
factory $PostListQueryCopyWith(PostListQuery value, $Res Function(PostListQuery) _then) = _$PostListQueryCopyWithImpl;
@useResult
$Res call({
String? pubName, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc
String? pubName, List<String>? publishers, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc
});
@@ -62,10 +62,11 @@ class _$PostListQueryCopyWithImpl<$Res>
/// Create a copy of PostListQuery
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? pubName = freezed,Object? realm = freezed,Object? type = freezed,Object? categories = freezed,Object? tags = freezed,Object? pinned = freezed,Object? shuffle = null,Object? includeReplies = freezed,Object? mediaOnly = freezed,Object? queryTerm = freezed,Object? order = freezed,Object? periodStart = freezed,Object? periodEnd = freezed,Object? orderDesc = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? pubName = freezed,Object? publishers = freezed,Object? realm = freezed,Object? type = freezed,Object? categories = freezed,Object? tags = freezed,Object? pinned = freezed,Object? shuffle = null,Object? includeReplies = freezed,Object? mediaOnly = freezed,Object? queryTerm = freezed,Object? order = freezed,Object? periodStart = freezed,Object? periodEnd = freezed,Object? orderDesc = null,}) {
return _then(_self.copyWith(
pubName: freezed == pubName ? _self.pubName : pubName // ignore: cast_nullable_to_non_nullable
as String?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_to_non_nullable
as String?,publishers: freezed == publishers ? _self.publishers : publishers // ignore: cast_nullable_to_non_nullable
as List<String>?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_to_non_nullable
as String?,type: freezed == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int?,categories: freezed == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable
as List<String>?,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
@@ -160,10 +161,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? pubName, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? pubName, List<String>? publishers, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _PostListQuery() when $default != null:
return $default(_that.pubName,_that.realm,_that.type,_that.categories,_that.tags,_that.pinned,_that.shuffle,_that.includeReplies,_that.mediaOnly,_that.queryTerm,_that.order,_that.periodStart,_that.periodEnd,_that.orderDesc);case _:
return $default(_that.pubName,_that.publishers,_that.realm,_that.type,_that.categories,_that.tags,_that.pinned,_that.shuffle,_that.includeReplies,_that.mediaOnly,_that.queryTerm,_that.order,_that.periodStart,_that.periodEnd,_that.orderDesc);case _:
return orElse();
}
@@ -181,10 +182,10 @@ return $default(_that.pubName,_that.realm,_that.type,_that.categories,_that.tags
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? pubName, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? pubName, List<String>? publishers, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc) $default,) {final _that = this;
switch (_that) {
case _PostListQuery():
return $default(_that.pubName,_that.realm,_that.type,_that.categories,_that.tags,_that.pinned,_that.shuffle,_that.includeReplies,_that.mediaOnly,_that.queryTerm,_that.order,_that.periodStart,_that.periodEnd,_that.orderDesc);}
return $default(_that.pubName,_that.publishers,_that.realm,_that.type,_that.categories,_that.tags,_that.pinned,_that.shuffle,_that.includeReplies,_that.mediaOnly,_that.queryTerm,_that.order,_that.periodStart,_that.periodEnd,_that.orderDesc);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -198,10 +199,10 @@ return $default(_that.pubName,_that.realm,_that.type,_that.categories,_that.tags
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? pubName, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? pubName, List<String>? publishers, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc)? $default,) {final _that = this;
switch (_that) {
case _PostListQuery() when $default != null:
return $default(_that.pubName,_that.realm,_that.type,_that.categories,_that.tags,_that.pinned,_that.shuffle,_that.includeReplies,_that.mediaOnly,_that.queryTerm,_that.order,_that.periodStart,_that.periodEnd,_that.orderDesc);case _:
return $default(_that.pubName,_that.publishers,_that.realm,_that.type,_that.categories,_that.tags,_that.pinned,_that.shuffle,_that.includeReplies,_that.mediaOnly,_that.queryTerm,_that.order,_that.periodStart,_that.periodEnd,_that.orderDesc);case _:
return null;
}
@@ -213,10 +214,19 @@ return $default(_that.pubName,_that.realm,_that.type,_that.categories,_that.tags
class _PostListQuery implements PostListQuery {
const _PostListQuery({this.pubName, this.realm, this.type, final List<String>? categories, final List<String>? tags, this.pinned, this.shuffle = false, this.includeReplies, this.mediaOnly, this.queryTerm, this.order, this.periodStart, this.periodEnd, this.orderDesc = true}): _categories = categories,_tags = tags;
const _PostListQuery({this.pubName, final List<String>? publishers, this.realm, this.type, final List<String>? categories, final List<String>? tags, this.pinned, this.shuffle = false, this.includeReplies, this.mediaOnly, this.queryTerm, this.order, this.periodStart, this.periodEnd, this.orderDesc = true}): _publishers = publishers,_categories = categories,_tags = tags;
@override final String? pubName;
final List<String>? _publishers;
@override List<String>? get publishers {
final value = _publishers;
if (value == null) return null;
if (_publishers is EqualUnmodifiableListView) return _publishers;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override final String? realm;
@override final int? type;
final List<String>? _categories;
@@ -257,16 +267,16 @@ _$PostListQueryCopyWith<_PostListQuery> get copyWith => __$PostListQueryCopyWith
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostListQuery&&(identical(other.pubName, pubName) || other.pubName == pubName)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.pinned, pinned) || other.pinned == pinned)&&(identical(other.shuffle, shuffle) || other.shuffle == shuffle)&&(identical(other.includeReplies, includeReplies) || other.includeReplies == includeReplies)&&(identical(other.mediaOnly, mediaOnly) || other.mediaOnly == mediaOnly)&&(identical(other.queryTerm, queryTerm) || other.queryTerm == queryTerm)&&(identical(other.order, order) || other.order == order)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&(identical(other.orderDesc, orderDesc) || other.orderDesc == orderDesc));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostListQuery&&(identical(other.pubName, pubName) || other.pubName == pubName)&&const DeepCollectionEquality().equals(other._publishers, _publishers)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.pinned, pinned) || other.pinned == pinned)&&(identical(other.shuffle, shuffle) || other.shuffle == shuffle)&&(identical(other.includeReplies, includeReplies) || other.includeReplies == includeReplies)&&(identical(other.mediaOnly, mediaOnly) || other.mediaOnly == mediaOnly)&&(identical(other.queryTerm, queryTerm) || other.queryTerm == queryTerm)&&(identical(other.order, order) || other.order == order)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&(identical(other.orderDesc, orderDesc) || other.orderDesc == orderDesc));
}
@override
int get hashCode => Object.hash(runtimeType,pubName,realm,type,const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_tags),pinned,shuffle,includeReplies,mediaOnly,queryTerm,order,periodStart,periodEnd,orderDesc);
int get hashCode => Object.hash(runtimeType,pubName,const DeepCollectionEquality().hash(_publishers),realm,type,const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_tags),pinned,shuffle,includeReplies,mediaOnly,queryTerm,order,periodStart,periodEnd,orderDesc);
@override
String toString() {
return 'PostListQuery(pubName: $pubName, realm: $realm, type: $type, categories: $categories, tags: $tags, pinned: $pinned, shuffle: $shuffle, includeReplies: $includeReplies, mediaOnly: $mediaOnly, queryTerm: $queryTerm, order: $order, periodStart: $periodStart, periodEnd: $periodEnd, orderDesc: $orderDesc)';
return 'PostListQuery(pubName: $pubName, publishers: $publishers, realm: $realm, type: $type, categories: $categories, tags: $tags, pinned: $pinned, shuffle: $shuffle, includeReplies: $includeReplies, mediaOnly: $mediaOnly, queryTerm: $queryTerm, order: $order, periodStart: $periodStart, periodEnd: $periodEnd, orderDesc: $orderDesc)';
}
@@ -277,7 +287,7 @@ abstract mixin class _$PostListQueryCopyWith<$Res> implements $PostListQueryCopy
factory _$PostListQueryCopyWith(_PostListQuery value, $Res Function(_PostListQuery) _then) = __$PostListQueryCopyWithImpl;
@override @useResult
$Res call({
String? pubName, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc
String? pubName, List<String>? publishers, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc
});
@@ -294,10 +304,11 @@ class __$PostListQueryCopyWithImpl<$Res>
/// Create a copy of PostListQuery
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? pubName = freezed,Object? realm = freezed,Object? type = freezed,Object? categories = freezed,Object? tags = freezed,Object? pinned = freezed,Object? shuffle = null,Object? includeReplies = freezed,Object? mediaOnly = freezed,Object? queryTerm = freezed,Object? order = freezed,Object? periodStart = freezed,Object? periodEnd = freezed,Object? orderDesc = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? pubName = freezed,Object? publishers = freezed,Object? realm = freezed,Object? type = freezed,Object? categories = freezed,Object? tags = freezed,Object? pinned = freezed,Object? shuffle = null,Object? includeReplies = freezed,Object? mediaOnly = freezed,Object? queryTerm = freezed,Object? order = freezed,Object? periodStart = freezed,Object? periodEnd = freezed,Object? orderDesc = null,}) {
return _then(_PostListQuery(
pubName: freezed == pubName ? _self.pubName : pubName // ignore: cast_nullable_to_non_nullable
as String?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_to_non_nullable
as String?,publishers: freezed == publishers ? _self._publishers : publishers // ignore: cast_nullable_to_non_nullable
as List<String>?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_to_non_nullable
as String?,type: freezed == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int?,categories: freezed == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable
as List<String>?,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable

View File

@@ -57,9 +57,6 @@ class SitePagesNotifier extends AsyncNotifier<List<SnPublicationPage>> {
);
final newPage = SnPublicationPage.fromJson(resp.data);
// Refresh the pages list
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
return newPage;
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
@@ -80,9 +77,6 @@ class SitePagesNotifier extends AsyncNotifier<List<SnPublicationPage>> {
);
final updatedPage = SnPublicationPage.fromJson(resp.data);
// Refresh the pages list
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
return updatedPage;
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
@@ -95,9 +89,6 @@ class SitePagesNotifier extends AsyncNotifier<List<SnPublicationPage>> {
try {
final apiClient = ref.read(apiClientProvider);
await apiClient.delete('/zone/sites/pages/$pageId');
// Refresh the pages list
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
rethrow;
@@ -105,8 +96,9 @@ class SitePagesNotifier extends AsyncNotifier<List<SnPublicationPage>> {
}
}
final sitePagesNotifierProvider = AsyncNotifierProvider.autoDispose.family<
SitePagesNotifier,
List<SnPublicationPage>,
({String pubName, String siteSlug})
>(SitePagesNotifier.new);
final sitePagesNotifierProvider = AsyncNotifierProvider.autoDispose
.family<
SitePagesNotifier,
List<SnPublicationPage>,
({String pubName, String siteSlug})
>(SitePagesNotifier.new);

View File

@@ -1,84 +0,0 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/publication_site.dart';
import 'package:island/pods/network.dart';
class SiteNotifier extends AsyncNotifier<SnPublicationSite> {
final ({String pubName, String? siteId}) arg;
SiteNotifier(this.arg);
@override
FutureOr<SnPublicationSite> build() async {
if (arg.siteId == null || arg.siteId!.isEmpty) {
return SnPublicationSite(
id: '',
slug: '',
name: '',
publisherId: arg.pubName,
accountId: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
pages: [],
);
}
try {
final client = ref.read(apiClientProvider);
final response = await client.get('/sphere/sites/${arg.siteId}');
return SnPublicationSite.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<void> saveSite(SnPublicationSite site) async {
state = const AsyncValue.loading();
try {
final client = ref.read(apiClientProvider);
final url = '/sphere/sites';
final response =
site.id.isEmpty
? await client.post(url, data: site.toJson())
: await client.patch('$url/${site.id}', data: site.toJson());
state = AsyncValue.data(SnPublicationSite.fromJson(response.data));
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
rethrow;
}
}
Future<void> deleteSite() async {
final siteId = arg.siteId;
if (siteId == null || siteId.isEmpty) return;
state = const AsyncValue.loading();
try {
final client = ref.read(apiClientProvider);
await client.delete('/sphere/sites/$siteId');
state = AsyncValue.data(
SnPublicationSite(
id: '',
slug: '',
name: '',
publisherId: arg.pubName,
accountId: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
pages: [],
),
);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
rethrow;
}
}
}
final siteNotifierProvider = AsyncNotifierProvider.autoDispose.family<
SiteNotifier,
SnPublicationSite,
({String pubName, String? siteId})
>(SiteNotifier.new);

View File

@@ -25,10 +25,9 @@ ThemeSet createAppThemeSet(AppSettings settings) {
}
ThemeData createAppTheme(Brightness brightness, AppSettings settings) {
final seedColor =
settings.appColorScheme != null
? Color(settings.appColorScheme!)
: Colors.indigo;
final seedColor = settings.appColorScheme != null
? Color(settings.appColorScheme!)
: Colors.indigo;
var colorScheme = ColorScheme.fromSeed(
seedColor: seedColor,
@@ -38,33 +37,33 @@ ThemeData createAppTheme(Brightness brightness, AppSettings settings) {
final customColors = settings.customColors;
if (customColors != null) {
colorScheme = colorScheme.copyWith(
primary:
customColors.primary != null ? Color(customColors.primary!) : null,
secondary:
customColors.secondary != null
? Color(customColors.secondary!)
: null,
tertiary:
customColors.tertiary != null ? Color(customColors.tertiary!) : null,
surface:
customColors.surface != null ? Color(customColors.surface!) : null,
background:
customColors.background != null
? Color(customColors.background!)
: null,
primary: customColors.primary != null
? Color(customColors.primary!)
: null,
secondary: customColors.secondary != null
? Color(customColors.secondary!)
: null,
tertiary: customColors.tertiary != null
? Color(customColors.tertiary!)
: null,
surface: customColors.surface != null
? Color(customColors.surface!)
: null,
background: customColors.background != null
? Color(customColors.background!)
: null,
error: customColors.error != null ? Color(customColors.error!) : null,
);
}
final hasAppBarTransparent = settings.appBarTransparent;
final useM3 = settings.useMaterial3;
final inUseFonts =
settings.customFonts?.split(',').map((ele) => ele.trim()).toList() ??
['Nunito'];
return ThemeData(
useMaterial3: useM3,
useMaterial3: true,
colorScheme: colorScheme,
brightness: brightness,
fontFamily: inUseFonts.firstOrNull,
@@ -78,10 +77,12 @@ ThemeData createAppTheme(Brightness brightness, AppSettings settings) {
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: hasAppBarTransparent ? 0 : null,
backgroundColor:
hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
foregroundColor:
hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
backgroundColor: hasAppBarTransparent
? Colors.transparent
: colorScheme.primary,
foregroundColor: hasAppBarTransparent
? colorScheme.onSurface
: colorScheme.onPrimary,
),
cardTheme: CardThemeData(
color: colorScheme.surfaceContainer.withOpacity(

View File

@@ -20,8 +20,6 @@ class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
Future<List<SnTimelineEvent>> fetch() async {
final client = ref.read(apiClientProvider);
final cursor = state.value?.lastOrNull?.createdAt.toUtc().toIso8601String();
final queryParameters = {
if (cursor != null) 'cursor': cursor,
'take': pageSize,
@@ -37,10 +35,16 @@ class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
.map((e) => SnTimelineEvent.fromJson(e as Map<String, dynamic>))
.toList();
final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
totalCount =
(state.value?.length ?? 0) + items.length + (hasMore ? pageSize : 0);
hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
// Find the latest createdAt timestamp from all items for cursor-based pagination
// This ensures we get items created before this timestamp, regardless of sort order
if (items.isNotEmpty) {
final latestCreatedAt = items
.where((e) => e.type.startsWith('posts.'))
.map((e) => e.createdAt)
.reduce((a, b) => a.isBefore(b) ? a : b);
cursor = latestCreatedAt.toUtc().toIso8601String();
}
return items;
}

View File

@@ -36,71 +36,41 @@ class UserInfoNotifier extends AsyncNotifier<SnAccount?> {
}
return user;
} catch (error, stackTrace) {
if (!kIsWeb) {
if (error is DioException) {
showOverlayDialog<bool>(
builder:
(context, close) => AlertDialog(
title: Text('failedToLoadUserInfo'.tr()),
content: Text(
[
(error.response?.statusCode == 401
? 'failedToLoadUserInfoUnauthorized'
: 'failedToLoadUserInfoNetwork')
.tr()
.trim(),
'',
'${error.response?.statusCode ?? 'Network Error'}',
if (error.response?.headers != null)
error.response?.headers,
if (error.response?.data != null)
jsonEncode(error.response?.data),
].join('\n'),
),
actions: [
TextButton(
onPressed: () => close(false),
child: Text('okay'.tr()),
),
TextButton(
onPressed: () => close(true),
child: Text('retry'.tr()),
),
],
),
).then((value) {
if (value == true) {
ref.invalidateSelf();
}
});
} else {
showOverlayDialog<bool>(
builder:
(context, close) => AlertDialog(
title: Text('failedToLoadUserInfo'.tr()),
content: Text(
[
'failedToLoadUserInfoNetwork'.tr(),
error.toString(),
].join('\n\n').trim(),
),
actions: [
TextButton(
onPressed: () => close(false),
child: Text('okay'.tr()),
),
TextButton(
onPressed: () => close(true),
child: Text('retry'.tr()),
),
],
),
).then((value) {
if (value == true) {
ref.invalidateSelf();
}
});
}
if (error is DioException) {
if (error.response?.statusCode == 503) return null;
showOverlayDialog<bool>(
builder: (context, close) => AlertDialog(
title: Text('failedToLoadUserInfo'.tr()),
content: Text(
[
(error.response?.statusCode == 401
? 'failedToLoadUserInfoUnauthorized'
: 'failedToLoadUserInfoNetwork')
.tr()
.trim(),
'',
'${error.response?.statusCode ?? 'Network Error'}',
if (error.response?.headers != null) error.response?.headers,
if (error.response?.data != null)
jsonEncode(error.response?.data),
].join('\n'),
),
actions: [
TextButton(
onPressed: () => close(false),
child: Text('okay'.tr()),
),
TextButton(
onPressed: () => close(true),
child: Text('retry'.tr()),
),
],
),
).then((value) {
if (value == true) {
ref.invalidateSelf();
}
});
}
talker.error(
"[UserInfo] Failed to fetch user info...",

View File

@@ -52,6 +52,11 @@ class WebSocketService {
DateTime? _heartbeatAt;
Duration? heartbeatDelay;
// Reconnection tracking
int _reconnectCount = 0;
DateTime? _reconnectWindowStart;
static const int _maxReconnectsPerMinute = 5;
Stream<WebSocketPacket> get dataStream => _streamController.stream;
Stream<WebSocketState> get statusStream => _statusStreamController.stream;
@@ -79,8 +84,9 @@ class WebSocketService {
_scheduleHeartbeat();
_channel!.stream.listen(
(data) {
final dataStr =
data is Uint8List ? utf8.decode(data) : data.toString();
final dataStr = data is Uint8List
? utf8.decode(data)
: data.toString();
final packet = WebSocketPacket.fromJson(jsonDecode(dataStr));
if (packet.type == 'error.dupe') {
_statusStreamController.sink.add(WebSocketState.duplicateDevice());
@@ -123,6 +129,35 @@ class WebSocketService {
}
void _scheduleReconnect() {
// Check if we've exceeded the reconnect limit
final now = DateTime.now();
if (_reconnectWindowStart == null ||
now.difference(_reconnectWindowStart!).inMinutes >= 1) {
// Reset window if it's been more than 1 minute since the window started
_reconnectWindowStart = now;
_reconnectCount = 0;
}
_reconnectCount++;
if (_reconnectCount > _maxReconnectsPerMinute) {
talker.error(
'[WebSocket] Reconnect limit exceeded: $_maxReconnectsPerMinute reconnections in the last minute. Stopping auto-reconnect.',
);
_statusStreamController.sink.add(WebSocketState.serverDown());
return;
}
_reconnectTimer?.cancel();
_reconnectTimer = Timer(const Duration(milliseconds: 500), () {
_statusStreamController.sink.add(WebSocketState.connecting());
connect(_ref);
});
}
void manualReconnect() {
_statusStreamController.sink.add(WebSocketState.connecting());
talker.info('[WebSocket] Manual reconnect triggered by user');
_reconnectTimer?.cancel();
_reconnectTimer = Timer(const Duration(milliseconds: 500), () {
_statusStreamController.sink.add(WebSocketState.connecting());
@@ -204,4 +239,9 @@ class WebSocketStateNotifier extends Notifier<WebSocketState> {
_reconnectTimer?.cancel();
state = const WebSocketState.disconnected();
}
void manualReconnect() {
final service = ref.read(websocketProvider);
service.manualReconnect();
}
}

View File

@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/about.dart';
import 'package:island/screens/dashboard/dash.dart';
import 'package:island/screens/developers/app_detail.dart';
import 'package:island/screens/developers/bot_detail.dart';
import 'package:island/screens/developers/hub.dart';
@@ -18,6 +19,7 @@ import 'package:island/screens/files/file_detail.dart';
import 'package:island/screens/posts/post_categories_list.dart';
import 'package:island/screens/posts/post_category_detail.dart';
import 'package:island/screens/posts/post_search.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/app_wrapper.dart';
import 'package:island/screens/tabs.dart';
import 'package:island/screens/explore.dart';
@@ -120,7 +122,12 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute(
name: 'logs',
path: '/logs',
builder: (context, state) => TalkerScreen(talker: talker),
builder: (context, state) => TalkerScreen(
talker: talker,
appBarTitle: 'Debug Logs',
appBarLeading: const PageBackButton(),
theme: TalkerScreenTheme.fromTheme(Theme.of(context)),
),
),
// Web articles
@@ -185,10 +192,20 @@ final routerProvider = Provider<GoRouter>((ref) {
return TabsScreen(child: child);
},
routes: [
// Dashboard tab
GoRoute(
name: 'dashboard',
path: '/',
pageBuilder: (context, state) => CustomTransitionPage(
key: const ValueKey('dashboard'),
child: const DashboardScreen(),
transitionsBuilder: _tabPagesTransitionBuilder,
),
),
// Explore tab
GoRoute(
name: 'explore',
path: '/',
path: '/explore',
pageBuilder: (context, state) => CustomTransitionPage(
key: const ValueKey('explore'),
child: const ExploreScreen(),

View File

@@ -12,7 +12,6 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/services/update_service.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -97,232 +96,213 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
return AppScaffold(
isNoBackground: false,
appBar: AppBar(title: Text('about'.tr()), elevation: 0),
body:
_isLoading
? const Center(child: CircularProgressIndicator())
: _errorMessage != null
? Center(child: Text(_errorMessage!))
: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 540),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 24),
// App Icon and Name
CircleAvatar(
radius: 50,
backgroundColor: theme.colorScheme.primary
.withOpacity(0.1),
child: Image.asset(
'assets/icons/icon.png',
width: 56,
height: 56,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _errorMessage != null
? Center(child: Text(_errorMessage!))
: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 540),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 24),
// App Icon and Name
CircleAvatar(
radius: 50,
backgroundColor: theme.colorScheme.primary.withOpacity(
0.1,
),
const SizedBox(height: 16),
Text(
_packageInfo.appName,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
child: Image.asset(
'assets/icons/icon.png',
width: 56,
height: 56,
),
Text(
'aboutScreenVersionInfo'.tr(
args: [
_packageInfo.version,
_packageInfo.buildNumber,
],
),
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.textTheme.bodySmall?.color,
),
),
const SizedBox(height: 16),
Text(
_packageInfo.appName,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
const SizedBox(height: 32),
// App Info Card
_buildSection(
context,
title: 'aboutScreenAppInfoSectionTitle'.tr(),
children: [
_buildInfoItem(
context,
icon: Symbols.info,
label: 'aboutScreenPackageNameLabel'.tr(),
value: _packageInfo.packageName,
),
_buildInfoItem(
context,
icon: Symbols.update,
label: 'aboutScreenVersionLabel'.tr(),
value: _packageInfo.version,
),
_buildInfoItem(
context,
icon: Symbols.build,
label: 'aboutScreenBuildNumberLabel'.tr(),
value: _packageInfo.buildNumber,
),
),
Text(
'aboutScreenVersionInfo'.tr(
args: [
_packageInfo.version,
_packageInfo.buildNumber,
],
),
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.textTheme.bodySmall?.color,
),
),
const SizedBox(height: 32),
if (_deviceInfo != null) const SizedBox(height: 16),
if (_deviceInfo != null)
_buildSection(
// App Info Card
_buildSection(
context,
title: 'aboutScreenAppInfoSectionTitle'.tr(),
children: [
_buildInfoItem(
context,
title: 'Device Information',
children: [
FutureBuilder<String>(
future: udid.getDeviceName(),
builder: (context, snapshot) {
final value =
snapshot.hasData
? snapshot.data!
: 'unknown'.tr();
return _buildInfoItem(
context,
icon: Symbols.label,
label: 'aboutDeviceName'.tr(),
value: value,
);
},
),
_buildInfoItem(
context,
icon: Symbols.fingerprint,
label: 'aboutDeviceIdentifier'.tr(),
value: _deviceUdid ?? 'N/A',
copyable: true,
),
],
icon: Symbols.info,
label: 'aboutScreenPackageNameLabel'.tr(),
value: _packageInfo.packageName,
),
_buildInfoItem(
context,
icon: Symbols.update,
label: 'aboutScreenVersionLabel'.tr(),
value: _packageInfo.version,
),
_buildInfoItem(
context,
icon: Symbols.build,
label: 'aboutScreenBuildNumberLabel'.tr(),
value: _packageInfo.buildNumber,
),
],
),
const SizedBox(height: 16),
if (_deviceInfo != null) const SizedBox(height: 16),
// Links Card
if (_deviceInfo != null)
_buildSection(
context,
title: 'aboutScreenLinksSectionTitle'.tr(),
title: 'Device Information',
children: [
_buildListTile(
context,
icon: Symbols.system_update,
title: 'Check for updates',
onTap: () async {
final svc = UpdateService();
showLoadingModal(context);
svc.checkForUpdates(context);
if (!context.mounted) return;
hideLoadingModal(context);
},
),
_buildListTile(
context,
icon: Symbols.privacy_tip,
title: 'aboutScreenPrivacyPolicyTitle'.tr(),
onTap:
() => _launchURL(
'https://solsynth.dev/terms/privacy-policy',
),
),
_buildListTile(
context,
icon: Symbols.description,
title: 'aboutScreenTermsOfServiceTitle'.tr(),
onTap:
() => _launchURL(
'https://solsynth.dev/terms/user-agreement',
),
),
_buildListTile(
context,
icon: Symbols.code,
title: 'aboutScreenOpenSourceLicensesTitle'.tr(),
onTap: () {
showLicensePage(
context: context,
applicationName: _packageInfo.appName,
applicationVersion:
'Version ${_packageInfo.version}',
FutureBuilder<String>(
future: udid.getDeviceName(),
builder: (context, snapshot) {
final value = snapshot.hasData
? snapshot.data!
: 'unknown'.tr();
return _buildInfoItem(
context,
icon: Symbols.label,
label: 'aboutDeviceName'.tr(),
value: value,
);
},
),
_buildInfoItem(
context,
icon: Symbols.fingerprint,
label: 'aboutDeviceIdentifier'.tr(),
value: _deviceUdid ?? 'N/A',
copyable: true,
),
],
),
const SizedBox(height: 16),
const SizedBox(height: 16),
// Developer Info
_buildSection(
context,
title: 'aboutScreenDeveloperSectionTitle'.tr(),
children: [
_buildListTile(
context,
icon: Symbols.email,
title: 'aboutScreenContactUsTitle'.tr(),
subtitle: 'lily@solsynth.dev',
onTap:
() => _launchURL('mailto:lily@solsynth.dev'),
// Links Card
_buildSection(
context,
title: 'aboutScreenLinksSectionTitle'.tr(),
children: [
_buildListTile(
context,
icon: Symbols.privacy_tip,
title: 'aboutScreenPrivacyPolicyTitle'.tr(),
onTap: () => _launchURL(
'https://solsynth.dev/terms/privacy-policy',
),
),
_buildListTile(
context,
icon: Symbols.description,
title: 'aboutScreenTermsOfServiceTitle'.tr(),
onTap: () => _launchURL(
'https://solsynth.dev/terms/user-agreement',
),
),
_buildListTile(
context,
icon: Symbols.code,
title: 'aboutScreenOpenSourceLicensesTitle'.tr(),
onTap: () {
showLicensePage(
context: context,
applicationName: _packageInfo.appName,
applicationVersion:
'Version ${_packageInfo.version}',
);
},
),
],
),
const SizedBox(height: 16),
// Developer Info
_buildSection(
context,
title: 'aboutScreenDeveloperSectionTitle'.tr(),
children: [
_buildListTile(
context,
icon: Symbols.email,
title: 'aboutScreenContactUsTitle'.tr(),
subtitle: 'lily@solsynth.dev',
onTap: () => _launchURL('mailto:lily@solsynth.dev'),
),
_buildListTile(
context,
icon: Symbols.copyright,
title: 'aboutScreenLicenseTitle'.tr(),
subtitle: 'aboutScreenLicenseContent'.tr(),
onTap: () => _launchURL(
'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt',
),
),
if (kIsWeb || !(Platform.isMacOS || Platform.isIOS))
_buildListTile(
context,
icon: Symbols.copyright,
title: 'aboutScreenLicenseTitle'.tr(),
subtitle: 'aboutScreenLicenseContent'.tr(
icon: Symbols.favorite,
title: 'donate'.tr(),
subtitle: 'donateDescription'.tr(),
onTap: () {
launchUrlString(
'https://afdian.com/@littlesheep',
);
},
),
],
),
const SizedBox(height: 32),
// Copyright
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text(
'aboutScreenCopyright'.tr(
args: [DateTime.now().year.toString()],
),
onTap:
() => _launchURL(
'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt',
),
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
if (kIsWeb || !(Platform.isMacOS || Platform.isIOS))
_buildListTile(
context,
icon: Symbols.favorite,
title: 'donate'.tr(),
subtitle: 'donateDescription'.tr(),
onTap: () {
launchUrlString(
'https://afdian.com/@littlesheep',
);
},
),
const Gap(1),
Text(
'aboutScreenMadeWith'.tr(),
textAlign: TextAlign.center,
).fontSize(10).opacity(0.8),
],
),
),
const SizedBox(height: 32),
// Copyright
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text(
'aboutScreenCopyright'.tr(
args: [DateTime.now().year.toString()],
),
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
const Gap(1),
Text(
'aboutScreenMadeWith'.tr(),
textAlign: TextAlign.center,
).fontSize(10).opacity(0.8),
],
),
),
Gap(MediaQuery.of(context).padding.bottom + 16),
],
),
Gap(MediaQuery.of(context).padding.bottom + 16),
],
),
),
),
),
);
}

View File

@@ -331,128 +331,136 @@ class AccountScreen extends HookConsumerWidget {
if (availableWidth > totalMin) {
return Row(
spacing: 8,
children:
children
.map((child) => Expanded(child: child))
.toList(),
children: children
.map((child) => Expanded(child: child))
.toList(),
).padding(horizontal: 12).height(48);
} else {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
spacing: 8,
children:
children
.map(
(child) =>
SizedBox(width: minWidth, child: child),
)
.toList(),
children: children
.map(
(child) =>
SizedBox(width: minWidth, child: child),
)
.toList(),
).padding(horizontal: 12),
).height(48);
}
},
),
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.notifications),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Row(
children: [
Expanded(child: Text('notifications').tr()),
Badge.count(
count: notificationUnreadCount.value ?? 0,
isLabelVisible: (notificationUnreadCount.value ?? 0) > 0,
),
],
),
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => const NotificationSheet(),
Builder(
builder: (context) {
final menuItems = [
{
'icon': Symbols.notifications,
'title': 'notifications',
'badgeCount': notificationUnreadCount.value ?? 0,
'onTap': () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => const NotificationSheet(),
);
},
},
if (!isWideScreen(context))
{
'icon': Symbols.files,
'title': 'files',
'onTap': () {
context.goNamed('files');
},
},
if (!isWideScreen(context))
{
'icon': Symbols.groups_3,
'title': 'realms',
'onTap': () {
context.goNamed('realmList');
},
},
{
'icon': Symbols.wallet,
'title': 'wallet',
'onTap': () {
context.pushNamed('wallet');
},
},
{
'icon': Symbols.people,
'title': 'relationships',
'onTap': () {
context.pushNamed('relationships');
},
},
{
'icon': Symbols.sticker_rounded,
'title': 'stickers',
'onTap': () {
context.pushNamed('stickerMarketplace');
},
},
{
'icon': Symbols.rss_feed,
'title': 'webFeeds',
'onTap': () {
context.pushNamed('webFeedMarketplace');
},
},
{
'icon': Symbols.gavel,
'title': 'abuseReport',
'onTap': () {
context.pushNamed('reportList');
},
},
];
return Column(
children: menuItems.map((item) {
final icon = item['icon'] as IconData;
final title = item['title'] as String;
final badgeCount = item['badgeCount'] as int?;
final onTap = item['onTap'] as VoidCallback?;
return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
trailing: const Icon(Symbols.chevron_right),
dense: true,
leading: Badge(
isLabelVisible: badgeCount != null && badgeCount > 0,
label: Text(badgeCount.toString()),
child: Icon(icon, size: 24),
),
title: Text(title).tr(),
onTap: onTap,
);
}).toList(),
);
},
),
if (!isWideScreen(context))
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.files),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('files').tr(),
onTap: () {
context.goNamed('files');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.wallet),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('wallet').tr(),
onTap: () {
context.pushNamed('wallet');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.people),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('relationships').tr(),
onTap: () {
context.pushNamed('relationships');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.emoji_emotions),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('stickers').tr(),
onTap: () {
context.pushNamed('stickerMarketplace');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.rss_feed),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('webFeeds').tr(),
onTap: () {
context.pushNamed('webFeedMarketplace');
},
),
ListTile(
minTileHeight: 48,
title: Text('abuseReport').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.gavel),
trailing: const Icon(Symbols.chevron_right),
onTap: () => context.pushNamed('reportList'),
),
const Divider(height: 1).padding(vertical: 8),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.info),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
dense: true,
title: Text('about').tr(),
onTap: () {
context.pushNamed('about');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.bug_report),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('debugOptions').tr(),
dense: true,
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
@@ -463,11 +471,11 @@ class AccountScreen extends HookConsumerWidget {
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.logout),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('logout').tr(),
dense: true,
onTap: () async {
final ws = ref.watch(websocketStateProvider.notifier);
final apiClient = ref.watch(apiClientProvider);
@@ -495,97 +503,96 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: const Text('account').tr()),
body:
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
body: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Card(
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
context.pushNamed('createAccount');
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(Symbols.person_add, size: 48),
const SizedBox(height: 8),
Text('createAccount').tr().bold(),
Text('createAccountDescription').tr(),
],
),
),
),
),
),
const Gap(8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Card(
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
context.pushNamed('login');
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(Symbols.login, size: 48),
const SizedBox(height: 8),
Text('login').tr().bold(),
Text('loginDescription').tr(),
],
),
),
),
),
),
const Gap(8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Card(
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
context.pushNamed('createAccount');
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(Symbols.person_add, size: 48),
const SizedBox(height: 8),
Text('createAccount').tr().bold(),
Text('createAccountDescription').tr(),
],
),
),
),
),
children: [
IconButton(
onPressed: () {
context.pushNamed('about');
},
iconSize: 18,
color: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.info, fill: 1),
tooltip: 'about'.tr(),
),
const Gap(8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Card(
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
context.pushNamed('login');
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(Symbols.login, size: 48),
const SizedBox(height: 8),
Text('login').tr().bold(),
Text('loginDescription').tr(),
],
),
),
),
),
IconButton(
icon: const Icon(Icons.bug_report, fill: 1),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) => DebugSheet(),
);
},
iconSize: 18,
color: Theme.of(context).colorScheme.secondary,
tooltip: 'debugOptions'.tr(),
),
const Gap(8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {
context.pushNamed('about');
},
iconSize: 18,
color: Theme.of(context).colorScheme.secondary,
icon: const Icon(Icons.info, fill: 1),
tooltip: 'about'.tr(),
),
IconButton(
icon: const Icon(Icons.bug_report, fill: 1),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) => DebugSheet(),
);
},
iconSize: 18,
color: Theme.of(context).colorScheme.secondary,
tooltip: 'debugOptions'.tr(),
),
IconButton(
onPressed: () {
context.pushNamed('settings');
},
icon: const Icon(Icons.settings, fill: 1),
iconSize: 18,
color: Theme.of(context).colorScheme.secondary,
tooltip: 'appSettings'.tr(),
),
],
IconButton(
onPressed: () {
context.pushNamed('settings');
},
icon: const Icon(Icons.settings, fill: 1),
iconSize: 18,
color: Theme.of(context).colorScheme.secondary,
tooltip: 'appSettings'.tr(),
),
],
),
).center(),
],
),
).center(),
);
}
}

View File

@@ -135,81 +135,73 @@ class AccountSettingsScreen extends HookConsumerWidget {
ref
.watch(accountConnectionsProvider)
.when(
data:
(connections) => Column(
children: [
for (final connection in connections)
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 16,
right: 17,
top: 2,
bottom: 4,
),
title:
Text(
getLocalizedProviderName(connection.provider),
).tr(),
subtitle:
connection.meta['email'] != null
? Text(connection.meta['email'])
: Text(connection.providedIdentifier),
leading: CircleAvatar(
child: getProviderIcon(
connection.provider,
size: 16,
color:
Theme.of(
context,
).colorScheme.onPrimaryContainer,
),
).padding(top: 4),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
builder:
(context) => AccountConnectionSheet(
connection: connection,
),
).then((value) {
if (value == true) {
ref.invalidate(accountConnectionsProvider);
}
});
},
),
if (connections.isNotEmpty) const Divider(height: 1),
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 24,
right: 17,
),
title: Text('accountConnectionAdd').tr(),
leading: const Icon(Symbols.add),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
builder:
(context) =>
const AccountConnectionNewSheet(),
).then((value) {
if (value == true) {
ref.invalidate(accountConnectionsProvider);
}
});
},
data: (connections) => Column(
children: [
for (final connection in connections)
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 16,
right: 17,
top: 2,
bottom: 4,
),
],
),
error:
(err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(accountConnectionsProvider),
title: Text(
getLocalizedProviderName(connection.provider),
).tr(),
subtitle: connection.meta['email'] != null
? Text(connection.meta['email'])
: Text(connection.providedIdentifier),
leading: CircleAvatar(
child: getProviderIcon(
connection.provider,
size: 16,
color: Theme.of(
context,
).colorScheme.onPrimaryContainer,
),
).padding(top: 4),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) =>
AccountConnectionSheet(connection: connection),
).then((value) {
if (value == true) {
ref.invalidate(accountConnectionsProvider);
}
});
},
),
if (connections.isNotEmpty) const Divider(height: 1),
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 24,
right: 17,
),
title: Text('accountConnectionAdd').tr(),
leading: const Icon(Symbols.add),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) =>
const AccountConnectionNewSheet(),
).then((value) {
if (value == true) {
ref.invalidate(accountConnectionsProvider);
}
});
},
),
],
),
error: (err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(accountConnectionsProvider),
),
loading: () => const ResponseLoadingWidget(),
),
],
@@ -223,95 +215,76 @@ class AccountSettingsScreen extends HookConsumerWidget {
tilePadding: const EdgeInsets.only(left: 24, right: 17),
children: [
authFactors.when(
data:
(factors) => Column(
children: [
for (final factor in factors)
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 16,
right: 17,
top: 2,
bottom: 4,
),
title:
Text(
kFactorTypes[factor.type]!.$1,
style:
factor.enabledAt == null
? TextStyle(
decoration: TextDecoration.lineThrough,
)
: null,
).tr(),
subtitle:
Text(
kFactorTypes[factor.type]!.$2,
style:
factor.enabledAt == null
? TextStyle(
decoration: TextDecoration.lineThrough,
)
: null,
).tr(),
leading: CircleAvatar(
backgroundColor:
factor.enabledAt == null
? Theme.of(
context,
).colorScheme.secondaryContainer
: Theme.of(
context,
).colorScheme.primaryContainer,
child: Icon(kFactorTypes[factor.type]!.$3),
).padding(top: 4),
trailing: const Icon(Symbols.chevron_right),
isThreeLine: true,
onTap: () {
if (factor.type == 0) {
requestResetPassword();
return;
}
showModalBottomSheet(
context: context,
builder:
(context) => AuthFactorSheet(factor: factor),
).then((value) {
if (value == true) {
ref.invalidate(authFactorsProvider);
}
});
},
),
if (factors.isNotEmpty) Divider(height: 1),
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 24,
right: 17,
),
title: Text('authFactorNew').tr(),
leading: const Icon(Symbols.add),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => const AuthFactorNewSheet(),
).then((value) {
if (value == true) {
ref.invalidate(authFactorsProvider);
}
});
},
data: (factors) => Column(
children: [
for (final factor in factors)
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 16,
right: 17,
top: 2,
bottom: 4,
),
],
),
error:
(err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(authFactorsProvider),
title: Text(
kFactorTypes[factor.type]!.$1,
style: factor.enabledAt == null
? TextStyle(decoration: TextDecoration.lineThrough)
: null,
).tr(),
subtitle: Text(
kFactorTypes[factor.type]!.$2,
style: factor.enabledAt == null
? TextStyle(decoration: TextDecoration.lineThrough)
: null,
).tr(),
leading: CircleAvatar(
backgroundColor: factor.enabledAt == null
? Theme.of(context).colorScheme.secondaryContainer
: Theme.of(context).colorScheme.primaryContainer,
child: Icon(kFactorTypes[factor.type]!.$3),
).padding(top: 4),
trailing: const Icon(Symbols.chevron_right),
isThreeLine: true,
onTap: () {
if (factor.type == 0) {
requestResetPassword();
return;
}
showModalBottomSheet(
context: context,
builder: (context) => AuthFactorSheet(factor: factor),
).then((value) {
if (value == true) {
ref.invalidate(authFactorsProvider);
}
});
},
),
if (factors.isNotEmpty) Divider(height: 1),
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(left: 24, right: 17),
title: Text('authFactorNew').tr(),
leading: const Icon(Symbols.add),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => const AuthFactorNewSheet(),
).then((value) {
if (value == true) {
ref.invalidate(authFactorsProvider);
}
});
},
),
],
),
error: (err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(authFactorsProvider),
),
loading: () => ResponseLoadingWidget(),
),
],
@@ -327,97 +300,84 @@ class AccountSettingsScreen extends HookConsumerWidget {
ref
.watch(contactMethodsProvider)
.when(
data:
(contacts) => Column(
children: [
for (final contact in contacts)
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 16,
right: 17,
top: 2,
bottom: 4,
),
title: Text(
contact.content,
style:
contact.verifiedAt == null
? TextStyle(
decoration: TextDecoration.lineThrough,
)
: null,
),
subtitle: Text(
contact.type == 0
? 'contactMethodTypeEmail'.tr()
: 'contactMethodTypePhone'.tr(),
style:
contact.verifiedAt == null
? TextStyle(
decoration: TextDecoration.lineThrough,
)
: null,
),
leading: CircleAvatar(
backgroundColor:
contact.verifiedAt == null
? Theme.of(
context,
).colorScheme.secondaryContainer
: Theme.of(
context,
).colorScheme.primaryContainer,
child: Icon(
contact.type == 0
? Symbols.mail
: Symbols.phone,
),
).padding(top: 4),
trailing: const Icon(Symbols.chevron_right),
isThreeLine: false,
onTap: () {
showModalBottomSheet(
context: context,
builder:
(context) =>
ContactMethodSheet(contact: contact),
).then((value) {
if (value == true) {
ref.invalidate(contactMethodsProvider);
}
});
},
),
if (contacts.isNotEmpty) const Divider(height: 1),
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 24,
right: 17,
),
title: Text('contactMethodNew').tr(),
leading: const Icon(Symbols.add),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
builder:
(context) => const ContactMethodNewSheet(),
).then((value) {
if (value == true) {
ref.invalidate(contactMethodsProvider);
}
});
},
data: (contacts) => Column(
children: [
for (final contact in contacts)
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 16,
right: 17,
top: 2,
bottom: 4,
),
],
),
error:
(err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(contactMethodsProvider),
title: Text(
contact.content,
style: contact.verifiedAt == null
? TextStyle(
decoration: TextDecoration.lineThrough,
)
: null,
),
subtitle: Text(
contact.type == 0
? 'contactMethodTypeEmail'.tr()
: 'contactMethodTypePhone'.tr(),
style: contact.verifiedAt == null
? TextStyle(
decoration: TextDecoration.lineThrough,
)
: null,
),
leading: CircleAvatar(
backgroundColor: contact.verifiedAt == null
? Theme.of(context).colorScheme.secondaryContainer
: Theme.of(context).colorScheme.primaryContainer,
child: Icon(
contact.type == 0 ? Symbols.mail : Symbols.phone,
),
).padding(top: 4),
trailing: const Icon(Symbols.chevron_right),
isThreeLine: false,
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) =>
ContactMethodSheet(contact: contact),
).then((value) {
if (value == true) {
ref.invalidate(contactMethodsProvider);
}
});
},
),
if (contacts.isNotEmpty) const Divider(height: 1),
ListTile(
minLeadingWidth: 48,
contentPadding: const EdgeInsets.only(
left: 24,
right: 17,
),
title: Text('contactMethodNew').tr(),
leading: const Icon(Symbols.add),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => const ContactMethodNewSheet(),
).then((value) {
if (value == true) {
ref.invalidate(contactMethodsProvider);
}
});
},
),
],
),
error: (err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(contactMethodsProvider),
),
loading: () => const ResponseLoadingWidget(),
),
],
@@ -439,6 +399,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
// Create a responsive layout based on screen width
Widget buildSettingsList() {
return Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
@@ -450,38 +411,36 @@ class AccountSettingsScreen extends HookConsumerWidget {
children: dangerZoneSettings,
),
],
);
).padding(horizontal: 16);
}
return AppScaffold(
appBar: AppBar(
title: Text('accountSettings').tr(),
actions:
isDesktop
? [
IconButton(
icon: const Icon(Symbols.help_outline),
onPressed: () {
// Show help dialog
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('accountSettingsHelp').tr(),
content: Text('accountSettingsHelpContent').tr(),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close').tr(),
),
],
),
);
},
),
const Gap(8),
]
: null,
actions: isDesktop
? [
IconButton(
icon: const Icon(Symbols.help_outline),
onPressed: () {
// Show help dialog
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('accountSettingsHelp').tr(),
content: Text('accountSettingsHelpContent').tr(),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close').tr(),
),
],
),
);
},
),
const Gap(8),
]
: null,
),
body: Focus(
autofocus: true,
@@ -513,22 +472,25 @@ class _SettingsSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Text(
title.tr(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
return Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Text(
title.tr(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
),
...children,
const SizedBox(height: 16),
],
...children,
const SizedBox(height: 16),
],
),
);
}
}

View File

@@ -69,11 +69,11 @@ class CallScreen extends HookConsumerWidget {
callState.isConnected
? formatDuration(callState.duration)
: (switch (callNotifier.room?.connectionState) {
ConnectionState.connected => 'connected',
ConnectionState.connecting => 'connecting',
ConnectionState.reconnecting => 'reconnecting',
_ => 'disconnected',
}).tr(),
ConnectionState.connected => 'connected',
ConnectionState.connecting => 'connecting',
ConnectionState.reconnecting => 'reconnecting',
_ => 'disconnected',
}).tr(),
style: const TextStyle(fontSize: 14),
),
],
@@ -92,40 +92,40 @@ class CallScreen extends HookConsumerWidget {
),
],
),
body:
callState.error != null
? Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320),
child: Column(
children: [
const Icon(Symbols.error_outline, size: 48),
const Gap(4),
Text(
callState.error!,
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)),
),
const Gap(8),
TextButton(
onPressed: () {
callNotifier.disconnect();
callNotifier.dispose();
callNotifier.joinRoom(room);
},
child: Text('retry').tr(),
),
],
),
body: callState.error != null
? Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320),
child: Column(
children: [
const Icon(Symbols.error_outline, size: 48),
const Gap(4),
Text(
callState.error!,
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)),
),
const Gap(8),
TextButton(
onPressed: () {
callNotifier.disconnect();
callNotifier.dispose();
callNotifier.joinRoom(room);
},
child: Text('retry').tr(),
),
],
),
)
: Column(
children: [
Expanded(child: CallContent()),
CallControlsBar(),
Gap(MediaQuery.of(context).padding.bottom + 16),
],
),
)
: Column(
children: [
const SizedBox(width: double.infinity),
Expanded(child: CallContent()),
CallControlsBar(popOnLeaves: true),
Gap(MediaQuery.of(context).padding.bottom + 16),
],
),
);
}
}

View File

@@ -9,212 +9,23 @@ import 'package:island/models/chat.dart';
import 'package:island/pods/chat/chat_summary.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/chat/chat_form.dart';
import 'package:island/screens/realm/realms.dart';
import 'package:island/services/event_bus.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat_room_widgets.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/navigation/fab_menu.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:island/pods/chat/chat_room.dart';
class ChatRoomListTile extends HookConsumerWidget {
final SnChatRoom room;
final bool isDirect;
final Widget? subtitle;
final Widget? trailing;
final VoidCallback? onTap;
const ChatRoomListTile({
super.key,
required this.room,
this.isDirect = false,
this.subtitle,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final summary = ref
.watch(chatSummaryProvider)
.whenData((summaries) => summaries[room.id]);
var validMembers = room.members ?? [];
if (validMembers.isNotEmpty) {
final userInfo = ref.watch(userInfoProvider);
if (userInfo.value != null) {
validMembers = validMembers
.where((e) => e.accountId != userInfo.value!.id)
.toList();
}
}
Widget buildSubtitle() {
if (subtitle != null) return subtitle!;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
layoutBuilder: (currentChild, previousChildren) => Stack(
alignment: Alignment.centerLeft,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
),
child: summary.when(
data: (data) => Container(
key: const ValueKey('data'),
child: data == null
? isDirect && room.description == null
? Text(
validMembers
.map((e) => '@${e.account.name}')
.join(', '),
maxLines: 1,
)
: Text(
room.description ?? 'descriptionNone'.tr(),
maxLines: 1,
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (data.unreadCount > 0)
Text(
'unreadMessages'.plural(data.unreadCount),
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
if (data.lastMessage == null)
Text(
room.description ?? 'descriptionNone'.tr(),
maxLines: 1,
)
else
Row(
spacing: 4,
children: [
Badge(
label: Text(
data.lastMessage!.sender.account.nick,
),
textColor: Theme.of(
context,
).colorScheme.onPrimary,
backgroundColor: Theme.of(
context,
).colorScheme.primary,
),
Expanded(
child: Text(
(data.lastMessage!.content?.isNotEmpty ?? false)
? data.lastMessage!.content!
: 'messageNone'.tr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
),
Align(
alignment: Alignment.centerRight,
child: Text(
RelativeTime(
context,
).format(data.lastMessage!.createdAt),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
],
),
),
loading: () => Container(
key: const ValueKey('loading'),
child: Builder(
builder: (context) {
final seed = DateTime.now().microsecondsSinceEpoch;
final len = 4 + (seed % 17); // 4..20 inclusive
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
var s = seed;
final buffer = StringBuffer();
for (var i = 0; i < len; i++) {
s = (s * 1103515245 + 12345) & 0x7fffffff;
buffer.write(chars[s % chars.length]);
}
return Skeletonizer(
enabled: true,
child: Text(buffer.toString()),
);
},
),
),
error: (_, _) => Container(
key: const ValueKey('error'),
child: isDirect && room.description == null
? Text(
validMembers.map((e) => '@${e.account.name}').join(', '),
maxLines: 1,
)
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1),
),
),
);
}
String titleText;
if (isDirect && room.name == null) {
if (room.members?.isNotEmpty ?? false) {
titleText = validMembers.map((e) => e.account.nick).join(', ');
} else {
titleText = 'Direct Message';
}
} else {
titleText = room.name ?? '';
}
return ListTile(
leading: Badge(
isLabelVisible: summary.when(
data: (data) => (data?.unreadCount ?? 0) > 0,
loading: () => false,
error: (_, _) => false,
),
child: (isDirect && room.picture?.id == null)
? SplitAvatarWidget(
filesId: validMembers
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.picture?.id == null
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
: ProfilePictureWidget(fileId: room.picture?.id),
),
title: Text(titleText),
subtitle: buildSubtitle(),
trailing: trailing, // Add this line
onTap: () async {
// Clear unread count if there are unread messages
ref.read(chatSummaryProvider.future).then((summary) {
if ((summary[room.id]?.unreadCount ?? 0) > 0) {
ref.read(chatSummaryProvider.notifier).clearUnreadCount(room.id);
}
});
onTap?.call();
},
);
}
}
import 'package:island/pods/config.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
class ChatListBodyWidget extends HookConsumerWidget {
final bool isFloating;
@@ -231,6 +42,7 @@ class ChatListBodyWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final chats = ref.watch(chatRoomJoinedProvider);
final settings = ref.watch(appSettingsProvider);
Widget bodyWidget = Column(
children: [
@@ -248,50 +60,238 @@ class ChatListBodyWidget extends HookConsumerWidget {
),
Expanded(
child: chats.when(
data: (items) => RefreshIndicator(
onRefresh: () => Future.sync(() {
ref.invalidate(chatRoomJoinedProvider);
}),
child: SuperListView.builder(
padding: EdgeInsets.only(bottom: 96),
itemCount: items
.where(
(item) =>
selectedTab.value == 0 ||
(selectedTab.value == 1 && item.type == 1) ||
(selectedTab.value == 2 && item.type != 1),
)
.length,
itemBuilder: (context, index) {
final filteredItems = items
.where(
(item) =>
selectedTab.value == 0 ||
(selectedTab.value == 1 && item.type == 1) ||
(selectedTab.value == 2 && item.type != 1),
)
.toList();
final item = filteredItems[index];
return ChatRoomListTile(
room: item,
isDirect: item.type == 1,
onTap: () {
if (isWideScreen(context)) {
context.replaceNamed(
'chatRoom',
pathParameters: {'id': item.id},
);
} else {
context.pushNamed(
'chatRoom',
pathParameters: {'id': item.id},
);
}
},
);
},
),
),
data: (items) {
final filteredItems = items.where(
(item) =>
selectedTab.value == 0 ||
(selectedTab.value == 1 && item.type == 1) ||
(selectedTab.value == 2 && item.type != 1),
);
final pinnedItems = filteredItems
.where((item) => item.isPinned)
.toList();
final unpinnedItems = filteredItems
.where((item) => !item.isPinned)
.toList();
return ExtendedRefreshIndicator(
onRefresh: () => Future.sync(() {
ref.invalidate(chatRoomJoinedProvider);
}),
child: Theme(
data: Theme.of(
context,
).copyWith(dividerColor: Colors.transparent),
child: Column(
children: [
// Always show pinned chats in their own section
if (pinnedItems.isNotEmpty)
ExpansionTile(
backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withOpacity(0.5),
collapsedBackgroundColor: Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.5),
title: Text('pinnedChatRoom'.tr()),
leading: const Icon(Symbols.keep, fill: 1),
tilePadding: const EdgeInsets.symmetric(
horizontal: 24,
),
initiallyExpanded: true,
children: [
for (final item in pinnedItems)
ChatRoomListTile(
room: item,
isDirect: item.type == 1,
onTap: () {
if (isWideScreen(context)) {
context.replaceNamed(
'chatRoom',
pathParameters: {'id': item.id},
);
} else {
context.pushNamed(
'chatRoom',
pathParameters: {'id': item.id},
);
}
},
),
],
),
Expanded(
child: Consumer(
builder: (context, ref, _) {
final summaries =
ref
.watch(chatSummaryProvider)
.whenData((data) => data)
.value ??
{};
if (settings.groupedChatList &&
selectedTab.value == 0) {
// Group by realm (include both pinned and unpinned)
final realmGroups = <String?, List<SnChatRoom>>{};
final ungrouped = <SnChatRoom>[];
for (final item in filteredItems) {
if (item.realmId != null) {
realmGroups
.putIfAbsent(item.realmId, () => [])
.add(item);
} else if (!item.isPinned) {
// Only unpinned chats without realm go to ungrouped
ungrouped.add(item);
}
}
final children = <Widget>[];
// Add realm groups
for (final entry in realmGroups.entries) {
final rooms = entry.value;
final realm = rooms.first.realm;
final realmName =
realm?.name ?? 'Unknown Realm';
// Calculate total unread count for this realm
final totalUnread = rooms.fold<int>(
0,
(sum, room) =>
sum +
(summaries[room.id]?.unreadCount ?? 0),
);
children.add(
ExpansionTile(
backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withOpacity(0.5),
collapsedBackgroundColor:
Colors.transparent,
title: Row(
children: [
Expanded(child: Text(realmName)),
Badge(
isLabelVisible: totalUnread > 0,
label: Text(totalUnread.toString()),
backgroundColor: Theme.of(
context,
).colorScheme.primary,
textColor: Theme.of(
context,
).colorScheme.onPrimary,
),
],
),
leading: ProfilePictureWidget(
file: realm?.picture,
radius: 16,
),
tilePadding: const EdgeInsets.only(
left: 20,
right: 24,
),
children: rooms.map((room) {
return ChatRoomListTile(
room: room,
isDirect: room.type == 1,
onTap: () {
if (isWideScreen(context)) {
context.replaceNamed(
'chatRoom',
pathParameters: {'id': room.id},
);
} else {
context.pushNamed(
'chatRoom',
pathParameters: {'id': room.id},
);
}
},
);
}).toList(),
),
);
}
// Add ungrouped chats
if (ungrouped.isNotEmpty) {
children.addAll(
ungrouped.map((room) {
return ChatRoomListTile(
room: room,
isDirect: room.type == 1,
onTap: () {
if (isWideScreen(context)) {
context.replaceNamed(
'chatRoom',
pathParameters: {'id': room.id},
);
} else {
context.pushNamed(
'chatRoom',
pathParameters: {'id': room.id},
);
}
},
);
}),
);
}
return ListView(
padding: EdgeInsets.only(bottom: 96),
children: children,
);
} else {
// Normal list view
return SuperListView.builder(
padding: EdgeInsets.only(bottom: 96),
itemCount: unpinnedItems
.where(
(item) =>
selectedTab.value == 0 ||
(selectedTab.value == 1 &&
item.type == 1) ||
(selectedTab.value == 2 &&
item.type != 1),
)
.length,
itemBuilder: (context, index) {
final item = unpinnedItems[index];
return ChatRoomListTile(
room: item,
isDirect: item.type == 1,
onTap: () {
if (isWideScreen(context)) {
context.replaceNamed(
'chatRoom',
pathParameters: {'id': item.id},
);
} else {
context.pushNamed(
'chatRoom',
pathParameters: {'id': item.id},
);
}
},
);
},
);
}
},
),
),
],
),
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ResponseErrorWidget(
error: error,
@@ -381,23 +381,6 @@ class ChatListScreen extends HookConsumerWidget {
};
}, [tabController]);
useEffect(() {
// Set FAB type to chat
final fabMenuNotifier = ref.read(fabMenuTypeProvider.notifier);
Future(() {
fabMenuNotifier.setMenuType(FabMenuType.chat);
});
return () {
// Clean up: reset FAB type to main
final fabMenu = ref.read(fabMenuTypeProvider);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (fabMenu == FabMenuType.chat) {
fabMenuNotifier.setMenuType(FabMenuType.main);
}
});
};
}, []);
if (isAside) {
return Card(
margin: EdgeInsets.zero,
@@ -468,70 +451,153 @@ class ChatListScreen extends HookConsumerWidget {
return const EmptyPageHolder();
}
final appbarFeColor = Theme.of(context).appBarTheme.foregroundColor;
final userInfo = ref.watch(userInfoProvider);
return AppScaffold(
extendBody: false, // Prevent conflicts with tabs navigation
floatingActionButton: userInfo.value != null
? FloatingActionButton(
child: const Icon(Symbols.add),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Gap(40),
ListTile(
title: const Text('createChatRoom').tr(),
leading: const Icon(Symbols.add),
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
onTap: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) => const EditChatScreen(),
).then((value) {
if (value != null) {
eventBus.fire(const ChatRoomsRefreshEvent());
}
});
},
),
ListTile(
title: const Text('createDirectMessage').tr(),
leading: const Icon(Symbols.person),
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
onTap: () async {
final result = await showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) => const AccountPickerSheet(),
);
if (result == null) return;
final client = ref.read(apiClientProvider);
try {
await client.post(
'/sphere/chat/direct',
data: {'related_user_id': result.id},
);
eventBus.fire(const ChatRoomsRefreshEvent());
} catch (err) {
showErrorAlert(err);
}
},
),
const Gap(16),
],
),
);
},
).padding(bottom: isWideScreen(context) ? null : 56)
: null,
appBar: AppBar(
title: const Text('chat').tr(),
bottom: TabBar(
controller: tabController,
tabs: [
Tab(
child: Text(
'chatTabAll'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'chatTabDirect'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'chatTabGroup'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
actions: [
IconButton(
icon: Badge(
label: Text(
chatInvites.when(
data: (invites) => invites.length.toString(),
error: (_, _) => '0',
loading: () => '0',
),
),
isLabelVisible: chatInvites.when(
data: (invites) => invites.isNotEmpty,
error: (_, _) => false,
loading: () => false,
),
child: const Icon(Symbols.email),
),
onPressed: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) => const _ChatInvitesSheet(),
);
},
flexibleSpace: Container(
height: 48,
margin: EdgeInsets.only(
left: 8,
right: 8,
top: 4 + MediaQuery.of(context).padding.top,
bottom: 4,
),
const Gap(8),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
Expanded(
child: Row(
spacing: 8,
children: [
IconButton(
icon: Icon(
Symbols.inbox,
fill: tabController.index == 0 ? 1 : 0,
),
color: appbarFeColor,
onPressed: () => tabController.animateTo(0),
tooltip: 'chatTabAll'.tr(),
),
IconButton(
icon: Icon(
Symbols.person,
fill: tabController.index == 1 ? 1 : 0,
),
color: appbarFeColor,
onPressed: () => tabController.animateTo(1),
tooltip: 'chatTabDirect'.tr(),
),
IconButton(
icon: Icon(
Symbols.group,
fill: tabController.index == 2 ? 1 : 0,
),
color: appbarFeColor,
onPressed: () => tabController.animateTo(2),
tooltip: 'chatTabGroup'.tr(),
),
],
),
),
IconButton(
icon: Badge(
label: Text(
chatInvites.when(
data: (invites) => invites.length.toString(),
error: (_, _) => '0',
loading: () => '0',
),
),
isLabelVisible: chatInvites.when(
data: (invites) => invites.isNotEmpty,
error: (_, _) => false,
loading: () => false,
),
child: const Icon(Symbols.email),
),
color: appbarFeColor,
onPressed: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) => const _ChatInvitesSheet(),
);
},
),
],
),
),
),
),
body: ChatListBodyWidget(
isFloating: false,
@@ -631,3 +697,75 @@ class _ChatInvitesSheet extends HookConsumerWidget {
);
}
}
class ChatRoomListTile extends HookConsumerWidget {
final SnChatRoom room;
final bool isDirect;
final Widget? subtitle;
final Widget? trailing;
final VoidCallback? onTap;
const ChatRoomListTile({
super.key,
required this.room,
this.isDirect = false,
this.subtitle,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final summary = ref
.watch(chatSummaryProvider)
.whenData((summaries) => summaries[room.id]);
var validMembers = room.members ?? [];
if (validMembers.isNotEmpty) {
final userInfo = ref.watch(userInfoProvider);
if (userInfo.value != null) {
validMembers = validMembers
.where((e) => e.accountId != userInfo.value!.id)
.toList();
}
}
String titleText;
if (isDirect && room.name == null) {
if (room.members?.isNotEmpty ?? false) {
titleText = validMembers.map((e) => e.account.nick).join(', ');
} else {
titleText = 'Direct Message';
}
} else {
titleText = room.name ?? '';
}
return ListTile(
leading: ChatRoomAvatar(
room: room,
isDirect: isDirect,
summary: summary,
validMembers: validMembers,
),
title: Text(titleText),
subtitle: ChatRoomSubtitle(
room: room,
isDirect: isDirect,
validMembers: validMembers,
summary: summary,
subtitle: subtitle,
),
trailing: trailing, // Add this line
onTap: () async {
// Clear unread count if there are unread messages
ref.read(chatSummaryProvider.future).then((summary) {
if ((summary[room.id]?.unreadCount ?? 0) > 0) {
ref.read(chatSummaryProvider.notifier).clearUnreadCount(room.id);
}
});
onTap?.call();
},
);
}
}

View File

@@ -1,5 +1,6 @@
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:freezed_annotation/freezed_annotation.dart';
import 'package:gap/gap.dart';
@@ -42,6 +43,20 @@ class ChatDetailScreen extends HookConsumerWidget {
final roomIdentity = ref.watch(chatRoomIdentityProvider(id));
final totalMessages = ref.watch(totalMessagesCountProvider(id));
// Local state for pinned status to provide immediate UI feedback
final isPinned = useState<bool?>(null);
// Initialize pinned state from database
useEffect(() {
final db = ref.read(databaseProvider);
(db.select(
db.chatRooms,
)..where((r) => r.id.equals(id))).getSingleOrNull().then((room) {
isPinned.value = room?.isPinned ?? false;
});
return null;
}, [id]);
const kNotifyLevelText = [
'chatNotifyLevelAll',
'chatNotifyLevelMention',
@@ -83,46 +98,45 @@ class ChatDetailScreen extends HookConsumerWidget {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => SheetScaffold(
height: 320,
titleText: 'chatNotifyLevel'.tr(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('chatNotifyLevelAll').tr(),
subtitle: const Text('chatNotifyLevelDescription').tr(),
leading: const Icon(Icons.notifications_active),
selected: identity.notify == 0,
onTap: () {
setNotifyLevel(0);
Navigator.pop(context);
},
),
ListTile(
title: const Text('chatNotifyLevelMention').tr(),
subtitle: const Text('chatNotifyLevelDescription').tr(),
leading: const Icon(Icons.alternate_email),
selected: identity.notify == 1,
onTap: () {
setNotifyLevel(1);
Navigator.pop(context);
},
),
ListTile(
title: const Text('chatNotifyLevelNone').tr(),
subtitle: const Text('chatNotifyLevelDescription').tr(),
leading: const Icon(Icons.notifications_off),
selected: identity.notify == 2,
onTap: () {
setNotifyLevel(2);
Navigator.pop(context);
},
),
],
builder: (context) => SheetScaffold(
height: 320,
titleText: 'chatNotifyLevel'.tr(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('chatNotifyLevelAll').tr(),
subtitle: const Text('chatNotifyLevelDescription').tr(),
leading: const Icon(Icons.notifications_active),
selected: identity.notify == 0,
onTap: () {
setNotifyLevel(0);
Navigator.pop(context);
},
),
),
ListTile(
title: const Text('chatNotifyLevelMention').tr(),
subtitle: const Text('chatNotifyLevelDescription').tr(),
leading: const Icon(Icons.alternate_email),
selected: identity.notify == 1,
onTap: () {
setNotifyLevel(1);
Navigator.pop(context);
},
),
ListTile(
title: const Text('chatNotifyLevelNone').tr(),
subtitle: const Text('chatNotifyLevelDescription').tr(),
leading: const Icon(Icons.notifications_off),
selected: identity.notify == 2,
onTap: () {
setNotifyLevel(2);
Navigator.pop(context);
},
),
],
),
),
);
}
@@ -132,118 +146,117 @@ class ChatDetailScreen extends HookConsumerWidget {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('chatBreak').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('chatBreakDescription').tr(),
const Gap(16),
ListTile(
title: const Text('chatBreakClearButton').tr(),
subtitle: const Text('chatBreakClear').tr(),
leading: const Icon(Icons.notifications_active),
onTap: () {
setChatBreak(now);
Navigator.pop(context);
if (context.mounted) {
showSnackBar('chatBreakCleared'.tr());
}
},
),
ListTile(
title: const Text('chatBreak5m').tr(),
subtitle: const Text(
'chatBreakHour',
).tr(args: ['chatBreak5m'.tr()]),
leading: const Icon(Symbols.circle),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 5)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar('chatBreakSet'.tr(args: ['5m']));
}
},
),
ListTile(
title: const Text('chatBreak10m').tr(),
subtitle: const Text(
'chatBreakHour',
).tr(args: ['chatBreak10m'.tr()]),
leading: const Icon(Symbols.circle),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 10)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar('chatBreakSet'.tr(args: ['10m']));
}
},
),
ListTile(
title: const Text('chatBreak15m').tr(),
subtitle: const Text(
'chatBreakHour',
).tr(args: ['chatBreak15m'.tr()]),
leading: const Icon(Symbols.timer_3),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 15)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar('chatBreakSet'.tr(args: ['15m']));
}
},
),
ListTile(
title: const Text('chatBreak30m').tr(),
subtitle: const Text(
'chatBreakHour',
).tr(args: ['chatBreak30m'.tr()]),
leading: const Icon(Symbols.timer),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 30)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar('chatBreakSet'.tr(args: ['30m']));
}
},
),
const Gap(8),
TextField(
controller: durationController,
decoration: InputDecoration(
labelText: 'chatBreakCustomMinutes'.tr(),
hintText: 'chatBreakEnterMinutes'.tr(),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: const Icon(Icons.check),
onPressed: () {
final minutes = int.tryParse(durationController.text);
if (minutes != null && minutes > 0) {
setChatBreak(now.add(Duration(minutes: minutes)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar(
'chatBreakSet'.tr(args: ['${minutes}m']),
);
}
}
},
),
),
keyboardType: TextInputType.number,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
builder: (context) => AlertDialog(
title: const Text('chatBreak').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('chatBreakDescription').tr(),
const Gap(16),
ListTile(
title: const Text('chatBreakClearButton').tr(),
subtitle: const Text('chatBreakClear').tr(),
leading: const Icon(Icons.notifications_active),
onTap: () {
setChatBreak(now);
Navigator.pop(context);
if (context.mounted) {
showSnackBar('chatBreakCleared'.tr());
}
},
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('cancel').tr(),
ListTile(
title: const Text('chatBreak5m').tr(),
subtitle: const Text(
'chatBreakHour',
).tr(args: ['chatBreak5m'.tr()]),
leading: const Icon(Symbols.circle),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 5)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar('chatBreakSet'.tr(args: ['5m']));
}
},
),
ListTile(
title: const Text('chatBreak10m').tr(),
subtitle: const Text(
'chatBreakHour',
).tr(args: ['chatBreak10m'.tr()]),
leading: const Icon(Symbols.circle),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 10)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar('chatBreakSet'.tr(args: ['10m']));
}
},
),
ListTile(
title: const Text('chatBreak15m').tr(),
subtitle: const Text(
'chatBreakHour',
).tr(args: ['chatBreak15m'.tr()]),
leading: const Icon(Symbols.timer_3),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 15)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar('chatBreakSet'.tr(args: ['15m']));
}
},
),
ListTile(
title: const Text('chatBreak30m').tr(),
subtitle: const Text(
'chatBreakHour',
).tr(args: ['chatBreak30m'.tr()]),
leading: const Icon(Symbols.timer),
onTap: () {
setChatBreak(now.add(const Duration(minutes: 30)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar('chatBreakSet'.tr(args: ['30m']));
}
},
),
const Gap(8),
TextField(
controller: durationController,
decoration: InputDecoration(
labelText: 'chatBreakCustomMinutes'.tr(),
hintText: 'chatBreakEnterMinutes'.tr(),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: const Icon(Icons.check),
onPressed: () {
final minutes = int.tryParse(durationController.text);
if (minutes != null && minutes > 0) {
setChatBreak(now.add(Duration(minutes: minutes)));
Navigator.pop(context);
if (context.mounted) {
showSnackBar(
'chatBreakSet'.tr(args: ['${minutes}m']),
);
}
}
},
),
),
],
keyboardType: TextInputType.number,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('cancel').tr(),
),
],
),
);
}
@@ -256,175 +269,197 @@ class ChatDetailScreen extends HookConsumerWidget {
return AppScaffold(
body: roomState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => Center(
child: Text('errorGeneric'.tr(args: [error.toString()])),
),
data:
(currentRoom) => CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 180,
pinned: true,
leading: PageBackButton(shadows: [iconShadow]),
flexibleSpace: FlexibleSpaceBar(
background:
(currentRoom!.type == 1 &&
currentRoom.background?.id != null)
? CloudImageWidget(
fileId: currentRoom.background!.id,
)
: (currentRoom.type == 1 &&
currentRoom.members!.length == 1 &&
currentRoom
.members!
.first
.account
.profile
.background
?.id !=
null)
? CloudImageWidget(
fileId:
currentRoom
.members!
.first
.account
.profile
.background!
.id,
)
: currentRoom.background?.id != null
? CloudImageWidget(
fileId: currentRoom.background!.id,
fit: BoxFit.cover,
)
: Container(
color:
Theme.of(context).appBarTheme.backgroundColor,
),
title: Text(
(currentRoom.type == 1 && currentRoom.name == null)
? currentRoom.members!
.map((e) => e.account.nick)
.join(', ')
: currentRoom.name!,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
shadows: [iconShadow],
error: (error, _) =>
Center(child: Text('errorGeneric'.tr(args: [error.toString()]))),
data: (currentRoom) => CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 180,
pinned: true,
leading: PageBackButton(shadows: [iconShadow]),
flexibleSpace: FlexibleSpaceBar(
background:
(currentRoom!.type == 1 &&
currentRoom.background?.id != null)
? CloudImageWidget(fileId: currentRoom.background!.id)
: (currentRoom.type == 1 &&
currentRoom.members!.length == 1 &&
currentRoom
.members!
.first
.account
.profile
.background
?.id !=
null)
? CloudImageWidget(
fileId: currentRoom
.members!
.first
.account
.profile
.background!
.id,
)
: currentRoom.background?.id != null
? CloudImageWidget(
fileId: currentRoom.background!.id,
fit: BoxFit.cover,
)
: Container(
color: Theme.of(context).appBarTheme.backgroundColor,
),
),
title: Text(
(currentRoom.type == 1 && currentRoom.name == null)
? currentRoom.members!
.map((e) => e.account.nick)
.join(', ')
: currentRoom.name!,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
shadows: [iconShadow],
),
actions: [
IconButton(
icon: const Icon(Icons.people, shadows: [iconShadow]),
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => _ChatMemberListSheet(roomId: id),
);
},
),
_ChatRoomActionMenu(id: id, iconShadow: iconShadow),
const Gap(8),
],
),
SliverToBoxAdapter(
child: Column(
),
actions: [
IconButton(
icon: const Icon(Icons.people, shadows: [iconShadow]),
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => _ChatMemberListSheet(roomId: id),
);
},
),
_ChatRoomActionMenu(id: id, iconShadow: iconShadow),
const Gap(8),
],
),
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
currentRoom.description ?? 'descriptionNone'.tr(),
style: const TextStyle(fontSize: 16),
).padding(all: 24),
const Divider(height: 1),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
currentRoom.description ?? 'descriptionNone'.tr(),
style: const TextStyle(fontSize: 16),
).padding(all: 24),
const Divider(height: 1),
// Pin/Unpin Switch
if (isPinned.value != null)
SwitchListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 24),
secondary: Icon(
Symbols.push_pin,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
title: const Text('pinChatRoom').tr(),
subtitle: const Text('pinChatRoomDescription').tr(),
value: isPinned.value!,
onChanged: (value) async {
// Update local state immediately for instant UI feedback
isPinned.value = value;
final db = ref.read(databaseProvider);
await db.toggleChatRoomPinned(id);
// Re-verify the state from database in case of error
final room = await (db.select(
db.chatRooms,
)..where((r) => r.id.equals(id))).getSingleOrNull();
final actualPinned = room?.isPinned ?? false;
if (actualPinned != value) {
// Revert if database operation failed
isPinned.value = actualPinned;
}
showSnackBar(
value
? 'chatRoomPinned'.tr()
: 'chatRoomUnpinned'.tr(),
);
},
),
roomIdentity.when(
data:
(identity) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Symbols.notifications),
trailing: const Icon(Symbols.chevron_right),
title: const Text('chatNotifyLevel').tr(),
subtitle: Text(
kNotifyLevelText[identity!.notify].tr(),
),
onTap:
() =>
showNotifyLevelBottomSheet(identity),
),
ListTile(
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Icons.timer),
trailing: const Icon(Symbols.chevron_right),
title: const Text('chatBreak').tr(),
subtitle:
identity.breakUntil != null &&
identity.breakUntil!.isAfter(
DateTime.now(),
)
? Text(
DateFormat(
'yyyy-MM-dd HH:mm',
).format(identity.breakUntil!),
)
: const Text('chatBreakNone').tr(),
onTap: () => showChatBreakDialog(),
),
ListTile(
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Icons.search),
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()],
),
),
),
onTap: () async {
final result = await context.pushNamed(
'searchMessages',
pathParameters: {'id': id},
);
if (result is SearchMessagesResult) {
// Navigate back to room screen with message to jump to
if (context.mounted) {
context.pop(result);
}
}
},
),
],
data: (identity) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Symbols.notifications),
trailing: const Icon(Symbols.chevron_right),
title: const Text('chatNotifyLevel').tr(),
subtitle: Text(
kNotifyLevelText[identity!.notify].tr(),
),
onTap: () => showNotifyLevelBottomSheet(identity),
),
ListTile(
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Icons.timer),
trailing: const Icon(Symbols.chevron_right),
title: const Text('chatBreak').tr(),
subtitle:
identity.breakUntil != null &&
identity.breakUntil!.isAfter(
DateTime.now(),
)
? Text(
DateFormat(
'yyyy-MM-dd HH:mm',
).format(identity.breakUntil!),
)
: const Text('chatBreakNone').tr(),
onTap: () => showChatBreakDialog(),
),
ListTile(
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Icons.search),
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()]),
),
),
onTap: () async {
final result = await context.pushNamed(
'searchMessages',
pathParameters: {'id': id},
);
if (result is SearchMessagesResult) {
// Navigate back to room screen with message to jump to
if (context.mounted) {
context.pop(result);
}
}
},
),
],
),
error: (_, _) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
],
),
),
],
],
),
),
],
),
),
);
}
@@ -447,97 +482,94 @@ class _ChatRoomActionMenu extends HookConsumerWidget {
return PopupMenuButton(
icon: Icon(Icons.more_vert, shadows: [iconShadow]),
itemBuilder:
(context) => [
if (isManagable)
PopupMenuItem(
onTap: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) => EditChatScreen(id: id),
).then((value) {
if (value != null) {
// Invalidate to refresh room data after edit
ref.invalidate(chatMemberListProvider(id));
}
});
},
child: Row(
children: [
Icon(
Icons.edit,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
const Gap(12),
const Text('editChatRoom').tr(),
],
itemBuilder: (context) => [
if (isManagable)
PopupMenuItem(
onTap: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) => EditChatScreen(id: id),
).then((value) {
if (value != null) {
// Invalidate to refresh room data after edit
ref.invalidate(chatMemberListProvider(id));
}
});
},
child: Row(
children: [
Icon(
Icons.edit,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
if (isManagable)
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.delete, color: Colors.red),
const Gap(12),
const Text(
'deleteChatRoom',
style: TextStyle(color: Colors.red),
).tr(),
],
const Gap(12),
const Text('editChatRoom').tr(),
],
),
),
if (isManagable)
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.delete, color: Colors.red),
const Gap(12),
const Text(
'deleteChatRoom',
style: TextStyle(color: Colors.red),
).tr(),
],
),
onTap: () {
showConfirmAlert(
'deleteChatRoomHint'.tr(),
'deleteChatRoom'.tr(),
isDanger: true,
).then((confirm) async {
if (confirm) {
final client = ref.watch(apiClientProvider);
await client.delete('/sphere/chat/$id');
ref.invalidate(chatRoomJoinedProvider);
if (context.mounted) {
context.pop();
}
}
});
},
)
else
PopupMenuItem(
child: Row(
children: [
Icon(
Icons.exit_to_app,
color: Theme.of(context).colorScheme.error,
),
onTap: () {
showConfirmAlert(
'deleteChatRoomHint'.tr(),
'deleteChatRoom'.tr(),
isDanger: true,
).then((confirm) async {
if (confirm) {
final client = ref.watch(apiClientProvider);
await client.delete('/sphere/chat/$id');
ref.invalidate(chatRoomJoinedProvider);
if (context.mounted) {
context.pop();
}
}
});
},
)
else
PopupMenuItem(
child: Row(
children: [
Icon(
Icons.exit_to_app,
color: Theme.of(context).colorScheme.error,
),
const Gap(12),
Text(
'leaveChatRoom',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
).tr(),
],
),
onTap: () {
showConfirmAlert(
'leaveChatRoomHint'.tr(),
'leaveChatRoom'.tr(),
).then((confirm) async {
if (confirm) {
final client = ref.watch(apiClientProvider);
await client.delete('/sphere/chat/$id/members/me');
ref.invalidate(chatRoomJoinedProvider);
if (context.mounted) {
context.pop();
}
}
});
},
),
],
const Gap(12),
Text(
'leaveChatRoom',
style: TextStyle(color: Theme.of(context).colorScheme.error),
).tr(),
],
),
onTap: () {
showConfirmAlert(
'leaveChatRoomHint'.tr(),
'leaveChatRoom'.tr(),
).then((confirm) async {
if (confirm) {
final client = ref.watch(apiClientProvider);
await client.delete('/sphere/chat/$id/members/me');
ref.invalidate(chatRoomJoinedProvider);
if (context.mounted) {
context.pop();
}
}
});
},
),
],
);
}
}
@@ -576,11 +608,10 @@ class ChatMemberListNotifier extends AsyncNotifier<List<SnChatMember>>
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final members =
response.data
.map((e) => SnChatMember.fromJson(e))
.cast<SnChatMember>()
.toList();
final members = response.data
.map((e) => SnChatMember.fromJson(e))
.cast<SnChatMember>()
.toList();
return members;
}

View File

@@ -35,7 +35,7 @@ Future<List<SnPublisher>> publishersManaged(Ref ref) async {
}
@riverpod
Future<SnPublisher?> publisher(Ref ref, String? identifier) async {
Future<SnPublisher?> publisherNullable(Ref ref, String? identifier) async {
if (identifier == null) return null;
final client = ref.watch(apiClientProvider);
final resp = await client.get('/sphere/publishers/$identifier');
@@ -93,14 +93,10 @@ class EditPublisherScreen extends HookConsumerWidget {
submitting.value = true;
try {
final cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
).future;
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(data: result, type: UniversalFileType.image),
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
@@ -118,7 +114,7 @@ class EditPublisherScreen extends HookConsumerWidget {
}
}
final publisher = ref.watch(publisherProvider(name));
final publisher = ref.watch(publisherNullableProvider(name));
final formKey = useMemoized(GlobalKey<FormState>.new, const []);
final nameController = useTextEditingController(
@@ -155,8 +151,8 @@ class EditPublisherScreen extends HookConsumerWidget {
final resp = await client.request(
'/sphere${name == null
? currentRealm.value == null
? '/publishers/individual'
: '/publishers/organization/${currentRealm.value!.slug}'
? '/publishers/individual'
: '/publishers/organization/${currentRealm.value!.slug}'
: '/publishers/$name'}',
data: {
'name': nameController.text,
@@ -194,13 +190,12 @@ class EditPublisherScreen extends HookConsumerWidget {
GestureDetector(
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudImageWidget(
fileId: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
child: background.value != null
? CloudImageWidget(
fileId: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
@@ -238,14 +233,14 @@ class EditPublisherScreen extends HookConsumerWidget {
prefixText: '@',
),
readOnly: name != null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
TextFormField(
controller: nickController,
decoration: InputDecoration(labelText: 'nickname'.tr()),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
TextFormField(
controller: bioController,
@@ -255,8 +250,8 @@ class EditPublisherScreen extends HookConsumerWidget {
),
minLines: 3,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
DropdownButtonFormField<SnRealm>(
value: currentRealm.value,
@@ -267,22 +262,20 @@ class EditPublisherScreen extends HookConsumerWidget {
child: Text('individual'.tr()),
),
...joinedRealms.maybeWhen(
data:
(realms) => realms.map(
(realm) => DropdownMenuItem(
value: realm,
child: Text(realm.name),
),
),
data: (realms) => realms.map(
(realm) => DropdownMenuItem(
value: realm,
child: Text(realm.name),
),
),
orElse: () => [],
),
],
onChanged:
joinedRealms.isLoading
? null
: (SnRealm? value) {
currentRealm.value = value;
},
onChanged: joinedRealms.isLoading
? null
: (SnRealm? value) {
currentRealm.value = value;
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -307,20 +300,18 @@ class EditPublisherScreen extends HookConsumerWidget {
currentRealm.value!.background?.id;
}
},
label:
Text(
currentRealm.value == null
? 'syncPublisher'
: 'syncPublisherRealm',
).tr(),
label: Text(
currentRealm.value == null
? 'syncPublisher'
: 'syncPublisherRealm',
).tr(),
icon: const Icon(Symbols.link),
),
TextButton.icon(
onPressed: submitting.value ? null : performAction,
label:
Text(
name == null ? 'create' : 'saveChanges',
).tr(),
label: Text(
name == null ? 'create' : 'saveChanges',
).tr(),
icon: const Icon(Symbols.save),
),
],

View File

@@ -50,10 +50,10 @@ final class PublishersManagedProvider
String _$publishersManagedHash() => r'ea83759fed9bd5119738b4d09f12b4476959e0a3';
@ProviderFor(publisher)
const publisherProvider = PublisherFamily._();
@ProviderFor(publisherNullable)
const publisherNullableProvider = PublisherNullableFamily._();
final class PublisherProvider
final class PublisherNullableProvider
extends
$FunctionalProvider<
AsyncValue<SnPublisher?>,
@@ -61,23 +61,23 @@ final class PublisherProvider
FutureOr<SnPublisher?>
>
with $FutureModifier<SnPublisher?>, $FutureProvider<SnPublisher?> {
const PublisherProvider._({
required PublisherFamily super.from,
const PublisherNullableProvider._({
required PublisherNullableFamily super.from,
required String? super.argument,
}) : super(
retry: null,
name: r'publisherProvider',
name: r'publisherNullableProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$publisherHash();
String debugGetCreateSourceHash() => _$publisherNullableHash();
@override
String toString() {
return r'publisherProvider'
return r'publisherNullableProvider'
''
'($argument)';
}
@@ -91,12 +91,12 @@ final class PublisherProvider
@override
FutureOr<SnPublisher?> create(Ref ref) {
final argument = this.argument as String?;
return publisher(ref, argument);
return publisherNullable(ref, argument);
}
@override
bool operator ==(Object other) {
return other is PublisherProvider && other.argument == argument;
return other is PublisherNullableProvider && other.argument == argument;
}
@override
@@ -105,22 +105,22 @@ final class PublisherProvider
}
}
String _$publisherHash() => r'18fb5c6b3d79dd8af4fbee108dec1a0e8a034038';
String _$publisherNullableHash() => r'49b28083a2f351c5e5cde0b1a97f6c7503969041';
final class PublisherFamily extends $Family
final class PublisherNullableFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<SnPublisher?>, String?> {
const PublisherFamily._()
const PublisherNullableFamily._()
: super(
retry: null,
name: r'publisherProvider',
name: r'publisherNullableProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
PublisherProvider call(String? identifier) =>
PublisherProvider._(argument: identifier, from: this);
PublisherNullableProvider call(String? identifier) =>
PublisherNullableProvider._(argument: identifier, from: this);
@override
String toString() => r'publisherProvider';
String toString() => r'publisherNullableProvider';
}

View File

@@ -2,9 +2,11 @@ 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/publication_site.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/sites/site_detail.dart';
import 'package:island/screens/creators/sites/site_list.dart';
import 'package:island/screens/creators/sites/widgets/site_config_form.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
@@ -23,6 +25,7 @@ class SiteForm extends HookConsumerWidget {
TextEditingController nameController,
TextEditingController descriptionController,
ValueNotifier<int> modeController,
ValueNotifier<SnPublicationSiteConfig> configController,
Function() saveSite,
Function() deleteSite,
String siteSlug,
@@ -103,38 +106,40 @@ class SiteForm extends HookConsumerWidget {
}
},
),
const SizedBox(height: 16),
SiteConfigForm(
initialConfig: configController.value,
onChanged: (value) => configController.value = value,
),
],
).padding(all: 20);
return SheetScaffold(
titleText: 'editPublicationSite'.tr(),
child: Builder(
builder:
(context) => SingleChildScrollView(
child: Column(
builder: (context) => SingleChildScrollView(
child: Column(
children: [
Form(key: formKey, child: formFields),
Row(
children: [
Form(key: formKey, child: formFields),
Row(
children: [
TextButton.icon(
onPressed: deleteSite,
icon: const Icon(Symbols.delete_forever),
label: Text('deletePublicationSite'.tr()),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
).alignment(Alignment.centerRight),
const Spacer(),
TextButton.icon(
onPressed: saveSite,
icon: const Icon(Symbols.save),
label: Text('saveChanges').tr(),
),
],
).padding(horizontal: 20, vertical: 12),
TextButton.icon(
onPressed: deleteSite,
icon: const Icon(Symbols.delete_forever),
label: Text('deletePublicationSite'.tr()),
style: TextButton.styleFrom(foregroundColor: Colors.red),
).alignment(Alignment.centerRight),
const Spacer(),
TextButton.icon(
onPressed: saveSite,
icon: const Icon(Symbols.save),
label: Text('saveChanges').tr(),
),
],
),
),
).padding(horizontal: 20, vertical: 12),
],
),
),
),
);
}
@@ -146,6 +151,9 @@ class SiteForm extends HookConsumerWidget {
final nameController = useTextEditingController();
final descriptionController = useTextEditingController();
final modeController = useState<int>(0); // Default to fully managed (0)
final configController = useState<SnPublicationSiteConfig>(
const SnPublicationSiteConfig(),
);
final isLoading = useState(false);
final saveSite = useCallback(() async {
@@ -162,6 +170,7 @@ class SiteForm extends HookConsumerWidget {
'mode': modeController.value,
if (descriptionController.text.isNotEmpty)
'description': descriptionController.text,
'config': configController.value.toJson(),
};
if (siteSlug != null) {
@@ -229,40 +238,39 @@ class SiteForm extends HookConsumerWidget {
nameController.text = site.name;
descriptionController.text = site.description ?? '';
modeController.value = site.mode ?? 0;
configController.value = site.config;
}
return null;
}, [siteAsync]);
// Handle loading and error states for editing using AsyncValue
return siteAsync.when(
data:
(_) => _buildForm(
formKey,
slugController,
nameController,
descriptionController,
modeController,
saveSite,
deleteSite,
editingSiteSlug,
),
loading:
() => SheetScaffold(
titleText: 'editPublicationSite'.tr(),
child: Center(child: CircularProgressIndicator()),
),
error:
(error, _) => SheetScaffold(
titleText: 'editPublicationSite'.tr(),
child: ResponseErrorWidget(
error: error.toString(),
onRetry: () {
ref.invalidate(
publicationSiteDetailProvider(pubName, editingSiteSlug),
);
},
),
),
data: (_) => _buildForm(
formKey,
slugController,
nameController,
descriptionController,
modeController,
configController,
saveSite,
deleteSite,
editingSiteSlug,
),
loading: () => SheetScaffold(
titleText: 'editPublicationSite'.tr(),
child: Center(child: CircularProgressIndicator()),
),
error: (error, _) => SheetScaffold(
titleText: 'editPublicationSite'.tr(),
child: ResponseErrorWidget(
error: error.toString(),
onRetry: () {
ref.invalidate(
publicationSiteDetailProvider(pubName, editingSiteSlug),
);
},
),
),
);
}
@@ -344,6 +352,11 @@ class SiteForm extends HookConsumerWidget {
}
},
),
const SizedBox(height: 16),
SiteConfigForm(
initialConfig: configController.value,
onChanged: (value) => configController.value = value,
),
],
).padding(all: 20);
@@ -354,10 +367,9 @@ class SiteForm extends HookConsumerWidget {
).padding(horizontal: 20, vertical: 12);
return SheetScaffold(
titleText:
siteSlug == null
? 'newPublicationSite'.tr()
: 'editPublicationSite'.tr(),
titleText: siteSlug == null
? 'newPublicationSite'.tr()
: 'editPublicationSite'.tr(),
child: SingleChildScrollView(
child: Column(
children: [

View File

@@ -0,0 +1,172 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:island/models/publication_site.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class SiteConfigForm extends HookWidget {
final SnPublicationSiteConfig? initialConfig;
final ValueChanged<SnPublicationSiteConfig> onChanged;
const SiteConfigForm({
super.key,
this.initialConfig,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final styleOverrideController = useTextEditingController(
text: initialConfig?.styleOverride,
);
final navItems = useState<List<SnPublicationSiteNavItems>>(
initialConfig?.navItems ?? [],
);
useEffect(() {
void listener() {
onChanged(
SnPublicationSiteConfig(
styleOverride: styleOverrideController.text,
navItems: navItems.value,
),
);
}
styleOverrideController.addListener(listener);
return () => styleOverrideController.removeListener(listener);
}, [styleOverrideController, navItems.value]);
return Card(
margin: EdgeInsets.zero,
child: ExpansionTile(
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
title: Text('siteConfig'.tr()),
children: [
Column(
spacing: 8,
children: [
TextFormField(
controller: styleOverrideController,
decoration: InputDecoration(
labelText: 'siteConfigStyleOverride'.tr(),
hintText: "You can paste your CSS here...",
alignLabelWithHint: true,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
minLines: 3,
maxLines: null,
).padding(bottom: 8),
Row(
children: [
Text('siteConfigNavItems'.tr()).bold(),
const Spacer(),
TextButton.icon(
onPressed: () {
navItems.value = [
...navItems.value,
const SnPublicationSiteNavItems(label: '', href: ''),
];
// Trigger update manually as list mutation doesn't trigger controller listener
onChanged(
SnPublicationSiteConfig(
styleOverride: styleOverrideController.text,
navItems: navItems.value,
),
);
},
icon: const Icon(Symbols.add),
label: Text('siteConfigAddNavItem'.tr()),
),
],
).padding(bottom: 4),
if (navItems.value.isEmpty)
Text('dataEmpty'.tr()).center().padding(vertical: 20),
...navItems.value.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
borderRadius: BorderRadius.circular(10),
),
child: Column(
spacing: 12,
children: [
TextFormField(
initialValue: item.label,
decoration: InputDecoration(
labelText: 'siteConfigNavItemLabel'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onChanged: (value) {
final newItems = [...navItems.value];
newItems[index] = item.copyWith(label: value);
navItems.value = newItems;
onChanged(
SnPublicationSiteConfig(
styleOverride: styleOverrideController.text,
navItems: newItems,
),
);
},
),
TextFormField(
initialValue: item.href,
decoration: InputDecoration(
labelText: 'siteConfigNavItemHref'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onChanged: (value) {
final newItems = [...navItems.value];
newItems[index] = item.copyWith(href: value);
navItems.value = newItems;
onChanged(
SnPublicationSiteConfig(
styleOverride: styleOverrideController.text,
navItems: newItems,
),
);
},
),
TextButton.icon(
onPressed: () {
final newItems = [...navItems.value];
newItems.removeAt(index);
navItems.value = newItems;
onChanged(
SnPublicationSiteConfig(
styleOverride: styleOverrideController.text,
navItems: newItems,
),
);
},
icon: const Icon(Symbols.delete),
label: Text('delete'.tr()),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
).alignment(Alignment.centerRight),
],
).padding(horizontal: 16, vertical: 20),
);
}),
],
).padding(all: 16),
],
),
);
}
}

View File

@@ -0,0 +1,557 @@
import 'dart:math' as math;
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:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/chat/chat_room.dart';
import 'package:island/pods/chat/chat_summary.dart';
import 'package:island/pods/event_calendar.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/chat/chat.dart';
import 'package:island/services/event_bus.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/fortune_graph.dart';
import 'package:island/widgets/account/friends_overview.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/notification_tile.dart';
import 'package:island/widgets/post/post_featured.dart';
import 'package:island/widgets/check_in.dart';
import 'package:island/models/activity.dart';
import 'package:island/screens/notification.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:slide_countdown/slide_countdown.dart';
import 'package:styled_widget/styled_widget.dart';
import 'dart:async';
class DashboardScreen extends HookConsumerWidget {
const DashboardScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return AppScaffold(
isNoBackground: false,
body: Center(child: DashboardGrid()),
);
}
}
class DashboardGrid extends HookConsumerWidget {
const DashboardGrid({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isWide = isWideScreen(context);
final devicePadding = MediaQuery.paddingOf(context);
final userInfo = ref.watch(userInfoProvider);
return Container(
constraints: BoxConstraints(
maxHeight: isWide
? math.min(640, MediaQuery.sizeOf(context).height * 0.65)
: MediaQuery.sizeOf(context).height,
),
padding: isWide
? EdgeInsets.only(top: devicePadding.top)
: EdgeInsets.only(top: 24 + devicePadding.top),
child: Column(
spacing: 16,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Clock card spans full width
if (isWide)
ClockCard().padding(horizontal: 24)
else
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Gap(8),
Expanded(child: ClockCard(compact: true)),
IconButton(
onPressed: () {
eventBus.fire(CommandPaletteTriggerEvent());
},
icon: const Icon(Symbols.search),
tooltip: 'searchAnything'.tr(),
),
],
).padding(horizontal: 24),
// Row with two cards side by side
if (isWide)
Padding(
padding: EdgeInsets.symmetric(horizontal: isWide ? 24 : 16),
child: SearchBar(
hintText: 'searchAnything'.tr(),
constraints: const BoxConstraints(minHeight: 56),
leading: const Icon(Symbols.search).padding(horizontal: 24),
readOnly: true,
onTap: () {
eventBus.fire(CommandPaletteTriggerEvent());
},
),
),
if (userInfo.value != null)
Expanded(
child:
SingleChildScrollView(
padding: isWide
? const EdgeInsets.symmetric(horizontal: 24)
: EdgeInsets.only(
bottom: 64 + devicePadding.bottom,
),
scrollDirection: isWide
? Axis.horizontal
: Axis.vertical,
child: isWide
? _DashboardGridWide()
: _DashboardGridNarrow(),
)
.clipRRect(
topLeft: isWide ? 0 : 12,
topRight: isWide ? 0 : 12,
)
.padding(horizontal: isWide ? 0 : 16),
),
],
),
);
}
}
class _DashboardGridWide extends HookConsumerWidget {
const _DashboardGridWide();
@override
Widget build(BuildContext context, WidgetRef ref) {
final userInfo = ref.watch(userInfoProvider);
return Row(
spacing: 16,
children: [
if (userInfo.value != null && userInfo.value?.activatedAt == null)
SizedBox(width: 400, child: AccountUnactivatedCard()),
SizedBox(
width: 400,
child: Column(
spacing: 16,
children: [
CheckInWidget(margin: EdgeInsets.zero),
Card(
margin: EdgeInsets.zero,
child: FortuneGraphWidget(
events: ref.watch(
eventCalendarProvider(
EventCalendarQuery(
uname: 'me',
year: DateTime.now().year,
month: DateTime.now().month,
),
),
),
),
),
Expanded(child: FortuneCard()),
],
),
),
SizedBox(width: 400, child: PostFeaturedList(collapsable: false)),
SizedBox(
width: 400,
child: Column(
spacing: 16,
children: [
FriendsOverviewWidget(),
Expanded(child: NotificationsCard()),
],
),
),
SizedBox(
width: 400,
child: Column(
spacing: 16,
children: [Expanded(child: ChatListCard())],
),
),
],
);
}
}
class _DashboardGridNarrow extends HookConsumerWidget {
const _DashboardGridNarrow();
@override
Widget build(BuildContext context, WidgetRef ref) {
final userInfo = ref.watch(userInfoProvider);
return Column(
spacing: 16,
children: [
if (userInfo.value != null && userInfo.value?.activatedAt == null)
AccountUnactivatedCard(),
CheckInWidget(margin: EdgeInsets.zero),
FortuneCard(),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
child: PostFeaturedList(),
),
FriendsOverviewWidget(),
NotificationsCard(),
ChatListCard(),
Card(
margin: EdgeInsets.zero,
child: FortuneGraphWidget(
events: ref.watch(
eventCalendarProvider(
EventCalendarQuery(
uname: 'me',
year: DateTime.now().year,
month: DateTime.now().month,
),
),
),
),
),
],
);
}
}
class ClockCard extends HookConsumerWidget {
final bool compact;
const ClockCard({super.key, this.compact = false});
@override
Widget build(BuildContext context, WidgetRef ref) {
final time = useState(DateTime.now());
final timer = useRef<Timer?>(null);
final notableDay = ref.watch(recentNotableDayProvider);
// Determine icon based on time of day
final int hour = time.value.hour;
final IconData timeIcon = (hour >= 6 && hour < 18)
? Symbols.sunny_rounded
: Symbols.dark_mode_rounded;
useEffect(() {
timer.value = Timer.periodic(const Duration(seconds: 1), (_) {
time.value = DateTime.now();
});
return () => timer.value?.cancel();
}, []);
return Card(
elevation: 0,
margin: EdgeInsets.zero,
color: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: Padding(
padding: compact
? EdgeInsets.zero
: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
timeIcon,
size: 32,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.ideographic,
children: [
Flexible(
child: Text(
'${time.value.hour.toString().padLeft(2, '0')}:${time.value.minute.toString().padLeft(2, '0')}:${time.value.second.toString().padLeft(2, '0')}',
style: GoogleFonts.robotoMono(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
Flexible(
child: Text(
'${time.value.month.toString().padLeft(2, '0')}/${time.value.day.toString().padLeft(2, '0')}',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
),
],
),
Row(
spacing: 5,
children: [
notableDay.when(
data: (day) => _buildNotableDayText(context, day!),
error: (err, _) =>
Text(err.toString()).fontSize(12),
loading: () =>
const Text('loading').tr().fontSize(12),
),
],
),
],
),
),
],
),
],
),
),
);
}
Widget _buildNotableDayText(BuildContext context, SnNotableDay notableDay) {
final today = DateTime.now();
final isToday =
notableDay.date.year == today.year &&
notableDay.date.month == today.month &&
notableDay.date.day == today.day;
if (isToday) {
return Row(
spacing: 5,
children: [
Text('notableDayToday').tr(args: [notableDay.localName]).fontSize(12),
Icon(
Symbols.celebration_rounded,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
],
);
} else {
return Row(
spacing: 5,
children: [
Text('notableDayNext').tr(args: [notableDay.localName]).fontSize(12),
SlideCountdown(
decoration: const BoxDecoration(),
style: const TextStyle(fontSize: 12),
separatorStyle: const TextStyle(fontSize: 12),
padding: EdgeInsets.zero,
duration: notableDay.date.difference(DateTime.now()),
),
],
);
}
}
}
class NotificationsCard extends HookConsumerWidget {
const NotificationsCard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final notifications = ref.watch(notificationListProvider);
final notificationsUnreadCount = ref.watch(notificationUnreadCountProvider);
return Card(
margin: EdgeInsets.zero,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(12)),
onTap: () {
// Show notification sheet similar to explore.dart
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => const NotificationSheet(),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Symbols.notifications,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'notifications'.tr(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
Badge.count(
count: notificationsUnreadCount.value ?? 0,
isLabelVisible: (notificationsUnreadCount.value ?? 0) > 0,
),
],
).padding(horizontal: 16, vertical: 12),
notifications.when(
loading: () => const SkeletonNotificationTile(),
error: (error, stack) => Center(child: Text('Error: $error')),
data: (notificationList) {
if (notificationList.isEmpty) {
return Center(child: Text('noNotificationsYet').tr());
}
// Get the most recent notification (first in the list)
final recentNotification = notificationList.first;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'mostRecent'.tr(),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
).padding(horizontal: 16),
const SizedBox(height: 8),
NotificationTile(
notification: recentNotification,
compact: true,
contentPadding: EdgeInsets.symmetric(horizontal: 16),
avatarRadius: 16.0,
),
],
);
},
),
Text(
'tapToViewAllNotifications'.tr(),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
).padding(horizontal: 16, vertical: 8),
],
),
),
);
}
}
class ChatListCard extends HookConsumerWidget {
const ChatListCard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final chatRooms = ref.watch(chatRoomJoinedProvider);
final chatUnreadCount = ref.watch(chatUnreadCountProvider);
return Card(
margin: EdgeInsets.zero,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Symbols.chat,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'recentChats'.tr(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
Badge.count(
count: chatUnreadCount.value ?? 0,
isLabelVisible: (chatUnreadCount.value ?? 0) > 0,
),
],
).padding(horizontal: 16, vertical: 16),
chatRooms.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
data: (rooms) {
if (rooms.isEmpty) {
return const Center(child: Text('No chat rooms available'));
}
// Take only the first 5 rooms
final recentRooms = rooms.take(5).toList();
return Column(
children: recentRooms.map((room) {
return ChatRoomListTile(
room: room,
isDirect: room.type == 1,
onTap: () {
context.pushNamed(
'chatRoom',
pathParameters: {'id': room.id},
);
},
);
}).toList(),
);
},
),
],
),
);
}
}
class FortuneCard extends HookConsumerWidget {
const FortuneCard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final fortuneAsync = ref.watch(randomFortuneSayingProvider);
return Card(
margin: EdgeInsets.zero,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: fortuneAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
data: (fortune) {
return Row(
spacing: 8,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
fortune.content,
maxLines: 2,
overflow: TextOverflow.fade,
),
),
Text('—— ${fortune.source}').bold(),
],
).padding(horizontal: 16);
},
),
).height(48);
}
}

View File

@@ -16,17 +16,14 @@ import 'package:island/pods/userinfo.dart';
import 'package:island/screens/auth/login_modal.dart';
import 'package:island/screens/notification.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/friends_overview.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/models/post.dart';
import 'package:island/widgets/check_in.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
import 'package:island/widgets/navigation/fab_menu.dart';
import 'package:island/widgets/paging/pagination_list.dart';
import 'package:island/widgets/post/post_featured.dart';
import 'package:island/widgets/post/compose_sheet.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_item_skeleton.dart';
import 'package:island/widgets/post/post_list.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:island/widgets/realm/realm_card.dart';
import 'package:island/widgets/publisher/publisher_card.dart';
@@ -35,38 +32,8 @@ import 'package:island/services/event_bus.dart';
import 'package:island/widgets/share/share_sheet.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
Widget notificationIndicatorWidget(
BuildContext context, {
required int count,
EdgeInsets? margin,
}) => Card(
margin: margin,
child: ListTile(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
minTileHeight: 48,
leading: const Icon(Symbols.notifications),
title: Row(
children: [
Text('notifications').tr().fontSize(14),
const Gap(8),
Badge(label: Text(count.toString())),
],
),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.only(left: 16, right: 15),
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => const NotificationSheet(),
);
},
),
);
import 'package:island/widgets/posts/post_subscription_filter.dart';
import 'package:island/pods/post/post_list.dart';
class ExploreScreen extends HookConsumerWidget {
const ExploreScreen({super.key});
@@ -74,26 +41,11 @@ class ExploreScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentFilter = useState<String?>(null);
final selectedPublisherNames = useState<List<String>>([]);
final selectedCategoryIds = useState<List<String>>([]);
final selectedTagIds = useState<List<String>>([]);
final notifier = ref.watch(activityListProvider.notifier);
useEffect(() {
// Set FAB type to chat
final fabMenuNotifier = ref.read(fabMenuTypeProvider.notifier);
Future(() {
fabMenuNotifier.setMenuType(FabMenuType.compose);
});
return () {
// Clean up: reset FAB type to main
final fabMenu = ref.read(fabMenuTypeProvider);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (fabMenu == FabMenuType.compose) {
fabMenuNotifier.setMenuType(FabMenuType.main);
}
});
};
}, []);
void handleFilterChange(String? filter) {
currentFilter.value = filter;
notifier.applyFilter(filter);
@@ -123,6 +75,8 @@ class ExploreScreen extends HookConsumerWidget {
final isWide = isWideScreen(context);
final hasSubscriptionsSelected = selectedPublisherNames.value.isNotEmpty;
final filterBar = Card(
margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: Row(
@@ -131,7 +85,9 @@ class ExploreScreen extends HookConsumerWidget {
spacing: 8,
children: [
IconButton(
onPressed: () => handleFilterChange(null),
onPressed: hasSubscriptionsSelected
? null
: () => handleFilterChange(null),
icon: Icon(
Symbols.explore,
fill: currentFilter.value == null ? 1 : null,
@@ -143,7 +99,9 @@ class ExploreScreen extends HookConsumerWidget {
: null,
),
IconButton(
onPressed: () => handleFilterChange('subscriptions'),
onPressed: hasSubscriptionsSelected
? null
: () => handleFilterChange('subscriptions'),
icon: Icon(
Symbols.subscriptions,
fill: currentFilter.value == 'subscriptions' ? 1 : null,
@@ -155,7 +113,9 @@ class ExploreScreen extends HookConsumerWidget {
: null,
),
IconButton(
onPressed: () => handleFilterChange('friends'),
onPressed: hasSubscriptionsSelected
? null
: () => handleFilterChange('friends'),
icon: Icon(
Symbols.people,
fill: currentFilter.value == 'friends' ? 1 : null,
@@ -222,9 +182,16 @@ class ExploreScreen extends HookConsumerWidget {
).padding(horizontal: 8, vertical: 4),
);
final userInfo = ref.watch(userInfoProvider);
final appBar = isWide
? null
: _buildAppBar(currentFilter.value, handleFilterChange, context);
: _buildAppBar(
currentFilter.value,
handleFilterChange,
context,
hasSubscriptionsSelected,
);
final dragging = useState(false);
@@ -247,6 +214,49 @@ class ExploreScreen extends HookConsumerWidget {
AppScaffold(
isNoBackground: false,
appBar: appBar,
floatingActionButton: userInfo.value != null
? FloatingActionButton(
child: const Icon(Symbols.create),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Gap(40),
ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Symbols.post_add_rounded),
title: Text('postCompose').tr(),
onTap: () async {
Navigator.of(context).pop();
await PostComposeSheet.show(context);
},
),
ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Symbols.article),
title: Text('articleCompose').tr(),
onTap: () async {
Navigator.of(context).pop();
GoRouter.of(
context,
).pushNamed('articleCompose');
},
),
const Gap(16),
],
),
);
},
).padding(bottom: isWideScreen(context) ? null : 56)
: null,
body: isWide
? _buildWideBody(
context,
@@ -257,6 +267,10 @@ class ExploreScreen extends HookConsumerWidget {
query,
events,
selectedDay,
currentFilter.value,
selectedPublisherNames,
selectedCategoryIds,
selectedTagIds,
)
: _buildNarrowBody(context, ref, currentFilter.value),
),
@@ -309,6 +323,25 @@ class ExploreScreen extends HookConsumerWidget {
);
}
Widget _buildPostList(
BuildContext context,
WidgetRef ref,
List<String> selectedPublishers,
List<String> selectedCategories,
List<String> selectedTags,
) {
return SliverPostList(
queryKey: 'explore_filtered',
query: PostListQuery(
publishers: selectedPublishers,
categories: selectedCategories,
tags: selectedTags,
),
padding: EdgeInsets.zero,
itemPadding: const EdgeInsets.only(bottom: 8),
);
}
Widget _buildWideBody(
BuildContext context,
WidgetRef ref,
@@ -318,10 +351,29 @@ class ExploreScreen extends HookConsumerWidget {
ValueNotifier<EventCalendarQuery> query,
AsyncValue<List<dynamic>> events,
ValueNotifier<DateTime> selectedDay,
String? currentFilter,
ValueNotifier<List<String>> selectedPublishers,
ValueNotifier<List<String>> selectedCategories,
ValueNotifier<List<String>> selectedTags,
) {
final bodyView = _buildActivityList(context, ref);
// Use post list when subscription filter is active and publishers are selected
final usePostList =
selectedPublishers.value.isNotEmpty ||
selectedCategories.value.isNotEmpty ||
selectedTags.value.isNotEmpty;
final bodyView = usePostList
? _buildPostList(
context,
ref,
selectedPublishers.value,
selectedCategories.value,
selectedTags.value,
)
: _buildActivityList(context, ref);
final notifier = ref.watch(activityListProvider.notifier);
final notifier = usePostList
? null // Post list handles its own refreshing
: ref.watch(activityListProvider.notifier);
return Row(
spacing: 12,
@@ -329,7 +381,9 @@ class ExploreScreen extends HookConsumerWidget {
Flexible(
flex: 3,
child: ExtendedRefreshIndicator(
onRefresh: notifier.refresh,
onRefresh: () async {
await notifier?.refresh();
},
child: CustomScrollView(
slivers: [
const SliverGap(12),
@@ -349,24 +403,21 @@ class ExploreScreen extends HookConsumerWidget {
child: Column(
spacing: 8,
children: [
const Gap(4),
if (user.value?.activatedAt == null)
AccountUnactivatedCard(),
CheckInWidget(
margin: EdgeInsets.zero,
onChecked: () {
ref.invalidate(eventCalendarProvider(query.value));
Gap(4 + MediaQuery.paddingOf(context).top),
PostSubscriptionFilterWidget(
initialSelectedPublishers: selectedPublishers.value,
initialSelectedCategories: selectedCategories.value,
initialSelectedTags: selectedTags.value,
onSelectedPublishersChanged: (names) {
selectedPublishers.value = names;
},
onSelectedCategoriesChanged: (ids) {
selectedCategories.value = ids;
},
onSelectedTagsChanged: (ids) {
selectedTags.value = ids;
},
),
if (notificationCount.value != null &&
notificationCount.value! > 0)
notificationIndicatorWidget(
context,
count: notificationCount.value ?? 0,
margin: EdgeInsets.zero,
),
PostFeaturedList(),
FriendsOverviewWidget(),
],
),
),
@@ -416,11 +467,11 @@ class ExploreScreen extends HookConsumerWidget {
String? currentFilter,
void Function(String?) handleFilterChange,
BuildContext context,
bool hasSubscriptionsSelected,
) {
final foregroundColor = Theme.of(context).appBarTheme.foregroundColor;
return AppBar(
toolbarHeight: 48,
flexibleSpace: Container(
height: 48,
margin: EdgeInsets.only(
@@ -429,91 +480,100 @@ class ExploreScreen extends HookConsumerWidget {
top: 4 + MediaQuery.of(context).padding.top,
bottom: 4,
),
child: Row(
spacing: 8,
children: [
IconButton(
onPressed: () => handleFilterChange(null),
icon: Icon(
Symbols.explore,
color: foregroundColor,
fill: currentFilter == null ? 1 : null,
),
tooltip: 'explore'.tr(),
isSelected: currentFilter == null,
color: currentFilter == null ? foregroundColor : null,
),
IconButton(
onPressed: () => handleFilterChange('subscriptions'),
icon: Icon(
Symbols.subscriptions,
color: foregroundColor,
fill: currentFilter == 'subscription' ? 1 : null,
),
tooltip: 'exploreFilterSubscriptions'.tr(),
isSelected: currentFilter == 'subscriptions',
),
IconButton(
onPressed: () => handleFilterChange('friends'),
icon: Icon(
Symbols.people,
color: foregroundColor,
fill: currentFilter == 'friends' ? 1 : null,
),
tooltip: 'exploreFilterFriends'.tr(),
isSelected: currentFilter == 'friends',
),
const Spacer(),
IconButton(
onPressed: () {
context.pushNamed('articles');
},
icon: Icon(Symbols.auto_stories, color: foregroundColor),
tooltip: 'webArticlesStand'.tr(),
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.category),
const Gap(12),
Text('categoriesAndTags').tr(),
],
),
onTap: () {
context.pushNamed('postCategories');
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
spacing: 8,
children: [
IconButton(
onPressed: hasSubscriptionsSelected
? null
: () => handleFilterChange(null),
icon: Icon(
Symbols.explore,
color: foregroundColor,
fill: currentFilter == null ? 1 : null,
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.shuffle),
const Gap(12),
Text('postShuffle').tr(),
],
),
onTap: () {
context.pushNamed('postShuffle');
},
tooltip: 'explore'.tr(),
isSelected: currentFilter == null,
color: currentFilter == null ? foregroundColor : null,
),
IconButton(
onPressed: hasSubscriptionsSelected
? null
: () => handleFilterChange('subscriptions'),
icon: Icon(
Symbols.subscriptions,
color: foregroundColor,
fill: currentFilter == 'subscription' ? 1 : null,
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.search),
const Gap(12),
Text('search').tr(),
],
),
onTap: () {
context.pushNamed('postSearch');
},
tooltip: 'exploreFilterSubscriptions'.tr(),
isSelected: currentFilter == 'subscriptions',
),
IconButton(
onPressed: hasSubscriptionsSelected
? null
: () => handleFilterChange('friends'),
icon: Icon(
Symbols.people,
color: foregroundColor,
fill: currentFilter == 'friends' ? 1 : null,
),
],
icon: Icon(Symbols.action_key, color: foregroundColor),
tooltip: 'search'.tr(),
),
],
tooltip: 'exploreFilterFriends'.tr(),
isSelected: currentFilter == 'friends',
),
const Spacer(),
IconButton(
onPressed: () {
context.pushNamed('articles');
},
icon: Icon(Symbols.auto_stories, color: foregroundColor),
tooltip: 'webArticlesStand'.tr(),
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.category),
const Gap(12),
Text('categoriesAndTags').tr(),
],
),
onTap: () {
context.pushNamed('postCategories');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.shuffle),
const Gap(12),
Text('postShuffle').tr(),
],
),
onTap: () {
context.pushNamed('postShuffle');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.search),
const Gap(12),
Text('search').tr(),
],
),
onTap: () {
context.pushNamed('postSearch');
},
),
],
icon: Icon(Symbols.action_key, color: foregroundColor),
tooltip: 'search'.tr(),
),
],
),
),
),
);
@@ -524,9 +584,6 @@ class ExploreScreen extends HookConsumerWidget {
WidgetRef ref,
String? currentFilter,
) {
final user = ref.watch(userInfoProvider);
final notificationCount = ref.watch(notificationUnreadCountProvider);
final bodyView = _buildActivityList(context, ref);
final notifier = ref.watch(activityListProvider.notifier);
@@ -536,43 +593,7 @@ class ExploreScreen extends HookConsumerWidget {
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: ExtendedRefreshIndicator(
onRefresh: notifier.refresh,
child: CustomScrollView(
slivers: [
const SliverGap(8),
if (user.value?.activatedAt == null)
SliverToBoxAdapter(
child: AccountUnactivatedCard().padding(bottom: 8),
),
if (user.value != null)
SliverToBoxAdapter(
child: CheckInWidget(
margin: const EdgeInsets.only(bottom: 8),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: PostFeaturedList(),
),
),
SliverToBoxAdapter(
child: FriendsOverviewWidget(
padding: const EdgeInsets.only(bottom: 8),
hideWhenEmpty: true,
),
),
if (notificationCount.value != null &&
notificationCount.value! > 0)
SliverToBoxAdapter(
child: notificationIndicatorWidget(
context,
count: notificationCount.value ?? 0,
margin: const EdgeInsets.only(bottom: 8),
),
),
bodyView,
],
),
child: CustomScrollView(slivers: [SliverGap(8), bodyView]),
),
).padding(horizontal: 8),
);
@@ -634,15 +655,15 @@ class _DiscoveryActivityItem extends StatelessWidget {
children: [
for (final item in items)
switch (type) {
'realm' => RealmCard(
'realm' => RealmDiscoveryCard(
realm: SnRealm.fromJson(item['data']),
maxWidth: 280,
),
'publisher' => PublisherCard(
'publisher' => PublisherDiscoveryCard(
publisher: SnPublisher.fromJson(item['data']),
maxWidth: 280,
),
'article' => WebArticleCard(
'article' => WebArticleDiscoveryCard(
article: SnWebArticle.fromJson(item['data']),
maxWidth: 280,
),

View File

@@ -4,23 +4,20 @@ import 'dart:math' as math;
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:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/route.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/notification_tile.dart';
import 'package:island/widgets/paging/pagination_list.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:relative_time/relative_time.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
part 'notification.g.dart';
@@ -188,7 +185,9 @@ class NotificationListNotifier extends AsyncNotifier<List<SnNotification>>
.toList();
final unreadCount = notifications.where((n) => n.viewedAt == null).length;
ref.read(notificationUnreadCountProvider.notifier).decrement(unreadCount);
if (ref.mounted) {
ref.read(notificationUnreadCountProvider.notifier).decrement(unreadCount);
}
return notifications;
}
@@ -197,31 +196,6 @@ class NotificationListNotifier extends AsyncNotifier<List<SnNotification>>
class NotificationSheet extends HookConsumerWidget {
const NotificationSheet({super.key});
IconData _getNotificationIcon(String topic) {
switch (topic) {
case 'post.replies':
return Symbols.reply;
case 'wallet.transactions':
return Symbols.account_balance_wallet;
case 'relationships.friends.request':
return Symbols.person_add;
case 'invites.chat':
return Symbols.chat;
case 'invites.realm':
return Symbols.domain;
case 'auth.login':
return Symbols.login;
case 'posts.new':
return Symbols.post_add;
case 'wallet.orders.paid':
return Symbols.shopping_bag;
case 'posts.reactions.new':
return Symbols.add_reaction;
default:
return Symbols.notifications;
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
// Refresh unread count when sheet opens to sync across devices
@@ -265,109 +239,7 @@ class NotificationSheet extends HookConsumerWidget {
notifier: notificationListProvider.notifier,
footerSkeletonChild: const SkeletonNotificationTile(),
itemBuilder: (context, index, notification) {
final pfp = notification.meta['pfp'] as String?;
final images = notification.meta['images'] as List?;
final imageIds = images?.cast<String>() ?? [];
return ListTile(
isThreeLine: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
leading: pfp != null
? ProfilePictureWidget(fileId: pfp, radius: 20)
: CircleAvatar(
backgroundColor: Theme.of(
context,
).colorScheme.primaryContainer,
child: Icon(
_getNotificationIcon(notification.topic),
color: Theme.of(
context,
).colorScheme.onPrimaryContainer,
),
),
title: Text(notification.title),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (notification.subtitle.isNotEmpty)
Text(notification.subtitle).bold(),
Row(
spacing: 6,
children: [
Text(
DateFormat().format(
notification.createdAt.toLocal(),
),
).fontSize(11),
Text('·').fontSize(11).bold(),
Text(
RelativeTime(
context,
).format(notification.createdAt.toLocal()),
).fontSize(11),
],
).opacity(0.75).padding(bottom: 4),
MarkdownTextContent(
content: notification.content,
textStyle: Theme.of(context).textTheme.bodyMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.8),
),
),
if (imageIds.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: imageIds.map((imageId) {
return SizedBox(
width: 80,
height: 80,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CloudImageWidget(
fileId: imageId,
aspectRatio: 1,
fit: BoxFit.cover,
),
),
);
}).toList(),
),
),
],
),
trailing: notification.viewedAt != null
? null
: Container(
width: 12,
height: 12,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
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);
}
}
},
);
return NotificationTile(notification: notification);
},
),
),

View File

@@ -30,7 +30,7 @@ Future<SnPostTag> postTag(Ref ref, String slug) async {
}
@riverpod
Future<bool> postCategorySubscriptionStatus(
Future<SnCategorySubscription?> postCategorySubscription(
Ref ref,
String slug,
bool isCategory,
@@ -40,9 +40,10 @@ Future<bool> postCategorySubscriptionStatus(
final resp = await apiClient.get(
'/sphere/posts/${isCategory ? 'categories' : 'tags'}/$slug/subscription',
);
return resp.statusCode == 200;
if (resp.data == 200) return SnCategorySubscription.fromJson(resp.data);
return null;
} catch (_) {
return false;
return null;
}
}
@@ -56,7 +57,7 @@ Future<void> _subscribeToCategoryOrTag(
'/sphere/posts/${isCategory ? 'categories' : 'tags'}/$slug/subscribe',
);
// Invalidate the subscription status to refresh it
ref.invalidate(postCategorySubscriptionStatusProvider(slug, isCategory));
ref.invalidate(postCategorySubscriptionProvider(slug, isCategory));
}
Future<void> _unsubscribeFromCategoryOrTag(
@@ -69,7 +70,7 @@ Future<void> _unsubscribeFromCategoryOrTag(
'/sphere/posts/${isCategory ? 'categories' : 'tags'}/$slug/unsubscribe',
);
// Invalidate the subscription status to refresh it
ref.invalidate(postCategorySubscriptionStatusProvider(slug, isCategory));
ref.invalidate(postCategorySubscriptionProvider(slug, isCategory));
}
class PostCategoryDetailScreen extends HookConsumerWidget {
@@ -88,7 +89,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
: null;
final postTag = isCategory ? null : ref.watch(postTagProvider(slug));
final subscriptionStatus = ref.watch(
postCategorySubscriptionStatusProvider(slug, isCategory),
postCategorySubscriptionProvider(slug, isCategory),
);
final postFilterTitle = isCategory
@@ -118,7 +119,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
Text('A category'),
const Gap(8),
subscriptionStatus.when(
data: (isSubscribed) => isSubscribed
data: (subscription) => subscription != null
? FilledButton.icon(
onPressed: () async {
await _unsubscribeFromCategoryOrTag(
@@ -176,7 +177,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
Text('A tag'),
const Gap(8),
subscriptionStatus.when(
data: (isSubscribed) => isSubscribed
data: (subscription) => subscription != null
? FilledButton.icon(
onPressed: () async {
await _unsubscribeFromCategoryOrTag(

View File

@@ -158,48 +158,55 @@ final class PostTagFamily extends $Family
String toString() => r'postTagProvider';
}
@ProviderFor(postCategorySubscriptionStatus)
const postCategorySubscriptionStatusProvider =
PostCategorySubscriptionStatusFamily._();
@ProviderFor(postCategorySubscription)
const postCategorySubscriptionProvider = PostCategorySubscriptionFamily._();
final class PostCategorySubscriptionStatusProvider
extends $FunctionalProvider<AsyncValue<bool>, bool, FutureOr<bool>>
with $FutureModifier<bool>, $FutureProvider<bool> {
const PostCategorySubscriptionStatusProvider._({
required PostCategorySubscriptionStatusFamily super.from,
final class PostCategorySubscriptionProvider
extends
$FunctionalProvider<
AsyncValue<SnCategorySubscription?>,
SnCategorySubscription?,
FutureOr<SnCategorySubscription?>
>
with
$FutureModifier<SnCategorySubscription?>,
$FutureProvider<SnCategorySubscription?> {
const PostCategorySubscriptionProvider._({
required PostCategorySubscriptionFamily super.from,
required (String, bool) super.argument,
}) : super(
retry: null,
name: r'postCategorySubscriptionStatusProvider',
name: r'postCategorySubscriptionProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$postCategorySubscriptionStatusHash();
String debugGetCreateSourceHash() => _$postCategorySubscriptionHash();
@override
String toString() {
return r'postCategorySubscriptionStatusProvider'
return r'postCategorySubscriptionProvider'
''
'$argument';
}
@$internal
@override
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
$FutureProviderElement<SnCategorySubscription?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<bool> create(Ref ref) {
FutureOr<SnCategorySubscription?> create(Ref ref) {
final argument = this.argument as (String, bool);
return postCategorySubscriptionStatus(ref, argument.$1, argument.$2);
return postCategorySubscription(ref, argument.$1, argument.$2);
}
@override
bool operator ==(Object other) {
return other is PostCategorySubscriptionStatusProvider &&
return other is PostCategorySubscriptionProvider &&
other.argument == argument;
}
@@ -209,26 +216,30 @@ final class PostCategorySubscriptionStatusProvider
}
}
String _$postCategorySubscriptionStatusHash() =>
r'407dc7fcaeffc461b591b4ee2418811aa4f0a63f';
String _$postCategorySubscriptionHash() =>
r'60fe0a68ab3d8d493eac3577187d7adcfc0244b9';
final class PostCategorySubscriptionStatusFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<bool>, (String, bool)> {
const PostCategorySubscriptionStatusFamily._()
final class PostCategorySubscriptionFamily extends $Family
with
$FunctionalFamilyOverride<
FutureOr<SnCategorySubscription?>,
(String, bool)
> {
const PostCategorySubscriptionFamily._()
: super(
retry: null,
name: r'postCategorySubscriptionStatusProvider',
name: r'postCategorySubscriptionProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
PostCategorySubscriptionStatusProvider call(String slug, bool isCategory) =>
PostCategorySubscriptionStatusProvider._(
PostCategorySubscriptionProvider call(String slug, bool isCategory) =>
PostCategorySubscriptionProvider._(
argument: (slug, isCategory),
from: this,
);
@override
String toString() => r'postCategorySubscriptionStatusProvider';
String toString() => r'postCategorySubscriptionProvider';
}

View File

@@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@@ -33,7 +34,7 @@ part 'publisher_profile.g.dart';
class _PublisherBasisWidget extends StatelessWidget {
final SnPublisher data;
final AsyncValue<SnSubscriptionStatus> subStatus;
final AsyncValue<SnPublisherSubscription?> subStatus;
final ValueNotifier<bool> subscribing;
final VoidCallback subscribe;
final VoidCallback unsubscribe;
@@ -208,16 +209,16 @@ class _PublisherBasisWidget extends StatelessWidget {
data: (status) => FilledButton.icon(
onPressed: subscribing.value
? null
: (status.isSubscribed
: (status != null
? unsubscribe
: subscribe),
icon: Icon(
status.isSubscribed
status != null
? Symbols.remove_circle
: Symbols.add_circle,
),
label: Text(
status.isSubscribed
status != null
? 'unsubscribe'
: 'subscribe',
).tr(),
@@ -366,13 +367,23 @@ Future<List<SnAccountBadge>> publisherBadges(Ref ref, String pubName) async {
}
@riverpod
Future<SnSubscriptionStatus> publisherSubscriptionStatus(
Future<SnPublisherSubscription?> publisherSubscriptionStatus(
Ref ref,
String pubName,
) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get("/sphere/publishers/$pubName/subscription");
return SnSubscriptionStatus.fromJson(resp.data);
try {
final resp = await apiClient.get(
"/sphere/publishers/$pubName/subscription",
);
return SnPublisherSubscription.fromJson(resp.data);
} catch (err) {
if (err is DioException) {
if (err.response?.statusCode == 404) return null;
rethrow;
}
}
return null;
}
@riverpod

View File

@@ -168,13 +168,13 @@ const publisherSubscriptionStatusProvider =
final class PublisherSubscriptionStatusProvider
extends
$FunctionalProvider<
AsyncValue<SnSubscriptionStatus>,
SnSubscriptionStatus,
FutureOr<SnSubscriptionStatus>
AsyncValue<SnPublisherSubscription?>,
SnPublisherSubscription?,
FutureOr<SnPublisherSubscription?>
>
with
$FutureModifier<SnSubscriptionStatus>,
$FutureProvider<SnSubscriptionStatus> {
$FutureModifier<SnPublisherSubscription?>,
$FutureProvider<SnPublisherSubscription?> {
const PublisherSubscriptionStatusProvider._({
required PublisherSubscriptionStatusFamily super.from,
required String super.argument,
@@ -198,12 +198,12 @@ final class PublisherSubscriptionStatusProvider
@$internal
@override
$FutureProviderElement<SnSubscriptionStatus> $createElement(
$FutureProviderElement<SnPublisherSubscription?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SnSubscriptionStatus> create(Ref ref) {
FutureOr<SnPublisherSubscription?> create(Ref ref) {
final argument = this.argument as String;
return publisherSubscriptionStatus(ref, argument);
}
@@ -221,10 +221,10 @@ final class PublisherSubscriptionStatusProvider
}
String _$publisherSubscriptionStatusHash() =>
r'634262ce519e1c8288267df11e08e1d4acaa4a44';
r'688bf38554afea9e68b2cb59c5f08c6e8dd31b62';
final class PublisherSubscriptionStatusFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<SnSubscriptionStatus>, String> {
with $FunctionalFamilyOverride<FutureOr<SnPublisherSubscription?>, String> {
const PublisherSubscriptionStatusFamily._()
: super(
retry: null,

View File

@@ -1,16 +1,16 @@
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';
import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/navigation/fab_menu.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -43,22 +43,7 @@ class RealmListScreen extends HookConsumerWidget {
final realms = ref.watch(realmsJoinedProvider);
final realmInvites = ref.watch(realmInvitesProvider);
useEffect(() {
// Set FAB type to realm
final fabMenuNotifier = ref.read(fabMenuTypeProvider.notifier);
Future(() {
fabMenuNotifier.setMenuType(FabMenuType.realm);
});
return () {
// Clean up: reset FAB type to main
final fabMenu = ref.read(fabMenuTypeProvider);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (fabMenu == FabMenuType.realm) {
fabMenuNotifier.setMenuType(FabMenuType.main);
}
});
};
}, []);
final userInfo = ref.watch(userInfoProvider);
return AppScaffold(
isNoBackground: false,
@@ -97,35 +82,68 @@ class RealmListScreen extends HookConsumerWidget {
const Gap(8),
],
),
floatingActionButton: userInfo.value != null
? FloatingActionButton(
child: const Icon(Symbols.add),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Gap(40),
ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
leading: const Icon(Symbols.group_add),
title: Text('createRealm').tr(),
onTap: () {
Navigator.of(context).pop();
context.pushNamed('realmNew').then((value) {
if (value != null) {
// Fire realm refresh event if needed
// eventBus.fire(const RealmsRefreshEvent());
}
});
},
),
const Gap(16),
],
),
);
},
).padding(bottom: isWideScreen(context) ? null : 56)
: null,
body: ExtendedRefreshIndicator(
child: realms.when(
data:
(value) => Column(
children: [
Expanded(
child: ListView.separated(
padding: EdgeInsets.only(
top: 8,
bottom: MediaQuery.of(context).padding.bottom + 8,
),
itemCount: value.length,
itemBuilder: (context, item) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 540),
child: RealmListTile(realm: value[item]),
).padding(horizontal: 8).center();
},
separatorBuilder: (_, _) => const Gap(8),
),
data: (value) => Column(
children: [
Expanded(
child: ListView.separated(
padding: EdgeInsets.only(
top: 8,
bottom: MediaQuery.of(context).padding.bottom + 8,
),
],
itemCount: value.length,
itemBuilder: (context, item) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 540),
child: RealmListTile(realm: value[item]),
).padding(horizontal: 8).center();
},
separatorBuilder: (_, _) => const Gap(8),
),
),
],
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(e, _) => ResponseErrorWidget(
error: e,
onRetry: () => ref.invalidate(realmsJoinedProvider),
),
error: (e, _) => ResponseErrorWidget(
error: e,
onRetry: () => ref.invalidate(realmsJoinedProvider),
),
),
onRefresh: () => ref.refresh(realmsJoinedProvider.future),
),
@@ -183,57 +201,49 @@ class _RealmInviteSheet extends HookConsumerWidget {
),
],
child: invites.when(
data:
(items) =>
items.isEmpty
? Center(
child:
Text(
'invitesEmpty',
textAlign: TextAlign.center,
).tr(),
)
: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.realm!.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(invite.realm!.name),
subtitle:
Text(
invite.role >= 100
? 'permissionOwner'
: invite.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
data: (items) => items.isEmpty
? Center(
child: Text('invitesEmpty', textAlign: TextAlign.center).tr(),
)
: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.realm!.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(invite.realm!.name),
subtitle: Text(
invite.role >= 100
? 'permissionOwner'
: invite.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(realmInvitesProvider),
),
error: (error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(realmInvitesProvider),
),
),
);
}

View File

@@ -407,66 +407,35 @@ class SettingsScreen extends HookConsumerWidget {
),
),
// FAB position settings
ListTile(
minLeadingWidth: 48,
title: Text('fabLocation').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.adjust),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
items: [
DropdownMenuItem<String>(
value: 'left',
child: Text('Left').fontSize(14),
),
DropdownMenuItem<String>(
value: 'center',
child: Text('Center').fontSize(14),
),
DropdownMenuItem<String>(
value: 'right',
child: Text('Right').fontSize(14),
),
],
value: settings.fabPosition,
onChanged: (String? value) {
if (value != null) {
ref.read(appSettingsProvider.notifier).setFabPosition(value);
showSnackBar('settingsApplied'.tr());
}
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
height: 40,
width: 120,
),
menuItemStyleData: const MenuItemStyleData(height: 40),
),
),
),
// Card background opacity settings
ListTile(
isThreeLine: true,
minLeadingWidth: 48,
title: Text('settingsCardBackgroundOpacity').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.opacity),
subtitle: Padding(
padding: const EdgeInsets.only(top: 8),
child: Slider(
value: settings.cardTransparency,
min: 0.0,
max: 1.0,
year2023: true,
padding: EdgeInsets.only(right: 24),
label: '${(settings.cardTransparency * 100).round()}%',
onChanged: (value) {
ref
.read(appSettingsProvider.notifier)
.setAppTransparentBackground(value);
},
child: SliderTheme(
data: SliderThemeData(
trackHeight: 2,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 24),
trackShape: RoundedRectSliderTrackShape(),
),
child: Slider(
value: settings.cardTransparency,
min: 0.0,
max: 1.0,
year2023: true,
padding: EdgeInsets.only(right: 24),
label: '${(settings.cardTransparency * 100).round()}%',
onChanged: (value) {
ref
.read(appSettingsProvider.notifier)
.setAppTransparentBackground(value);
},
),
),
),
),
@@ -512,7 +481,7 @@ class SettingsScreen extends HookConsumerWidget {
minLeadingWidth: 48,
title: Text('settingsBackgroundImageEnable').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.image),
leading: const Icon(Symbols.hide_image),
trailing: Switch(
value: settings.showBackgroundImage,
onChanged: (value) {
@@ -713,20 +682,6 @@ class SettingsScreen extends HookConsumerWidget {
];
final behaviorSettings = [
ListTile(
minLeadingWidth: 48,
title: Text('settingsAutoTranslate').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.translate),
trailing: Switch(
value: settings.autoTranslate,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setAutoTranslate(value);
},
),
),
// Sound effects settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsSoundEffects').tr(),
@@ -743,13 +698,13 @@ class SettingsScreen extends HookConsumerWidget {
// April Fool features settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsAprilFoolFeatures').tr(),
title: Text('settingsFestivalFeatures').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.celebration),
trailing: Switch(
value: settings.aprilFoolFeatures,
value: settings.festivalFeatures,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setAprilFoolFeatures(value);
ref.read(appSettingsProvider.notifier).setFeativalFeatures(value);
},
),
),
@@ -810,6 +765,116 @@ class SettingsScreen extends HookConsumerWidget {
},
),
),
// Grouped chat list settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsGroupedChatList').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.chat),
trailing: Switch(
value: settings.groupedChatList,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setGroupedChatList(value);
},
),
),
// Haptic feedback settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsNotifyWithHaptic').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.vibration),
trailing: Switch(
value: settings.notifyWithHaptic,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setNotifyWithHaptic(value);
},
),
),
// Default screen settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsDefaultScreen').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.home),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
items: [
DropdownMenuItem<String>(
value: 'dashboard',
child: Text('dashboard').tr().fontSize(14),
),
DropdownMenuItem<String>(
value: 'explore',
child: Text('explore').tr().fontSize(14),
),
DropdownMenuItem<String>(
value: 'chat',
child: Text('chat').tr().fontSize(14),
),
DropdownMenuItem<String>(
value: 'account',
child: Text('account').tr().fontSize(14),
),
],
value: settings.defaultScreen ?? 'dashboard',
onChanged: (String? value) {
if (value != null) {
ref.read(appSettingsProvider.notifier).setDefaultScreen(value);
showSnackBar('settingsApplied'.tr());
}
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
height: 40,
width: 140,
),
menuItemStyleData: const MenuItemStyleData(height: 40),
),
),
),
// Dash search engine settings
ListTile(
isThreeLine: true,
minLeadingWidth: 48,
title: Text('settingsDashSearchEngine').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.search),
subtitle: Padding(
padding: const EdgeInsets.only(top: 6),
child: TextField(
controller: TextEditingController(text: settings.dashSearchEngine),
decoration: InputDecoration(
hintText: 'https://google.com/?q=%s',
helperText: 'settingsDashSearchEngineHelper'.tr(),
suffixIcon: IconButton(
icon: const Icon(Symbols.restart_alt),
onPressed: () {
ref
.read(appSettingsProvider.notifier)
.setDashSearchEngine(null);
showSnackBar('settingsApplied'.tr());
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
isDense: true,
),
onSubmitted: (value) {
ref
.read(appSettingsProvider.notifier)
.setDashSearchEngine(value.isEmpty ? null : value);
showSnackBar('settingsApplied'.tr());
},
),
),
),
];
// Desktop-specific settings
@@ -821,20 +886,33 @@ class SettingsScreen extends HookConsumerWidget {
title: Text('settingsWindowOpacity').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.opacity),
isThreeLine: true,
subtitle: Padding(
padding: const EdgeInsets.only(top: 8),
child: Slider(
value: settings.windowOpacity,
min: 0.1,
max: 1.0,
year2023: true,
padding: EdgeInsets.only(right: 24),
label: '${(settings.windowOpacity * 100).round()}%',
onChanged: (value) {
ref
.read(appSettingsProvider.notifier)
.setWindowOpacity(value);
},
child: SliderTheme(
data: SliderThemeData(
trackHeight: 2,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 8,
),
overlayShape: const RoundSliderOverlayShape(
overlayRadius: 24,
),
trackShape: RoundedRectSliderTrackShape(),
),
child: Slider(
value: settings.windowOpacity,
min: 0.1,
max: 1.0,
year2023: true,
padding: EdgeInsets.only(right: 24),
label: '${(settings.windowOpacity * 100).round()}%',
onChanged: (value) {
ref
.read(appSettingsProvider.notifier)
.setWindowOpacity(value);
},
),
),
),
),
@@ -844,45 +922,52 @@ class SettingsScreen extends HookConsumerWidget {
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: 'settingsAppearance'.tr(),
children: appearanceSettings,
),
_SettingsSection(
title: 'settingsServer'.tr(),
children: serverSettings,
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'settingsBehavior'.tr(),
children: behaviorSettings,
),
if (desktopSettings.isNotEmpty)
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 920),
child: Row(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'settingsDesktop'.tr(),
children: desktopSettings,
title: 'settingsAppearance'.tr(),
children: appearanceSettings,
),
],
_SettingsSection(
title: 'settingsServer'.tr(),
children: serverSettings,
),
],
),
),
),
],
);
Expanded(
child: Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'settingsBehavior'.tr(),
children: behaviorSettings,
),
if (desktopSettings.isNotEmpty)
_SettingsSection(
title: 'settingsDesktop'.tr(),
children: desktopSettings,
),
],
),
),
],
).padding(horizontal: 16),
).center();
} else {
// Single column layout for narrow screens
return Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
@@ -903,7 +988,7 @@ class SettingsScreen extends HookConsumerWidget {
children: desktopSettings,
),
],
);
).padding(horizontal: 16);
}
}
@@ -940,22 +1025,25 @@ class _SettingsSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
return Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
),
...children,
const SizedBox(height: 16),
],
...children,
const SizedBox(height: 16),
],
),
);
}
}

View File

@@ -146,7 +146,7 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
itemBuilder: (context, index) {
final sticker = stickers[index];
return Tooltip(
message: ':${p?.prefix ?? ''}${sticker.slug}:',
message: ':${p?.prefix ?? ''}+${sticker.slug}:',
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),

View File

@@ -91,7 +91,6 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
if (query.value.query == null || query.value.query!.isEmpty) {
searchController.clear();
}
notifier.applyFilter(query.value);
return null;
}, [query]);
@@ -109,6 +108,7 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
IconButton(
onPressed: () {
query.value = query.value.copyWith(byUsage: !query.value.byUsage);
notifier.applyFilter(query.value);
},
icon: query.value.byUsage
? const Icon(Symbols.local_fire_department)
@@ -141,6 +141,7 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
icon: const Icon(Symbols.close),
onPressed: () {
query.value = query.value.copyWith(query: null);
notifier.applyFilter(query.value);
searchController.clear();
focusNode.unfocus();
},
@@ -153,11 +154,13 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
const Duration(milliseconds: 500),
() {
query.value = query.value.copyWith(query: value);
notifier.applyFilter(query.value);
},
);
},
onSubmitted: (value) {
query.value = query.value.copyWith(query: value);
notifier.applyFilter(query.value);
focusNode.unfocus();
},
),

View File

@@ -1,5 +1,6 @@
import 'dart:math' as math;
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -10,11 +11,9 @@ import 'package:island/screens/notification.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/navigation/conditional_bottom_nav.dart';
import 'package:island/widgets/navigation/fab_menu.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/chat/chat_summary.dart';
import 'package:styled_widget/styled_widget.dart';
final currentRouteProvider = NotifierProvider<CurrentRouteNotifier, String?>(
CurrentRouteNotifier.new,
@@ -31,9 +30,10 @@ class CurrentRouteNotifier extends Notifier<String?> {
}
}
const kWideScreenRouteStart = 4;
const kWideScreenRouteStart = 5;
const kTabRoutes = [
'/',
'/explore',
'/chat',
'/realms',
'/account',
@@ -67,6 +67,10 @@ class TabsScreen extends HookConsumerWidget {
final wideScreen = isWideScreen(context);
final destinations = [
NavigationDestination(
label: 'dashboard'.tr(),
icon: const Icon(Symbols.dashboard_rounded),
),
NavigationDestination(
label: 'explore'.tr(),
icon: const Icon(Symbols.explore_rounded),
@@ -81,7 +85,7 @@ class TabsScreen extends HookConsumerWidget {
),
NavigationDestination(
label: 'realms'.tr(),
icon: const Icon(Symbols.group_rounded),
icon: const Icon(Symbols.groups_3),
),
NavigationDestination(
label: 'account'.tr(),
@@ -140,13 +144,6 @@ class TabsScreen extends HookConsumerWidget {
final currentIndex = getCurrentIndex();
final routes = kTabRoutes.sublist(
0,
isWideScreen(context) ? null : kWideScreenRouteStart,
);
final shouldShowFab = routes.contains(currentLocation) && !wideScreen;
final settings = ref.watch(appSettingsProvider);
if (isWideScreen(context)) {
return Container(
color: Theme.of(context).colorScheme.surfaceContainer,
@@ -154,21 +151,23 @@ class TabsScreen extends HookConsumerWidget {
children: [
NavigationRail(
backgroundColor: Colors.transparent,
destinations:
destinations
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
),
)
.toList(),
destinations: destinations.mapIndexed((idx, d) {
if (d.icon is Icon) {
return NavigationRailDestination(
icon: Icon(
(d.icon as Icon).icon,
fill: currentIndex == idx ? 1 : null,
),
label: Text(d.label),
);
}
return NavigationRailDestination(
icon: d.icon,
label: Text(d.label),
);
}).toList(),
selectedIndex: currentIndex,
onDestinationSelected: onDestinationSelected,
trailingAtBottom: true,
trailing: const FabMenu(
elevation: 0,
).padding(bottom: MediaQuery.of(context).padding.bottom + 16),
),
Expanded(
child: ClipRRect(
@@ -194,11 +193,6 @@ class TabsScreen extends HookConsumerWidget {
),
child: child ?? const SizedBox.shrink(),
),
floatingActionButton: shouldShowFab ? const FabMenu() : null,
floatingActionButtonLocation:
shouldShowFab
? _DockedFabLocation(context, settings.fabPosition)
: null,
bottomNavigationBar: ConditionalBottomNav(
child: ClipRRect(
borderRadius: BorderRadius.only(
@@ -208,49 +202,43 @@ class TabsScreen extends HookConsumerWidget {
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: BottomAppBar(
height: 56,
padding: EdgeInsets.symmetric(horizontal: 24),
shape: AutomaticNotchedShape(
RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
),
color: Theme.of(context).colorScheme.surface.withOpacity(0.8),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: () {
final navItems =
destinations.asMap().entries.map<Widget>((entry) {
int index = entry.key;
NavigationDestination dest = entry.value;
return IconButton(
icon: dest.icon,
onPressed: () => onDestinationSelected(index),
color:
index == currentIndex
? Theme.of(context).colorScheme.primary
: null,
);
}).toList();
// Add mock item to leave space for FAB based on position
final gapIndex = switch (settings.fabPosition) {
'left' => 0,
'right' => navItems.length,
_ => navItems.length ~/ 2, // center
};
navItems.insert(
gapIndex,
SizedBox(
width: settings.fabPosition == 'center' ? 72 : 48,
),
);
return navItems;
}(),
),
],
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: NavigationBar(
height: 56,
destinations: destinations.mapIndexed((idx, d) {
if (d.icon is Icon) {
return NavigationDestination(
icon: Icon(
(d.icon as Icon).icon,
fill: currentIndex == idx ? 1 : null,
),
label: d.label,
);
}
return d;
}).toList(),
selectedIndex: currentIndex,
onDestinationSelected: onDestinationSelected,
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
backgroundColor: Theme.of(
context,
).colorScheme.surface.withOpacity(0.8),
indicatorColor: Theme.of(
context,
).colorScheme.primary.withOpacity(0.2),
).padding(horizontal: 12),
),
),
),
@@ -259,40 +247,3 @@ class TabsScreen extends HookConsumerWidget {
);
}
}
class _DockedFabLocation extends FloatingActionButtonLocation {
final BuildContext context;
final String fabPosition;
const _DockedFabLocation(this.context, this.fabPosition);
@override
Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
final mediaQuery = MediaQuery.of(context);
final safeAreaPadding = mediaQuery.padding;
// Position horizontally based on setting
final double fabX = switch (fabPosition) {
'left' => scaffoldGeometry.minInsets.left + 24,
'right' =>
scaffoldGeometry.scaffoldSize.width -
scaffoldGeometry.floatingActionButtonSize.width -
scaffoldGeometry.minInsets.right -
24,
_ =>
(scaffoldGeometry.scaffoldSize.width -
scaffoldGeometry.floatingActionButtonSize.width) /
2, // center
};
// Position closer to bottom with reduced padding
final double fabY =
scaffoldGeometry.scaffoldSize.height -
scaffoldGeometry.floatingActionButtonSize.height -
scaffoldGeometry.bottomSheetSize.height -
safeAreaPadding.bottom -
16;
return Offset(fabX, fabY);
}
}

View File

@@ -10,17 +10,20 @@ import "package:island/widgets/thought/thought_shared.dart";
import "package:material_symbols_icons/material_symbols_icons.dart";
class ThoughtSheet extends HookConsumerWidget {
final String? initialMessage;
final List<Map<String, dynamic>> attachedMessages;
final List<String> attachedPosts;
const ThoughtSheet({
super.key,
this.initialMessage,
this.attachedMessages = const [],
this.attachedPosts = const [],
});
static Future<void> show(
BuildContext context, {
String? initialMessage,
List<Map<String, dynamic>> attachedMessages = const [],
List<String> attachedPosts = const [],
}) {
@@ -28,11 +31,11 @@ class ThoughtSheet extends HookConsumerWidget {
context: context,
isScrollControlled: true,
useSafeArea: true,
builder:
(context) => ThoughtSheet(
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
),
builder: (context) => ThoughtSheet(
initialMessage: initialMessage,
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
),
);
}
@@ -40,6 +43,7 @@ class ThoughtSheet extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final chatState = useThoughtChat(
ref,
initialMessage: initialMessage,
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
);
@@ -75,31 +79,30 @@ class ThoughtSheet extends HookConsumerWidget {
return status
? chatInterface
: Column(
children: [
MaterialBanner(
leading: const Icon(Symbols.error),
content: const Text(
'You have unpaid orders. Please settle your payment to continue using the service.',
style: TextStyle(fontWeight: FontWeight.bold),
),
actions: [
TextButton(
onPressed: () {
retry();
},
child: Text('retry'.tr()),
children: [
MaterialBanner(
leading: const Icon(Symbols.error),
content: const Text(
'You have unpaid orders. Please settle your payment to continue using the service.',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
Expanded(child: chatInterface),
],
);
actions: [
TextButton(
onPressed: () {
retry();
},
child: Text('retry'.tr()),
),
],
),
Expanded(child: chatInterface),
],
);
},
orElse:
() => ThoughtChatInterface(
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
),
orElse: () => ThoughtChatInterface(
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
),
),
);
}

View File

@@ -52,7 +52,7 @@ class TrayService {
break;
case 'exit_app':
windowManager.destroy();
break;
exit(0);
}
}
}

View File

@@ -23,3 +23,31 @@ class OidcAuthCallbackEvent {
const OidcAuthCallbackEvent(this.challengeId);
}
/// Event fired to trigger the command palette
class CommandPaletteTriggerEvent {
const CommandPaletteTriggerEvent();
}
/// Event fired to show the compose post sheet
class ShowComposeSheetEvent {
const ShowComposeSheetEvent();
}
/// Event fired to show the notification sheet
class ShowNotificationSheetEvent {
const ShowNotificationSheetEvent();
}
/// Event fired to show the thought sheet
class ShowThoughtSheetEvent {
final String? initialMessage;
final List<Map<String, dynamic>> attachedMessages;
final List<String> attachedPosts;
const ShowThoughtSheetEvent({
this.initialMessage,
this.attachedMessages = const [],
this.attachedPosts = const [],
});
}

View File

@@ -4,10 +4,12 @@ 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/services.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';
import 'package:island/pods/config.dart';
import 'package:island/route.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/websocket.dart';
@@ -89,6 +91,7 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
BuildContext context,
WidgetRef ref,
) {
final settings = ref.watch(appSettingsProvider);
final ws = ref.watch(websocketProvider);
return ws.dataStream.listen((pkt) async {
if (pkt.type == "notifications.new") {
@@ -98,6 +101,9 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
talker.info(
'[Notification] Showing in-app notification: ${notification.title}',
);
if (settings.notifyWithHaptic) {
HapticFeedback.heavyImpact();
}
showTopSnackBar(
globalOverlay.currentState!,
Center(
@@ -115,12 +121,12 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
right: 16,
top:
(!kIsWeb &&
(Platform.isMacOS ||
Platform.isWindows ||
Platform.isLinux))
? 28
// ignore: use_build_context_synchronously
: MediaQuery.of(context).padding.top + 16,
(Platform.isMacOS ||
Platform.isWindows ||
Platform.isLinux))
? 28
// ignore: use_build_context_synchronously
: MediaQuery.of(context).padding.top + 16,
bottom: 16,
),
);

View File

@@ -7,6 +7,7 @@ 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:island/widgets/post/post_shared.dart';
import 'package:path_provider/path_provider.dart' show getTemporaryDirectory;
import 'package:screenshot/screenshot.dart';
import 'package:share_plus/share_plus.dart';
@@ -29,6 +30,9 @@ Future<void> sharePostAsScreenshot(
sharedPreferencesProvider.overrideWithValue(
ref.watch(sharedPreferencesProvider),
),
repliesProvider(
post.id,
).overrideWithValue(ref.watch(repliesProvider(post.id))),
],
child: Directionality(
textDirection: TextDirection.ltr,

View File

@@ -219,20 +219,20 @@ class AccountName extends StatelessWidget {
if (account.automatedId != null)
hideOverlay
? Icon(
Symbols.smart_toy,
size: 16,
color: nameStyle.color,
fill: 1,
)
: Tooltip(
message: 'accountAutomated'.tr(),
child: Icon(
Symbols.smart_toy,
size: 16,
color: nameStyle.color,
fill: 1,
)
: Tooltip(
message: 'accountAutomated'.tr(),
child: Icon(
Symbols.smart_toy,
size: 16,
color: nameStyle.color,
fill: 1,
),
),
),
],
);
}
@@ -275,20 +275,20 @@ class AccountName extends StatelessWidget {
if (account.automatedId != null)
hideOverlay
? Icon(
Symbols.smart_toy,
size: 16,
color: nameStyle.color,
fill: 1,
)
: Tooltip(
message: 'accountAutomated'.tr(),
child: Icon(
Symbols.smart_toy,
size: 16,
color: nameStyle.color,
fill: 1,
)
: Tooltip(
message: 'accountAutomated'.tr(),
child: Icon(
Symbols.smart_toy,
size: 16,
color: nameStyle.color,
fill: 1,
),
),
),
],
);
}
@@ -310,29 +310,28 @@ class VerificationMark extends StatelessWidget {
? kVerificationMarkIcons[mark.type]
: Symbols.verified,
size: 16,
color:
(kVerificationMarkColors.length > mark.type && mark.type >= 0)
? kVerificationMarkColors[mark.type]
: Colors.blue,
color: (kVerificationMarkColors.length > mark.type && mark.type >= 0)
? kVerificationMarkColors[mark.type]
: Colors.blue,
fill: 1,
);
return hideOverlay
? icon
: Tooltip(
richMessage: TextSpan(
text: mark.title ?? 'No title',
children: [
TextSpan(text: '\n'),
TextSpan(
text: mark.description ?? 'descriptionNone'.tr(),
style: TextStyle(fontWeight: FontWeight.normal),
),
],
style: TextStyle(fontWeight: FontWeight.bold),
),
child: icon,
);
richMessage: TextSpan(
text: mark.title ?? 'No title',
children: [
TextSpan(text: '\n'),
TextSpan(
text: mark.description ?? 'descriptionNone'.tr(),
style: TextStyle(fontWeight: FontWeight.normal),
),
],
style: TextStyle(fontWeight: FontWeight.bold),
),
child: icon,
);
}
}
@@ -384,19 +383,19 @@ class StellarMembershipMark extends StatelessWidget {
return hideOverlay
? icon
: Tooltip(
richMessage: TextSpan(
text: 'stellarMembership'.tr(),
children: [
TextSpan(text: '\n'),
TextSpan(
text: 'currentMembershipMember'.tr(args: [tierName]),
style: TextStyle(fontWeight: FontWeight.normal),
),
],
style: TextStyle(fontWeight: FontWeight.bold),
),
child: icon,
);
richMessage: TextSpan(
text: 'stellarMembership'.tr(),
children: [
TextSpan(text: '\n'),
TextSpan(
text: 'currentMembershipMember'.tr(args: [tierName]),
style: TextStyle(fontWeight: FontWeight.normal),
),
],
style: TextStyle(fontWeight: FontWeight.bold),
),
child: icon,
);
}
}
@@ -414,10 +413,9 @@ class VerificationStatusCard extends StatelessWidget {
? kVerificationMarkIcons[mark.type]
: Symbols.verified,
size: 32,
color:
(kVerificationMarkColors.length > mark.type && mark.type >= 0)
? kVerificationMarkColors[mark.type]
: Colors.blue,
color: (kVerificationMarkColors.length > mark.type && mark.type >= 0)
? kVerificationMarkColors[mark.type]
: Colors.blue,
fill: 1,
).alignment(Alignment.centerLeft),
const Gap(8),

View File

@@ -12,6 +12,7 @@ import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:gap/gap.dart';
import 'package:skeletonizer/skeletonizer.dart';
part 'friends_overview.g.dart';
@@ -50,8 +51,9 @@ class FriendsOverviewWidget extends HookConsumerWidget {
return friendsOverviewAsync.when(
data: (friends) {
// Filter for online friends
final onlineFriends =
friends.where((friend) => friend.status.isOnline).toList();
final onlineFriends = friends
.where((friend) => friend.status.isOnline)
.toList();
if (onlineFriends.isEmpty && hideWhenEmpty) {
return const SizedBox.shrink();
@@ -62,12 +64,23 @@ class FriendsOverviewWidget extends HookConsumerWidget {
child: Column(
children: [
Row(
spacing: 8,
children: [
const Icon(Symbols.group),
Text('friendsOnline').tr(),
Icon(
Symbols.group,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'friendsOnline'.tr(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
],
).padding(horizontal: 16).height(48),
).padding(horizontal: 16, vertical: 12),
if (onlineFriends.isEmpty)
Container(
height: 80,
@@ -105,16 +118,115 @@ class FriendsOverviewWidget extends HookConsumerWidget {
}
return result;
},
loading:
() => const SizedBox(
height: 80,
child: Center(child: CircularProgressIndicator()),
loading: () {
final card = Card(
margin: EdgeInsets.zero,
child: Column(
children: [
Row(
children: [
Icon(
Symbols.group,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'friendsOnline'.tr(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
],
).padding(horizontal: 16, vertical: 12),
SizedBox(
height: 80,
child: ListView(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 4),
scrollDirection: Axis.horizontal,
children: List.generate(
4,
(index) => const SkeletonFriendTile(),
),
),
),
],
),
);
Widget result = Skeletonizer(child: card);
if (padding != null) {
result = Padding(padding: padding!, child: result);
}
return result;
},
error: (error, stack) => const SizedBox.shrink(), // Hide on error
);
}
}
class SkeletonFriendTile extends StatelessWidget {
const SkeletonFriendTile({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 60,
margin: const EdgeInsets.only(right: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Avatar with online indicator
Stack(
children: [
CircleAvatar(
radius: 24,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Text(
'A',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
// Online indicator - green dot for skeleton
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).colorScheme.surface,
width: 2,
),
),
),
),
],
),
const Gap(4),
// Name placeholder
Text(
'Friend',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
).center();
}
}
class _FriendTile extends ConsumerWidget {
final SnFriendOverviewItem friend;
@@ -141,19 +253,19 @@ class _FriendTile extends ConsumerWidget {
children: [
CircleAvatar(
radius: 24,
backgroundImage:
uri != null ? CachedNetworkImageProvider(uri) : null,
child:
uri == null
? Text(
friend.account.nick.isNotEmpty
? friend.account.nick[0].toUpperCase()
: friend.account.name[0].toUpperCase(),
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.onPrimary,
),
)
: null,
backgroundImage: uri != null
? CachedNetworkImageProvider(uri)
: null,
child: uri == null
? Text(
friend.account.nick.isNotEmpty
? friend.account.nick[0].toUpperCase()
: friend.account.name[0].toUpperCase(),
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.onPrimary,
),
)
: null,
),
// Online indicator - show play arrow if user has activities, otherwise green dot
Positioned(
@@ -163,32 +275,28 @@ class _FriendTile extends ConsumerWidget {
width: 16,
height: 16,
decoration: BoxDecoration(
color:
friend.activities.isNotEmpty
? Colors.blue.withOpacity(0.8)
: Colors.green,
shape:
friend.activities.isNotEmpty
? BoxShape.rectangle
: BoxShape.circle,
borderRadius:
friend.activities.isNotEmpty
? BorderRadius.circular(4)
: null,
color: friend.activities.isNotEmpty
? Colors.blue.withOpacity(0.8)
: Colors.green,
shape: friend.activities.isNotEmpty
? BoxShape.rectangle
: BoxShape.circle,
borderRadius: friend.activities.isNotEmpty
? BorderRadius.circular(4)
: null,
border: Border.all(
color: theme.colorScheme.surface,
width: 2,
),
),
child:
friend.activities.isNotEmpty
? Icon(
Symbols.play_arrow,
size: 10,
color: Colors.white,
fill: 1,
)
: null,
child: friend.activities.isNotEmpty
? Icon(
Symbols.play_arrow,
size: 10,
color: Colors.white,
fill: 1,
)
: null,
),
),
],

View File

@@ -96,28 +96,28 @@ void showLoadingModal(BuildContext context) {
if (_loadingOverlay != null) return;
_loadingOverlay = OverlayEntry(
builder:
(context) => _FadeOverlay(
key: _loadingOverlayKey,
child: Material(
color: Colors.black54,
child: Center(
child: Material(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8),
elevation: 4,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(year2023: false),
const Gap(24),
Text('loading'.tr()),
],
).padding(all: 32),
),
builder: (context) => _FadeOverlay(
key: _loadingOverlayKey,
child: Material(
color: Colors.black54,
child: Center(
child: AlertDialog(
content: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
year2023: false,
padding: EdgeInsets.zero,
).width(28).height(28).padding(horizontal: 8),
const Gap(16),
Text('loading'.tr()),
],
),
contentPadding: EdgeInsets.symmetric(horizontal: 32, vertical: 24),
),
),
),
),
);
Overlay.of(context).insert(_loadingOverlay!);
@@ -187,40 +187,39 @@ Future<T?> showOverlayDialog<T>({
}
entry = OverlayEntry(
builder:
(context) => _FadeOverlay(
key: key,
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
builder: (context, animation) {
return Stack(
children: [
Positioned.fill(
child: FadeTransition(
opacity: animation,
child: GestureDetector(
onTap: barrierDismissible ? () => close(null) : null,
behavior: HitTestBehavior.opaque,
child: const ColoredBox(color: Colors.black54),
),
),
builder: (context) => _FadeOverlay(
key: key,
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
builder: (context, animation) {
return Stack(
children: [
Positioned.fill(
child: FadeTransition(
opacity: animation,
child: GestureDetector(
onTap: barrierDismissible ? () => close(null) : null,
behavior: HitTestBehavior.opaque,
child: const ColoredBox(color: Colors.black54),
),
Center(
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.05),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: builder(context, close),
),
),
),
),
Center(
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.05),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: builder(context, close),
),
],
);
},
),
),
),
],
);
},
),
);
_activeOverlayDialogs.add(() => close(null));
@@ -252,77 +251,75 @@ void showErrorAlert(dynamic err, {IconData? icon}) {
};
showOverlayDialog<void>(
builder:
(context, close) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
child: AlertDialog(
title: null,
titlePadding: EdgeInsets.zero,
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon ?? Icons.error_outline_rounded,
size: 48,
color: Theme.of(context).colorScheme.error,
),
const Gap(16),
Text(
'somethingWentWrong'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const Gap(8),
Text(text),
const Gap(8),
],
builder: (context, close) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
child: AlertDialog(
title: null,
titlePadding: EdgeInsets.zero,
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon ?? Icons.error_outline_rounded,
size: 48,
color: Theme.of(context).colorScheme.error,
),
actions: [
TextButton(
onPressed: () => close(null),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
),
const Gap(16),
Text(
'somethingWentWrong'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const Gap(8),
Text(text),
const Gap(8),
],
),
actions: [
TextButton(
onPressed: () => close(null),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
),
),
);
}
void showInfoAlert(String message, String title, {IconData? icon}) {
showOverlayDialog<void>(
builder:
(context, close) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
child: AlertDialog(
title: null,
titlePadding: EdgeInsets.zero,
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon ?? Symbols.info_rounded,
fill: 1,
size: 48,
color: Theme.of(context).colorScheme.primary,
),
const Gap(16),
Text(title, style: Theme.of(context).textTheme.titleLarge),
const Gap(8),
Text(message),
const Gap(8),
],
builder: (context, close) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
child: AlertDialog(
title: null,
titlePadding: EdgeInsets.zero,
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon ?? Symbols.info_rounded,
fill: 1,
size: 48,
color: Theme.of(context).colorScheme.primary,
),
actions: [
TextButton(
onPressed: () => close(null),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
),
const Gap(16),
Text(title, style: Theme.of(context).textTheme.titleLarge),
const Gap(8),
Text(message),
const Gap(8),
],
),
actions: [
TextButton(
onPressed: () => close(null),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
),
),
);
}
@@ -333,50 +330,46 @@ Future<bool> showConfirmAlert(
bool isDanger = false,
}) async {
final result = await showOverlayDialog<bool>(
builder:
(context, close) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
child: AlertDialog(
title: null,
titlePadding: EdgeInsets.zero,
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon ?? Symbols.help_rounded,
size: 48,
fill: 1,
color: Theme.of(context).colorScheme.primary,
),
const Gap(16),
Text(title, style: Theme.of(context).textTheme.titleLarge),
const Gap(8),
Text(message),
const Gap(8),
],
builder: (context, close) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kDialogMaxWidth),
child: AlertDialog(
title: null,
titlePadding: EdgeInsets.zero,
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon ?? Symbols.help_rounded,
size: 48,
fill: 1,
color: Theme.of(context).colorScheme.primary,
),
actions: [
TextButton(
onPressed: () => close(false),
child: Text(
MaterialLocalizations.of(context).cancelButtonLabel,
),
),
TextButton(
onPressed: () => close(true),
style:
isDanger
? TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
)
: null,
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
),
const Gap(16),
Text(title, style: Theme.of(context).textTheme.titleLarge),
const Gap(8),
Text(message),
const Gap(8),
],
),
actions: [
TextButton(
onPressed: () => close(false),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton(
onPressed: () => close(true),
style: isDanger
? TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
)
: null,
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
),
),
);
return result ?? false;
}

View File

@@ -8,15 +8,19 @@ import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:island/pods/config.dart';
import 'package:island/route.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/services/event_bus.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/cmp/pattle.dart';
import 'package:island/widgets/upload_overlay.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shake/shake.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:window_manager/window_manager.dart';
@@ -36,6 +40,13 @@ class WindowScaffold extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isMaximized = useState(false);
final showPalette = useState(false);
final keyboardFocusNode = useFocusNode();
useEffect(() {
keyboardFocusNode.requestFocus();
return null;
}, []);
// Add window resize listener for desktop platforms
useEffect(() {
@@ -68,6 +79,15 @@ class WindowScaffold extends HookConsumerWidget {
return null;
}, []);
// Event bus listener for command palette
final subscription = useMemoized(
() => eventBus.on<CommandPaletteTriggerEvent>().listen(
(_) => showPalette.value = true,
),
[],
);
useEffect(() => subscription.cancel, [subscription]);
final router = ref.watch(routerProvider);
final pageActionsButton = [
@@ -92,135 +112,166 @@ class WindowScaffold extends HookConsumerWidget {
const Gap(8),
];
final popHotKey = HotKey(
identifier: 'return_previous_page',
key: PhysicalKeyboardKey.escape,
scope: HotKeyScope.inapp,
);
final cmpHotKey = HotKey(
identifier: 'open_command_pattle',
key: PhysicalKeyboardKey.tab,
modifiers: [HotKeyModifier.shift],
scope: HotKeyScope.inapp,
);
useEffect(() {
hotKeyManager.register(
popHotKey,
keyDownHandler: (_) {
if (closeTopmostOverlayDialog()) {
return;
}
// If no overlay to close, pop the route
if (ref.watch(routerProvider).canPop()) {
ref.read(routerProvider).pop();
}
},
);
hotKeyManager.register(
cmpHotKey,
keyDownHandler: (_) {
showPalette.value = true;
},
);
ShakeDetector detector = ShakeDetector.autoStart(
onPhoneShake: (_) {
showPalette.value = true;
},
);
return () {
hotKeyManager.unregister(popHotKey);
hotKeyManager.unregister(cmpHotKey);
detector.stopListening();
};
}, []);
if (!kIsWeb &&
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.escape): const PopIntent(),
},
child: Actions(
actions: <Type, Action<Intent>>{PopIntent: PopAction(ref)},
child: Material(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Stack(
fit: StackFit.expand,
return Material(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Stack(
fit: StackFit.expand,
children: [
Column(
children: [
Column(
children: [
DragToMoveArea(
child:
Platform.isMacOS
? Stack(
alignment: Alignment.center,
DragToMoveArea(
child: Platform.isMacOS
? Stack(
alignment: Alignment.center,
children: [
if (isWideScreen(context))
Row(
children: [
if (isWideScreen(context))
Row(
children: [
const Spacer(),
...pageActionsButton,
],
)
else
SizedBox(height: 32),
Text(
'Solar Network',
textAlign: TextAlign.center,
style: TextStyle(
color:
Theme.of(
context,
).colorScheme.onSurface,
),
),
const Spacer(),
...pageActionsButton,
],
)
: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
else
SizedBox(height: 32),
Text(
'Solar Network',
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
)
: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Expanded(
child: Row(
children: [
Image.asset(
Theme.of(context).brightness ==
Brightness.dark
? 'assets/icons/icon-dark.png'
: 'assets/icons/icon.png',
width: 20,
height: 20,
),
const SizedBox(width: 8),
Text(
'Solar Network',
textAlign: TextAlign.start,
),
],
).padding(horizontal: 12, vertical: 5),
Image.asset(
Theme.of(context).brightness ==
Brightness.dark
? 'assets/icons/icon-dark.png'
: 'assets/icons/icon.png',
width: 20,
height: 20,
),
IconButton(
icon: Icon(Symbols.minimize),
onPressed: () => windowManager.minimize(),
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
IconButton(
icon: Icon(
isMaximized.value
? Symbols.fullscreen_exit
: Symbols.fullscreen,
),
onPressed: () async {
if (await windowManager.isMaximized()) {
windowManager.restore();
} else {
windowManager.maximize();
}
},
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
IconButton(
icon: Icon(Symbols.close),
onPressed: () => windowManager.hide(),
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
const SizedBox(width: 8),
Text(
'Solar Network',
textAlign: TextAlign.start,
),
],
).padding(horizontal: 12, vertical: 5),
),
IconButton(
icon: Icon(Symbols.minimize),
onPressed: () => windowManager.minimize(),
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
IconButton(
icon: Icon(
isMaximized.value
? Symbols.fullscreen_exit
: Symbols.fullscreen,
),
),
Expanded(child: child),
],
onPressed: () async {
if (await windowManager.isMaximized()) {
windowManager.restore();
} else {
windowManager.maximize();
}
},
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
IconButton(
icon: Icon(Symbols.close),
onPressed: () => windowManager.hide(),
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
],
),
),
_WebSocketIndicator(),
const UploadOverlay(),
Expanded(child: child),
],
),
),
_WebSocketIndicator(),
const UploadOverlay(),
if (showPalette.value)
CommandPattleWidget(onDismiss: () => showPalette.value = false),
],
),
);
}
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.escape): const PopIntent(),
},
child: Actions(
actions: <Type, Action<Intent>>{PopIntent: PopAction(ref)},
child: Stack(
fit: StackFit.expand,
children: [
Positioned.fill(child: child),
_WebSocketIndicator(),
const UploadOverlay(),
],
),
),
return Stack(
fit: StackFit.expand,
children: [
Positioned.fill(child: child),
_WebSocketIndicator(),
const UploadOverlay(),
if (showPalette.value)
CommandPattleWidget(onDismiss: () => showPalette.value = false),
],
);
}
}
@@ -352,29 +403,6 @@ class AppScaffold extends HookConsumerWidget {
}
}
class PopIntent extends Intent {
const PopIntent();
}
class PopAction extends Action<PopIntent> {
final WidgetRef ref;
PopAction(this.ref);
@override
void invoke(PopIntent intent) {
// First, try to close any overlay dialogs
if (closeTopmostOverlayDialog()) {
return;
}
// If no overlay to close, pop the route
if (ref.watch(routerProvider).canPop()) {
ref.read(routerProvider).pop();
}
}
}
class PageBackButton extends StatelessWidget {
final Color? color;
final List<Shadow>? shadows;
@@ -407,8 +435,8 @@ class PageBackButton extends StatelessWidget {
color: color,
context.canPop()
? (!kIsWeb && (Platform.isMacOS || Platform.isIOS))
? Symbols.arrow_back_ios_new
: Symbols.arrow_back
? Symbols.arrow_back_ios_new
: Symbols.arrow_back
: Symbols.home,
shadows: shadows,
),
@@ -463,11 +491,10 @@ class AppBackground extends ConsumerWidget {
);
},
loading: () => const SizedBox(),
error:
(_, _) => Material(
color: Theme.of(context).colorScheme.surface,
child: child,
),
error: (_, _) => Material(
color: Theme.of(context).colorScheme.surface,
child: child,
),
);
}
@@ -496,53 +523,113 @@ class _WebSocketIndicator extends HookConsumerWidget {
final isDesktop =
!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux);
final devicePadding = MediaQuery.of(context).padding;
final user = ref.watch(userInfoProvider);
final websocketState = ref.watch(websocketStateProvider);
final indicatorHeight =
MediaQuery.of(context).padding.top + (isDesktop ? 27.5 : 25);
Color indicatorColor;
String indicatorText;
Widget indicatorIcon;
bool isInteractive = true;
double opacity = 0.0;
if (websocketState == WebSocketState.connected()) {
indicatorColor = Colors.green;
indicatorText = 'connectionConnected';
indicatorIcon = Icon(
key: ValueKey('ws_connected'),
Symbols.power,
color: Colors.white,
size: 16,
);
opacity = 0.0;
isInteractive = false;
} else if (websocketState == WebSocketState.connecting()) {
indicatorColor = Colors.teal;
indicatorText = 'connectionReconnecting';
indicatorIcon = SizedBox(
key: ValueKey('ws_connecting'),
width: 16,
height: 16,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
strokeWidth: 2,
padding: EdgeInsets.zero,
),
);
opacity = 1.0;
isInteractive = false;
} else if (websocketState == WebSocketState.serverDown()) {
indicatorColor = Colors.red;
indicatorText = 'connectionServerDown';
isInteractive = true;
indicatorIcon = Icon(
key: ValueKey('ws_server_down'),
Symbols.power_off,
color: Colors.white,
size: 16,
);
opacity = 1.0;
} else {
indicatorColor = Colors.red;
indicatorText = 'connectionDisconnected';
indicatorIcon = Icon(
key: ValueKey('ws_disconnected'),
Symbols.power_off,
color: Colors.white,
size: 16,
);
opacity = 1.0;
isInteractive = false;
}
return AnimatedPositioned(
duration: Duration(milliseconds: 1850),
top:
user.value == null ||
user.value == null ||
websocketState == WebSocketState.connected()
? -indicatorHeight
: 0,
curve: Curves.fastLinearToSlowEaseIn,
return Positioned(
top: devicePadding.top + (isDesktop ? 27.5 : 25),
left: 0,
right: 0,
height: indicatorHeight,
child: IgnorePointer(
child: Material(
elevation:
user.value == null || websocketState == WebSocketState.connected()
ignoring: !isInteractive,
child: Align(
alignment: Alignment.topCenter,
child: AnimatedOpacity(
duration: Duration(milliseconds: 300),
opacity: opacity,
child: Material(
elevation:
user.value == null ||
websocketState == WebSocketState.connected()
? 0
: 4,
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
color: indicatorColor,
child: Center(
child:
Text(
indicatorText,
style: TextStyle(color: Colors.white, fontSize: 16),
).tr(),
).padding(top: MediaQuery.of(context).padding.top),
borderRadius: BorderRadius.circular(999),
child: GestureDetector(
onTap: () {
ref.read(websocketStateProvider.notifier).manualReconnect();
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: indicatorColor,
borderRadius: BorderRadius.circular(999),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: indicatorIcon,
),
Text(
indicatorText,
style: TextStyle(color: Colors.white, fontSize: 13),
).tr(),
],
),
),
),
),
),
),
),

View File

@@ -1,8 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:in_app_review/in_app_review.dart';
import 'package:protocol_handler/protocol_handler.dart';
import 'package:island/pods/activity/activity_rpc.dart';
import 'package:island/pods/config.dart';
@@ -17,109 +18,228 @@ 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:island/widgets/post/compose_sheet.dart';
import 'package:island/screens/notification.dart';
import 'package:island/screens/thought/think_sheet.dart';
import 'package:island/services/event_bus.dart';
import 'package:snow_fall_animation/snow_fall_animation.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';
class AppWrapper extends ConsumerStatefulWidget {
class AppWrapper extends HookConsumerWidget {
final Widget child;
const AppWrapper({super.key, required this.child});
@override
ConsumerState<AppWrapper> createState() => _AppWrapperState();
}
Widget build(BuildContext context, WidgetRef ref) {
final networkStateShowing = useState(false);
final websocketState = ref.watch(websocketStateProvider);
final apiState = ref.watch(networkStatusProvider);
final isShowSnow = useState(false);
final isSnowGone = useState(false);
class _AppWrapperState extends ConsumerState<AppWrapper>
with ProtocolListener, TrayListener {
StreamSubscription? ntySubs;
bool networkStateShowing = false;
// Handle network status modal
useEffect(() {
bool triedOpen = false;
if (websocketState == WebSocketState.duplicateDevice() &&
!networkStateShowing.value &&
!triedOpen) {
networkStateShowing.value = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => NetworkStatusSheet(autoClose: true),
).then((_) => networkStateShowing.value = false);
});
triedOpen = true;
}
@override
void initState() {
super.initState();
protocolHandler.addListener(this);
Future(() async {
if (mounted) ntySubs = setupNotificationListener(context, ref);
if (apiState != NetworkStatus.online &&
!networkStateShowing.value &&
!triedOpen) {
networkStateShowing.value = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => const NetworkStatusSheet(),
).then((_) => networkStateShowing.value = false);
});
triedOpen = true;
}
return null;
}, [websocketState, apiState]);
// Initialize services and listeners
useEffect(() {
final ntySubs = setupNotificationListener(context, ref);
final sharingService = SharingIntentService();
if (mounted) sharingService.initialize(context);
if (mounted) UpdateService().checkForUpdates(context);
sharingService.initialize(context);
UpdateService().checkForUpdates(context);
TrayService.instance.initialize(this);
final trayService = TrayService.instance;
trayService.initialize(
_TrayListenerImpl(
onTrayIconMouseDown: () => windowManager.show(),
onTrayIconRightMouseUp: () => trayManager.popUpContextMenu(),
onTrayMenuItemClick: (menuItem) => trayService.handleAction(menuItem),
),
);
ref.read(rpcServerStateProvider.notifier).start();
ref.read(webAuthServerStateProvider.notifier).start();
final initialUrl = await protocolHandler.getInitialUrl();
if (initialUrl != null && mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleDeepLink(Uri.parse(initialUrl), ref);
// Listen to special action events
final composeSheetSubs = eventBus.on<ShowComposeSheetEvent>().listen((
event,
) {
if (context.mounted) _showComposeSheet(context);
});
final notificationSheetSubs = eventBus
.on<ShowNotificationSheetEvent>()
.listen((event) {
if (context.mounted) _showNotificationSheet(context);
});
final thoughtSheetSubs = eventBus.on<ShowThoughtSheetEvent>().listen((
event,
) {
if (context.mounted) _showThoughtSheet(context, event);
});
// Protocol handler listener
final protocolListener = _ProtocolListenerImpl(
onProtocolUrlReceived: (url) =>
_handleDeepLink(Uri.parse(url), ref, context),
);
protocolHandler.addListener(protocolListener);
// Handle initial URL
protocolHandler.getInitialUrl().then((initialUrl) {
if (initialUrl != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleDeepLink(Uri.parse(initialUrl), ref, context);
});
}
});
return () {
protocolHandler.removeListener(protocolListener);
ref.read(rpcServerProvider).stop();
trayService.dispose(
_TrayListenerImpl(
onTrayIconMouseDown: () => {},
onTrayIconRightMouseUp: () => {},
onTrayMenuItemClick: (menuItem) => {},
),
);
ntySubs?.cancel();
composeSheetSubs.cancel();
notificationSheetSubs.cancel();
thoughtSheetSubs.cancel();
};
}, []);
final settings = ref.watch(appSettingsProvider);
final settingsNotifier = ref.watch(appSettingsProvider.notifier);
useEffect(() {
if (settings.defaultScreen != null &&
settings.defaultScreen != 'dashboard') {
Future(() {
ref.read(routerProvider).goNamed(settings.defaultScreen!);
});
}
});
}
return null;
}, []);
@override
void dispose() {
protocolHandler.removeListener(this);
ref.read(rpcServerProvider).stop();
TrayService.instance.dispose(this);
ntySubs?.cancel();
super.dispose();
}
final now = DateTime.now();
final doesShowSnow =
settings.festivalFeatures &&
now.month == 12 &&
(now.day >= 22 && now.day <= 28);
@override
Widget build(BuildContext context) {
final wsNotifier = ref.watch(websocketStateProvider.notifier);
final websocketState = ref.watch(websocketStateProvider);
if (websocketState == WebSocketState.duplicateDevice()) {
if (!networkStateShowing) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() => networkStateShowing = true);
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: false,
builder:
(context) =>
NetworkStatusSheet(onReconnect: () => wsNotifier.connect()),
).then((_) => setState(() => networkStateShowing = false));
useEffect(() {
final now = DateTime.now();
if (doesShowSnow) {
isShowSnow.value = true;
Future.delayed(const Duration(seconds: 60), () {
if (!context.mounted) return;
isShowSnow.value = false;
Future.delayed(const Duration(seconds: 3), () {
if (!context.mounted) return;
isSnowGone.value = true;
});
});
}
}
return TourTriggerWidget(key: UniqueKey(), child: widget.child);
if (settings.firstLaunchAt == null) {
settingsNotifier.setFirstLaunchAt(now.toIso8601String());
} else if (!settings.askedReview) {
final launchAt = DateTime.parse(settings.firstLaunchAt!);
final daysSinceFirstLaunch = now.difference(launchAt).inDays;
if (daysSinceFirstLaunch >= 3 &&
!kIsWeb &&
(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) {
final InAppReview inAppReview = InAppReview.instance;
Future(() async {
if (await inAppReview.isAvailable()) {
inAppReview.requestReview();
}
});
settingsNotifier.setAskedReview(true);
}
}
return null;
}, []);
return TourTriggerWidget(
key: const Key("app_tour_trigger"),
child: Stack(
children: [
child,
if (doesShowSnow && !isSnowGone.value)
IgnorePointer(
child: AnimatedOpacity(
opacity: isShowSnow.value ? 1 : 00,
duration: const Duration(seconds: 3),
child: SnowFallAnimation(
key: const Key("app_snow_animation"),
config: SnowfallConfig(numberOfSnowflakes: 50, speed: 1.0),
),
),
),
],
),
);
}
@override
void onProtocolUrlReceived(String url) {
_handleDeepLink(Uri.parse(url), ref);
void _showComposeSheet(BuildContext context) {
PostComposeSheet.show(context);
}
void _trayIconPrimaryAction() {
windowManager.show();
void _showNotificationSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => const NotificationSheet(),
);
}
void _trayIconSecondaryAction() {
trayManager.popUpContextMenu();
void _showThoughtSheet(BuildContext context, ShowThoughtSheetEvent event) {
ThoughtSheet.show(
context,
initialMessage: event.initialMessage,
attachedMessages: event.attachedMessages,
attachedPosts: event.attachedPosts,
);
}
@override
void onTrayIconMouseUp() {
_trayIconPrimaryAction();
}
@override
void onTrayIconRightMouseDown() {
_trayIconSecondaryAction();
}
@override
void onTrayMenuItemClick(MenuItem menuItem) {
TrayService.instance.handleAction(menuItem);
}
void _handleDeepLink(Uri uri, WidgetRef ref) async {
void _handleDeepLink(Uri uri, WidgetRef ref, BuildContext context) async {
String path = '/${uri.host}${uri.path}';
// Special handling for OIDC auth callback
@@ -129,9 +249,7 @@ class _AppWrapperState extends ConsumerState<AppWrapper>
ref.invalidate(tokenProvider);
// Do post login tasks
if (mounted) {
await performPostLogin(context, ref);
}
await performPostLogin(context, ref);
if (!kIsWeb &&
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
@@ -153,10 +271,9 @@ class _AppWrapperState extends ConsumerState<AppWrapper>
final router = ref.read(routerProvider);
if (uri.queryParameters.isNotEmpty) {
path =
Uri.parse(
path,
).replace(queryParameters: uri.queryParameters).toString();
path = Uri.parse(
path,
).replace(queryParameters: uri.queryParameters).toString();
}
router.push(path);
if (!kIsWeb &&
@@ -165,3 +282,42 @@ class _AppWrapperState extends ConsumerState<AppWrapper>
}
}
}
class _TrayListenerImpl implements TrayListener {
final VoidCallback _primaryAction;
final VoidCallback _secondaryAction;
final void Function(MenuItem) _onTrayMenuItemClick;
_TrayListenerImpl({
required VoidCallback onTrayIconMouseDown,
required VoidCallback onTrayIconRightMouseUp,
required void Function(MenuItem) onTrayMenuItemClick,
}) : _primaryAction = onTrayIconMouseDown,
_secondaryAction = onTrayIconRightMouseUp,
_onTrayMenuItemClick = onTrayMenuItemClick;
@override
void onTrayIconMouseDown() => _primaryAction();
@override
void onTrayIconRightMouseUp() => _secondaryAction();
@override
void onTrayIconMouseUp() => _primaryAction();
@override
void onTrayIconRightMouseDown() => _secondaryAction();
@override
void onTrayMenuItemClick(MenuItem menuItem) => _onTrayMenuItemClick(menuItem);
}
class _ProtocolListenerImpl implements ProtocolListener {
final void Function(String) _onProtocolUrlReceived;
_ProtocolListenerImpl({required void Function(String) onProtocolUrlReceived})
: _onProtocolUrlReceived = onProtocolUrlReceived;
@override
void onProtocolUrlReceived(String url) => _onProtocolUrlReceived(url);
}

View File

@@ -43,6 +43,7 @@ class AudioCallButton extends HookConsumerWidget {
isLoading.value = true;
try {
await apiClient.post('/sphere/chat/realtime/${room.id}');
ref.invalidate(ongoingCallProvider(room.id));
// Just join the room, the overlay will handle the UI
await callNotifier.joinRoom(room);
} catch (e) {

View File

@@ -1,12 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/chat/call.dart';
import 'package:island/widgets/chat/call_participant_tile.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:styled_widget/styled_widget.dart';
class CallStageView extends HookConsumerWidget {
final List<CallParticipantLive> participants;
final double? outerMaxHeight;
const CallStageView({
super.key,
required this.participants,
this.outerMaxHeight,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final focusedIndex = useState<int>(0);
final focusedParticipant = participants[focusedIndex.value];
final otherParticipants = participants
.where((p) => p != focusedParticipant)
.toList();
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Focused participant (takes most space)
LayoutBuilder(
builder: (context, constraints) {
// Calculate dynamic width based on available space
final maxWidth = constraints.maxWidth * 0.8;
final maxHeight = (outerMaxHeight ?? constraints.maxHeight) * 0.6;
return Container(
constraints: BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: CallParticipantTile(
live: focusedParticipant,
allTiles: true,
),
),
);
},
),
// Horizontal list of other participants
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (final participant in otherParticipants)
Padding(
padding: const EdgeInsets.all(8),
child: SizedBox(
width: 180,
child: GestureDetector(
onTapDown: (_) {
final newIndex = participants.indexOf(participant);
focusedIndex.value = newIndex;
},
child: CallParticipantTile(
live: participant,
radius: 32,
allTiles: true,
),
),
),
),
],
),
),
],
);
}
}
class CallContent extends HookConsumerWidget {
const CallContent({super.key});
final double? outerMaxHeight;
const CallContent({super.key, this.outerMaxHeight});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -34,7 +111,7 @@ class CallContent extends HookConsumerWidget {
);
if (allAudioOnly) {
// Audio-only: show avatars in a compact row
// Audio-only: show avatars in a compact row with animated containers
return Center(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
@@ -45,36 +122,49 @@ class CallContent extends HookConsumerWidget {
runSpacing: 8,
children: [
for (final live in participants)
SpeakingRippleAvatar(
live: live,
size: 72,
).padding(horizontal: 4),
Padding(
padding: const EdgeInsets.all(8),
child: SpeakingRippleAvatar(live: live, size: 72),
),
],
),
),
);
}
// Show all participants in a responsive grid
return LayoutBuilder(
builder: (context, constraints) {
// Calculate width for responsive 2-column layout
final itemWidth = (constraints.maxWidth / 2) - 16;
if (callState.viewMode == ViewMode.stage) {
// Stage: allow user to select a participant to focus, show others below
return CallStageView(
participants: participants,
outerMaxHeight: outerMaxHeight,
);
} else {
// Grid: show all participants in a responsive grid
return LayoutBuilder(
builder: (context, constraints) {
// Calculate width for responsive 2-column layout
final itemWidth = (constraints.maxWidth / 2) - 16;
return Wrap(
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: [
for (final participant in participants)
SizedBox(
width: itemWidth,
child: CallParticipantTile(live: participant),
),
],
);
},
);
return SingleChildScrollView(
child: Wrap(
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: [
for (final participant in participants)
SizedBox(
width: itemWidth,
child: CallParticipantTile(
live: participant,
allTiles: true,
),
),
],
),
);
},
);
}
}
}

View File

@@ -4,7 +4,6 @@ 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/account.dart';
import 'package:island/models/chat.dart';
import 'package:island/pods/chat/call.dart';
import 'package:island/pods/userinfo.dart';
@@ -18,10 +17,16 @@ import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:collection/collection.dart';
class CallControlsBar extends HookConsumerWidget {
final bool isCompact;
const CallControlsBar({super.key, this.isCompact = false});
final bool popOnLeaves;
const CallControlsBar({
super.key,
this.isCompact = false,
this.popOnLeaves = false,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -41,91 +46,97 @@ class CallControlsBar extends HookConsumerWidget {
_buildCircularButtonWithDropdown(
context: context,
ref: ref,
icon:
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
icon: callState.isCameraEnabled
? Symbols.videocam
: Symbols.videocam_off,
onPressed: () => callNotifier.toggleCamera(),
backgroundColor: const Color(0xFF424242),
hasDropdown: true,
deviceType: 'videoinput',
),
_buildCircularButton(
icon:
callState.isScreenSharing
? Icons.stop_screen_share
: Icons.screen_share,
icon: callState.isScreenSharing
? Symbols.stop_screen_share
: Symbols.screen_share,
onPressed: () => callNotifier.toggleScreenShare(context),
backgroundColor: const Color(0xFF424242),
),
_buildCircularButtonWithDropdown(
context: context,
ref: ref,
icon: callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
icon: callState.isMicrophoneEnabled ? Symbols.mic : Symbols.mic_off,
onPressed: () => callNotifier.toggleMicrophone(),
backgroundColor: const Color(0xFF424242),
hasDropdown: true,
deviceType: 'audioinput',
),
_buildCircularButton(
icon:
callState.isSpeakerphone
? Symbols.mobile_speaker
: Symbols.ear_sound,
icon: callState.isSpeakerphone
? Symbols.mobile_speaker
: Symbols.ear_sound,
onPressed: () => callNotifier.toggleSpeakerphone(),
backgroundColor: const Color(0xFF424242),
),
_buildCircularButton(
icon: callState.viewMode == ViewMode.grid
? Symbols.grid_view
: Symbols.view_list,
onPressed: () => callNotifier.toggleViewMode(),
backgroundColor: const Color(0xFF424242),
),
_buildCircularButton(
icon: Icons.call_end,
onPressed:
() => showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder:
(innerContext) => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Gap(24),
ListTile(
leading: const Icon(Symbols.logout, fill: 1),
title: Text('callLeave').tr(),
onTap: () {
callNotifier.disconnect();
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
Navigator.of(innerContext).pop();
},
),
ListTile(
leading: const Icon(Symbols.call_end, fill: 1),
iconColor: Colors.red,
title: Text('callEnd').tr(),
onTap: () async {
callNotifier.disconnect();
final apiClient = ref.watch(apiClientProvider);
try {
showLoadingModal(context);
await apiClient.delete(
'/sphere/chat/realtime/${callNotifier.roomId}',
);
callNotifier.dispose();
if (context.mounted) {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
Navigator.of(innerContext).pop();
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
),
onPressed: () => showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (innerContext) => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Gap(24),
ListTile(
leading: const Icon(Symbols.logout, fill: 1),
title: Text('callLeave').tr(),
onTap: () {
callNotifier.disconnect();
if (popOnLeaves) {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
Navigator.of(innerContext).pop();
}
},
),
ListTile(
leading: const Icon(Symbols.call_end, fill: 1),
iconColor: Colors.red,
title: Text('callEnd').tr(),
onTap: () async {
callNotifier.disconnect();
final apiClient = ref.watch(apiClientProvider);
try {
showLoadingModal(context);
await apiClient.delete(
'/sphere/chat/realtime/${callNotifier.roomId}',
);
callNotifier.dispose();
if (context.mounted && popOnLeaves) {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
Navigator.of(innerContext).pop();
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
),
backgroundColor: const Color(0xFFE53E3E),
iconColor: Colors.white,
),
@@ -185,12 +196,11 @@ class CallControlsBar extends HookConsumerWidget {
bottom: 0,
right: isCompact ? 0 : -4,
child: Material(
color:
Colors
.transparent, // Make Material transparent to show underlying color
color: Colors
.transparent, // Make Material transparent to show underlying color
child: InkWell(
onTap:
() => _showDeviceSelectionDialog(context, ref, deviceType),
onTap: () =>
_showDeviceSelectionDialog(context, ref, deviceType),
borderRadius: BorderRadius.circular((isCompact ? 16 : 24) / 2),
child: Container(
width: isCompact ? 16 : 24,
@@ -232,10 +242,9 @@ class CallControlsBar extends HookConsumerWidget {
context: context,
builder: (BuildContext dialogContext) {
return SheetScaffold(
titleText:
deviceType == 'videoinput'
? 'selectCamera'.tr()
: 'selectMicrophone'.tr(),
titleText: deviceType == 'videoinput'
? 'selectCamera'.tr()
: 'selectMicrophone'.tr(),
child: ListView.builder(
itemCount: devices.length,
itemBuilder: (context, index) {
@@ -312,21 +321,66 @@ class CallOverlayBar extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final callState = ref.watch(callProvider);
// Use selective watching to reduce rebuilds
final isConnected = ref.watch(
callProvider.select((state) => state.isConnected),
);
final duration = ref.watch(callProvider.select((state) => state.duration));
final isMicrophoneEnabled = ref.watch(
callProvider.select((state) => state.isMicrophoneEnabled),
);
final callNotifier = ref.read(callProvider.notifier);
final ongoingCall = ref.watch(ongoingCallProvider(room.id));
// Memoize expensive computations
final lastSpeaker = useMemoized(() {
final participants = callNotifier.participants;
if (participants.isEmpty) return null;
final speakers = participants.where(
(element) => element.remoteParticipant.lastSpokeAt != null,
);
if (speakers.isEmpty) return participants.first;
return speakers.fold<CallParticipantLive?>(null, (previous, current) {
if (previous == null) return current;
return current.remoteParticipant.lastSpokeAt!.compareTo(
previous.remoteParticipant.lastSpokeAt!,
) >
0
? current
: previous;
});
}, [callNotifier.participants]);
final userInfo = ref.watch(userInfoProvider).value!;
// Memoize chat room name
final chatRoomName = useMemoized(() {
final room = callNotifier.chatRoom;
if (room == null) return 'unnamed'.tr();
return room.name ??
(room.members ?? [])
.where((element) => element.id != userInfo.id)
.map((element) => element.account.nick)
.first;
}, [callNotifier.chatRoom, userInfo]);
// State for overlay mode: compact or preview
// Default to true (preview mode) so user sees video immediately after joining
final isExpanded = useState(true);
Widget child;
if (callState.isConnected) {
if (isConnected) {
child = _buildActiveCallOverlay(
context,
ref,
callState,
duration,
isMicrophoneEnabled,
callNotifier,
lastSpeaker,
chatRoomName,
isExpanded,
);
} else if (ongoingCall.value != null) {
@@ -416,55 +470,20 @@ class CallOverlayBar extends HookConsumerWidget {
);
}
String _getChatRoomName(SnChatRoom? room, SnAccount currentUser) {
if (room == null) return 'unnamed'.tr();
return room.name ??
(room.members ?? [])
.where((element) => element.id != currentUser.id)
.map((element) => element.account.nick)
.first;
}
Widget _buildActiveCallOverlay(
BuildContext context,
WidgetRef ref,
CallState callState,
Duration duration,
bool isMicrophoneEnabled,
CallNotifier callNotifier,
CallParticipantLive? lastSpeaker,
String chatRoomName,
ValueNotifier<bool> isExpanded,
) {
final lastSpeaker =
callNotifier.participants
.where(
(element) => element.remoteParticipant.lastSpokeAt != null,
)
.isEmpty
? callNotifier.participants.firstOrNull
: callNotifier.participants
.where(
(element) => element.remoteParticipant.lastSpokeAt != null,
)
.fold(
callNotifier.participants.firstOrNull,
(value, element) =>
element.remoteParticipant.lastSpokeAt != null &&
(value?.remoteParticipant.lastSpokeAt == null ||
element.remoteParticipant.lastSpokeAt!
.compareTo(
value!
.remoteParticipant
.lastSpokeAt!,
) >
0)
? element
: value,
);
if (lastSpeaker == null) {
return const SizedBox.shrink(key: ValueKey('active_waiting'));
}
final userInfo = ref.watch(userInfoProvider).value!;
// Preview Mode (Expanded)
if (isExpanded.value) {
return Card(
@@ -478,9 +497,9 @@ class CallOverlayBar extends HookConsumerWidget {
Row(
children: [
const Gap(4),
Text(_getChatRoomName(callNotifier.chatRoom, userInfo)),
Text(chatRoomName),
const Gap(4),
Text(formatDuration(callState.duration)).bold(),
Text(formatDuration(duration)).bold(),
const Spacer(),
OpenContainer(
closedElevation: 0,
@@ -488,16 +507,15 @@ class CallOverlayBar extends HookConsumerWidget {
openColor: Theme.of(context).scaffoldBackgroundColor,
middleColor: Theme.of(context).scaffoldBackgroundColor,
openBuilder: (context, action) => CallScreen(room: room),
closedBuilder:
(context, openContainer) => IconButton(
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
icon: const Icon(Icons.fullscreen),
onPressed: openContainer,
tooltip: 'Full Screen',
),
closedBuilder: (context, openContainer) => IconButton(
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
icon: const Icon(Icons.fullscreen),
onPressed: openContainer,
tooltip: 'Full Screen',
),
),
IconButton(
visualDensity: const VisualDensity(
@@ -512,10 +530,10 @@ class CallOverlayBar extends HookConsumerWidget {
).padding(horizontal: 12, vertical: 8),
// Video Preview
Container(
height: 200,
height: 320,
width: double.infinity,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: const CallContent(),
child: const CallContent(outerMaxHeight: 320),
),
const CallControlsBar(
isCompact: true,
@@ -540,11 +558,10 @@ class CallOverlayBar extends HookConsumerWidget {
SizedBox(
width: 40,
height: 40,
child:
SpeakingRippleAvatar(
live: lastSpeaker,
size: 36,
).center(),
child: SpeakingRippleAvatar(
live: lastSpeaker,
size: 36,
).center(),
),
const Gap(8),
Column(
@@ -555,11 +572,11 @@ class CallOverlayBar extends HookConsumerWidget {
spacing: 4,
children: [
Text(
_getChatRoomName(callNotifier.chatRoom, userInfo),
chatRoomName,
style: Theme.of(context).textTheme.bodySmall,
),
Text(
formatDuration(callState.duration),
formatDuration(duration),
style: Theme.of(context).textTheme.bodySmall,
),
],
@@ -571,7 +588,7 @@ class CallOverlayBar extends HookConsumerWidget {
),
IconButton(
icon: Icon(
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
size: 20,
),
onPressed: () {

View File

@@ -83,24 +83,21 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
alignment: Alignment.center,
decoration: const BoxDecoration(shape: BoxShape.circle),
child: account.when(
data:
(value) => CallParticipantGestureDetector(
participant: live,
child: ProfilePictureWidget(
file: value.profile.picture,
radius: size / 2,
),
),
error:
(_, _) => CircleAvatar(
radius: size / 2,
child: const Icon(Symbols.person_remove),
),
loading:
() => CircleAvatar(
radius: size / 2,
child: CircularProgressIndicator(),
),
data: (value) => CallParticipantGestureDetector(
participant: live,
child: ProfilePictureWidget(
file: value.profile.picture,
radius: size / 2,
),
),
error: (_, _) => CircleAvatar(
radius: size / 2,
child: const Icon(Symbols.question_mark),
),
loading: () => CircleAvatar(
radius: size / 2,
child: CircularProgressIndicator(),
),
),
),
if (live.remoteParticipant.isMuted)
@@ -130,12 +127,20 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
class CallParticipantTile extends HookConsumerWidget {
final CallParticipantLive live;
final bool allTiles;
final double radius;
const CallParticipantTile({super.key, required this.live});
const CallParticipantTile({
super.key,
required this.live,
this.allTiles = false,
this.radius = 48,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userInfo = ref.watch(accountProvider(live.participant.name));
final account = ref.watch(accountProvider(live.participant.identity));
final hasVideo =
live.hasVideo &&
@@ -143,7 +148,7 @@ class CallParticipantTile extends HookConsumerWidget {
.where((pub) => pub.track != null && pub.kind == TrackType.VIDEO)
.isNotEmpty;
if (hasVideo) {
if (hasVideo || allTiles) {
return Padding(
padding: const EdgeInsets.all(8),
child: LayoutBuilder(
@@ -166,12 +171,11 @@ class CallParticipantTile extends HookConsumerWidget {
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color:
isSpeaking
? Colors.green.withOpacity(
0.5 + 0.5 * audioLevel.clamp(0.0, 1.0),
)
: Theme.of(context).colorScheme.outlineVariant,
color: isSpeaking
? Colors.green.withOpacity(
0.5 + 0.5 * audioLevel.clamp(0.0, 1.0),
)
: Theme.of(context).colorScheme.outlineVariant,
width: isSpeaking ? 4 : 1,
),
),
@@ -182,14 +186,37 @@ class CallParticipantTile extends HookConsumerWidget {
child: Stack(
fit: StackFit.expand,
children: [
VideoTrackRenderer(
live.remoteParticipant.trackPublications.values
.where((track) => track.kind == TrackType.VIDEO)
.first
.track
as VideoTrack,
renderMode: VideoRenderMode.platformView,
),
if (hasVideo)
VideoTrackRenderer(
live.remoteParticipant.trackPublications.values
.where(
(track) => track.kind == TrackType.VIDEO,
)
.first
.track
as VideoTrack,
renderMode: VideoRenderMode.platformView,
)
else
Center(
child: account.when(
data: (value) => CallParticipantGestureDetector(
participant: live,
child: ProfilePictureWidget(
file: value.profile.picture,
radius: radius,
),
),
error: (_, _) => CircleAvatar(
radius: radius,
child: const Icon(Symbols.question_mark),
),
loading: () => CircleAvatar(
radius: radius,
child: CircularProgressIndicator(),
),
),
),
Positioned(
left: 8,
bottom: 8,

View File

@@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/chat.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:relative_time/relative_time.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:easy_localization/easy_localization.dart';
class ChatRoomAvatar extends StatelessWidget {
final SnChatRoom room;
final bool isDirect;
final AsyncValue<SnChatSummary?> summary;
final List<SnChatMember> validMembers;
const ChatRoomAvatar({
super.key,
required this.room,
required this.isDirect,
required this.summary,
required this.validMembers,
});
@override
Widget build(BuildContext context) {
final avatarChild = (isDirect && room.picture?.id == null)
? SplitAvatarWidget(
filesId: validMembers
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.picture?.id == null
? CircleAvatar(child: Text((room.name ?? 'DM')[0].toUpperCase()))
: ProfilePictureWidget(fileId: room.picture?.id);
final badgeChild = Badge(
isLabelVisible: summary.when(
data: (data) => (data?.unreadCount ?? 0) > 0,
loading: () => false,
error: (_, _) => false,
),
child: avatarChild,
);
// Show realm avatar as small overlay if chat belongs to a realm
if (room.realm != null) {
return Stack(
children: [
badgeChild,
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: ClipOval(
child: ProfilePictureWidget(file: room.realm!.picture),
),
),
),
],
);
}
return badgeChild;
}
}
class ChatRoomSubtitle extends StatelessWidget {
final SnChatRoom room;
final bool isDirect;
final List<SnChatMember> validMembers;
final AsyncValue<SnChatSummary?> summary;
final Widget? subtitle;
const ChatRoomSubtitle({
super.key,
required this.room,
required this.isDirect,
required this.validMembers,
required this.summary,
this.subtitle,
});
@override
Widget build(BuildContext context) {
if (subtitle != null) return subtitle!;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
layoutBuilder: (currentChild, previousChildren) => Stack(
alignment: Alignment.centerLeft,
children: [...previousChildren, if (currentChild != null) currentChild],
),
child: summary.when(
data: (data) => Container(
key: const ValueKey('data'),
child: data == null
? isDirect && room.description == null
? Text(
validMembers
.map((e) => '@${e.account.name}')
.join(', '),
maxLines: 1,
)
: Text(
room.description ?? 'descriptionNone'.tr(),
maxLines: 1,
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (data.unreadCount > 0)
Text(
'unreadMessages'.plural(data.unreadCount),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
if (data.lastMessage == null)
Text(
room.description ?? 'descriptionNone'.tr(),
maxLines: 1,
)
else
Row(
spacing: 4,
children: [
Badge(
label: Text(data.lastMessage!.sender.account.nick),
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(
context,
).colorScheme.primary,
),
Expanded(
child: Text(
(data.lastMessage!.content?.isNotEmpty ?? false)
? data.lastMessage!.content!
: 'messageNone'.tr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
),
Align(
alignment: Alignment.centerRight,
child: Text(
RelativeTime(
context,
).format(data.lastMessage!.createdAt),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
],
),
),
loading: () => Container(
key: const ValueKey('loading'),
child: Builder(
builder: (context) {
final seed = DateTime.now().microsecondsSinceEpoch;
final len = 4 + (seed % 17); // 4..20 inclusive
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
var s = seed;
final buffer = StringBuffer();
for (var i = 0; i < len; i++) {
s = (s * 1103515245 + 12345) & 0x7fffffff;
buffer.write(chars[s % chars.length]);
}
return Skeletonizer(
enabled: true,
child: Text(buffer.toString()),
);
},
),
),
error: (_, _) => Container(
key: const ValueKey('error'),
child: isDirect && room.description == null
? Text(
validMembers.map((e) => '@${e.account.name}').join(', '),
maxLines: 1,
)
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1),
),
),
);
}
}

View File

@@ -8,6 +8,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:island/models/fortune.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/auth/captcha.dart';
@@ -17,7 +18,6 @@ import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/account/event_calendar_content.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';
@@ -43,12 +43,50 @@ Future<SnNotableDay?> nextNotableDay(Ref ref) async {
final client = ref.watch(apiClientProvider);
try {
final resp = await client.get('/pass/notable/me/next');
return SnNotableDay.fromJson(resp.data);
final day = SnNotableDay.fromJson(resp.data);
if (day.localizableKey != null) {
final key = 'notableDay${day.localizableKey}';
if (key.trExists()) {
return day.copyWith(
localName: key.tr(),
date: day.date.toLocal().copyWith(hour: 0, second: 0),
);
}
}
return day.copyWith(date: day.date.toLocal().copyWith(hour: 0, second: 0));
} catch (err) {
return null;
}
}
@riverpod
Future<SnNotableDay?> recentNotableDay(Ref ref) async {
final client = ref.watch(apiClientProvider);
try {
final resp = await client.get('/pass/notable/me/recent');
final day = SnNotableDay.fromJson(resp.data[0]);
if (day.localizableKey != null) {
final key = 'notableDay${day.localizableKey}';
if (key.trExists()) {
return day.copyWith(
localName: key.tr(),
date: day.date.toLocal().copyWith(hour: 0, second: 0),
);
}
}
return day.copyWith(date: day.date.toLocal().copyWith(hour: 0, second: 0));
} catch (err) {
return null;
}
}
@riverpod
Future<SnFortuneSaying> randomFortuneSaying(Ref ref) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/pass/fortune/random');
return SnFortuneSaying.fromJson(resp.data[0]);
}
class CheckInWidget extends HookConsumerWidget {
final EdgeInsets? margin;
final VoidCallback? onChecked;
@@ -57,7 +95,6 @@ class CheckInWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final todayResult = ref.watch(checkInResultTodayProvider);
final nextNotableDay = ref.watch(nextNotableDayProvider);
// Update time every second for live progress
final currentTime = useState(DateTime.now());
@@ -68,28 +105,6 @@ class CheckInWidget extends HookConsumerWidget {
return timer.cancel;
}, []);
final now = currentTime.value;
final userinfo = ref.watch(userInfoProvider);
final isAdult = useMemoized(() {
final birthday = userinfo.value?.profile.birthday;
if (birthday == null) return false;
final age =
now.year -
birthday.year -
((now.month < birthday.month ||
(now.month == birthday.month && now.day < birthday.day))
? 1
: 0);
return age >= 18;
}, [userinfo]);
final progress = (now.hour * 60.0 + now.minute) / (24 * 60);
final endOfDay = DateTime(now.year, now.month, now.day, 23, 59, 59);
final timeLeft = endOfDay.difference(now);
final timeLeftFormatted =
'${timeLeft.inHours.toString().padLeft(2, '0')}:${(timeLeft.inMinutes % 60).toString().padLeft(2, '0')}:${(timeLeft.inSeconds % 60).toString().padLeft(2, '0')}';
Future<void> checkIn({String? captchatTk}) async {
final client = ref.read(apiClientProvider);
try {
@@ -122,58 +137,22 @@ class CheckInWidget extends HookConsumerWidget {
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(),
Text(
DateFormat('MM/dd').format(DateTime.now()),
).fontSize(16),
Tooltip(
message: timeLeftFormatted,
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
trackGap: 0,
value: progress,
strokeWidth: 2,
),
),
),
],
),
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(2),
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(),
),
).padding(right: 4),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: todayResult.when(
@@ -213,14 +192,13 @@ class CheckInWidget extends HookConsumerWidget {
);
},
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),
],
),
error: (err, stack) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('error').tr().fontSize(15).bold(),
Text(err.toString()).fontSize(11),
],
),
),
).alignment(Alignment.centerLeft),
],
@@ -231,21 +209,6 @@ class CheckInWidget extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.end,
spacing: 4,
children: [
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(),
),
).padding(right: 4),
IconButton.outlined(
iconSize: 16,
visualDensity: const VisualDensity(
@@ -259,27 +222,22 @@ class CheckInWidget extends HookConsumerWidget {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'eventCalendar'.tr(),
child: EventCalendarContent(
name: 'me',
isSheet: true,
),
),
builder: (context) => SheetScaffold(
titleText: 'eventCalendar'.tr(),
child: EventCalendarContent(name: 'me', isSheet: true),
),
);
}
},
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: todayResult.when(
data:
(result) => Icon(
result == null
? Symbols.local_fire_department
: Symbols.event,
key: ValueKey(result != null),
),
data: (result) => Icon(
result == null
? Symbols.local_fire_department
: Symbols.event,
key: ValueKey(result != null),
),
loading: () => const Icon(Symbols.refresh),
error: (_, _) => const Icon(Symbols.error),
),

View File

@@ -86,4 +86,83 @@ final class NextNotableDayProvider
}
}
String _$nextNotableDayHash() => r'c8404308f6b0f581cc7df251bce8f3c5ac130245';
String _$nextNotableDayHash() => r'60d0546a086bdcb89c433c38133eb4197e4fb0a6';
@ProviderFor(recentNotableDay)
const recentNotableDayProvider = RecentNotableDayProvider._();
final class RecentNotableDayProvider
extends
$FunctionalProvider<
AsyncValue<SnNotableDay?>,
SnNotableDay?,
FutureOr<SnNotableDay?>
>
with $FutureModifier<SnNotableDay?>, $FutureProvider<SnNotableDay?> {
const RecentNotableDayProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'recentNotableDayProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$recentNotableDayHash();
@$internal
@override
$FutureProviderElement<SnNotableDay?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SnNotableDay?> create(Ref ref) {
return recentNotableDay(ref);
}
}
String _$recentNotableDayHash() => r'e0cc4a0e8016afe1c469a7c744dbab41e0d54c2d';
@ProviderFor(randomFortuneSaying)
const randomFortuneSayingProvider = RandomFortuneSayingProvider._();
final class RandomFortuneSayingProvider
extends
$FunctionalProvider<
AsyncValue<SnFortuneSaying>,
SnFortuneSaying,
FutureOr<SnFortuneSaying>
>
with $FutureModifier<SnFortuneSaying>, $FutureProvider<SnFortuneSaying> {
const RandomFortuneSayingProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'randomFortuneSayingProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$randomFortuneSayingHash();
@$internal
@override
$FutureProviderElement<SnFortuneSaying> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SnFortuneSaying> create(Ref ref) {
return randomFortuneSaying(ref);
}
}
String _$randomFortuneSayingHash() =>
r'861378dba8021e8555b568fb8e0390b2b24056f6';

719
lib/widgets/cmp/pattle.dart Normal file
View File

@@ -0,0 +1,719 @@
import 'dart:io';
import 'dart:math' as math;
import 'dart:ui';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/route_item.dart';
import 'package:island/pods/chat/chat_room.dart';
import 'package:island/pods/chat/chat_summary.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/route.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/chat_room_widgets.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/services/event_bus.dart';
import 'package:url_launcher/url_launcher.dart';
class CommandPattleWidget extends HookConsumerWidget {
final VoidCallback onDismiss;
const CommandPattleWidget({super.key, required this.onDismiss});
static List<SpecialAction> _getSpecialActions(BuildContext context) {
return [
SpecialAction(
name: 'postCompose'.tr(),
description: 'postComposeDescription'.tr(),
icon: Symbols.edit,
action: () {
eventBus.fire(const ShowComposeSheetEvent());
},
),
SpecialAction(
name: 'notifications'.tr(),
description: 'notificationsDescription'.tr(),
searchableAliases: ['notifications', 'alert', 'bell'],
icon: Symbols.notifications,
action: () {
eventBus.fire(const ShowNotificationSheetEvent());
},
),
];
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final textController = useTextEditingController();
final focusNode = useFocusNode();
final searchQuery = useState('');
final focusedIndex = useState<int?>(null);
final scrollController = useScrollController();
final animationController = useAnimationController(
duration: const Duration(milliseconds: 200),
);
final scaleAnimation = useAnimation(
Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(parent: animationController, curve: Curves.easeOut),
),
);
final opacityAnimation = useAnimation(
Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: animationController, curve: Curves.easeOut),
),
);
useEffect(() {
focusNode.requestFocus();
animationController.forward();
return null;
}, []);
useEffect(() {
void listener() {
searchQuery.value = textController.text;
// Reset focused index when search changes
focusedIndex.value = null;
}
textController.addListener(listener);
return () => textController.removeListener(listener);
}, [textController]);
final chatRooms = ref.watch(chatRoomJoinedProvider);
bool isDesktop() =>
kIsWeb ||
(!kIsWeb &&
(Platform.isWindows || Platform.isLinux || Platform.isMacOS));
final filteredChats = chatRooms.maybeWhen(
data: (rooms) {
if (searchQuery.value.isEmpty) return <SnChatRoom>[];
return rooms
.where((room) {
final title = room.name ?? '';
final desc = room.description ?? '';
final query = searchQuery.value.toLowerCase();
return title.toLowerCase().contains(query) ||
desc.toLowerCase().contains(query) ||
(room.members?.any(
(member) =>
member.account.name.contains(query) ||
member.account.nick.contains(query),
) ??
false);
})
.take(5) // Limit to 5 results
.toList();
},
orElse: () => <SnChatRoom>[],
);
final filteredRoutes = searchQuery.value.isEmpty
? <RouteItem>[]
: kAvailableRoutes
.where((route) {
final query = searchQuery.value.toLowerCase();
return route.name.toLowerCase().contains(query) ||
route.description.toLowerCase().contains(query) ||
route.searchableAliases.any(
(e) => e.toLowerCase().contains(query),
);
})
.take(5) // Limit to 5 results
.toList();
final filteredSpecialActions = searchQuery.value.isEmpty
? <SpecialAction>[]
: _getSpecialActions(context)
.where((action) {
final query = searchQuery.value.toLowerCase();
return action.name.toLowerCase().contains(query) ||
action.description.toLowerCase().contains(query) ||
action.searchableAliases.any(
(e) => e.toLowerCase().contains(query),
);
})
.take(5) // Limit to 5 results
.toList();
final filteredFallbacks =
searchQuery.value.isNotEmpty &&
filteredChats.isEmpty &&
filteredSpecialActions.isEmpty &&
filteredRoutes.isEmpty
? _getFallbackActions(ref, context, searchQuery.value)
: <FallbackAction>[];
// Combine results: fallbacks first, then chats, special actions, routes
final allResults = [
...filteredFallbacks,
...filteredChats,
...filteredSpecialActions,
...filteredRoutes,
];
// Scroll to focused item
useEffect(() {
if (focusedIndex.value != null && allResults.isNotEmpty) {
// Wait for the next frame to ensure ScrollController is attached
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
// Estimate item height (ListTile is typically around 72-88 pixels)
const double estimatedItemHeight = 80.0;
final double itemTopOffset =
focusedIndex.value! * estimatedItemHeight;
final double viewportHeight =
scrollController.position.viewportDimension;
final double centeredOffset =
itemTopOffset -
(viewportHeight / 2) +
(estimatedItemHeight / 2);
// Animate scroll to center the focused item
scrollController.animateTo(
centeredOffset.clamp(
0.0,
scrollController.position.maxScrollExtent,
),
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
});
}
return null;
}, [focusedIndex.value]);
return KeyboardListener(
focusNode: FocusNode(),
onKeyEvent: (event) {
if (event is KeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.escape) {
onDismiss();
} else if (isDesktop()) {
if (event.logicalKey == LogicalKeyboardKey.enter ||
event.logicalKey == LogicalKeyboardKey.numpadEnter) {
final item = allResults[focusedIndex.value ?? 0];
_executeItem(context, ref, item);
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
if (allResults.isNotEmpty) {
if (focusedIndex.value == null) {
focusedIndex.value = 0;
} else {
focusedIndex.value = math.max(0, focusedIndex.value! - 1);
}
}
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
if (allResults.isNotEmpty) {
if (focusedIndex.value == null) {
focusedIndex.value = 0;
} else {
focusedIndex.value = math.min(
allResults.length - 1,
focusedIndex.value! + 1,
);
}
}
}
}
}
},
child: GestureDetector(
onTap: onDismiss,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
color: Colors.black.withOpacity(0.5),
child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).size.height * 0.2,
),
child: AnimatedBuilder(
animation: animationController,
builder: (context, child) => Opacity(
opacity: opacityAnimation,
child: Transform.scale(scale: scaleAnimation, child: child),
),
child: GestureDetector(
onTap:
() {}, // Prevent tap from dismissing when tapping inside
child: Container(
width: math.max(
MediaQuery.of(context).size.width * 0.6,
320,
),
constraints: const BoxConstraints(
maxWidth: 600,
maxHeight: 500,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 10,
spreadRadius: 2,
),
],
),
child: Material(
elevation: 0,
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(28),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SearchBar(
controller: textController,
focusNode: focusNode,
hintText: 'searchChatsAndPages'.tr(),
leading: CircleAvatar(
child: const Icon(Symbols.keyboard_command_key),
).padding(horizontal: 8),
onSubmitted: !isDesktop() && allResults.isNotEmpty
? (value) => _executeItem(
context,
ref,
allResults[0],
)
: null,
),
AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
child: allResults.isNotEmpty
? ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 300,
),
child: ListView.builder(
padding: EdgeInsets.zero,
controller: scrollController,
shrinkWrap: true,
itemCount: allResults.length,
itemBuilder: (context, index) {
final item = allResults[index];
if (item is SnChatRoom) {
return _ChatRoomSearchResult(
room: item,
isFocused:
index == focusedIndex.value,
onTap: () => _navigateToChat(
context,
ref,
item,
),
);
} else if (item is SpecialAction) {
return _SpecialActionSearchResult(
action: item,
isFocused:
index == focusedIndex.value,
onTap: () {
onDismiss();
item.action();
},
);
} else if (item is RouteItem) {
return _RouteSearchResult(
route: item,
isFocused:
index == focusedIndex.value,
onTap: () => _navigateToRoute(
context,
ref,
item,
),
);
} else if (item is FallbackAction) {
return _FallbackSearchResult(
action: item,
isFocused:
index == focusedIndex.value,
onTap: () {
onDismiss();
item.action();
},
);
}
return const SizedBox.shrink();
},
),
)
: const SizedBox.shrink(),
),
],
),
),
),
),
),
),
),
),
),
),
);
}
void _navigateToChat(BuildContext context, WidgetRef ref, SnChatRoom room) {
onDismiss();
if (isWideScreen(context)) {
debugPrint('${room.name}');
ref
.read(routerProvider)
.replaceNamed('chatRoom', pathParameters: {'id': room.id});
} else {
ref
.read(routerProvider)
.pushNamed('chatRoom', pathParameters: {'id': room.id});
}
}
void _navigateToRoute(BuildContext context, WidgetRef ref, RouteItem route) {
onDismiss();
ref.read(routerProvider).go(route.path);
}
void _executeItem(BuildContext context, WidgetRef ref, dynamic item) {
if (item is SnChatRoom) {
_navigateToChat(context, ref, item);
} else if (item is SpecialAction) {
onDismiss();
item.action();
} else if (item is RouteItem) {
_navigateToRoute(context, ref, item);
} else if (item is FallbackAction) {
onDismiss();
item.action();
}
}
static List<FallbackAction> _getFallbackActions(
WidgetRef ref,
BuildContext context,
String query,
) {
final settings = ref.watch(appSettingsProvider);
final List<FallbackAction> actions = [];
// Check if query is a URL
final Uri? uri = Uri.tryParse(query);
final isValidUrl =
uri != null && (uri.scheme == 'http' || uri.scheme == 'https');
final isDomain = RegExp(
r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$',
).hasMatch(query);
if (isValidUrl || isDomain) {
final finalUri = isDomain ? Uri.parse('https://$query') : uri!;
actions.add(
FallbackAction(
name: 'Open URL',
description: 'Open ${finalUri.toString()} in browser',
icon: Symbols.open_in_new,
action: () async {
if (await canLaunchUrl(finalUri)) {
await launchUrl(finalUri, mode: LaunchMode.externalApplication);
}
},
),
);
}
// Ask the AI
// Bugged, DO NOT USE
// actions.add(
// FallbackAction(
// name: 'Ask the AI',
// description: 'Ask "$query" to the AI',
// icon: Symbols.bubble_chart,
// action: () {
// eventBus.fire(ShowThoughtSheetEvent(initialMessage: query));
// },
// ),
// );
// Search the web
actions.add(
FallbackAction(
name: 'Search the web',
description: 'Search "$query" on the Internet',
icon: Symbols.search,
action: () async {
final searchUri = Uri.parse(
settings.dashSearchEngine != null
? settings.dashSearchEngine!.replaceFirst('%s', query)
: 'https://www.google.com/search?q=$query',
);
if (await canLaunchUrl(searchUri)) {
await launchUrl(searchUri, mode: LaunchMode.externalApplication);
}
},
),
);
return actions;
}
}
class FallbackAction {
final String name;
final String description;
final IconData icon;
final VoidCallback action;
final List<String> searchableAliases;
const FallbackAction({
required this.name,
required this.description,
required this.icon,
required this.action,
this.searchableAliases = const [],
});
}
class _RouteSearchResult extends StatelessWidget {
final RouteItem route;
final bool isFocused;
final VoidCallback onTap;
const _RouteSearchResult({
required this.route,
required this.isFocused,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: isFocused
? Theme.of(context).colorScheme.surfaceContainerHighest
: null,
borderRadius: const BorderRadius.all(Radius.circular(28)),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
foregroundColor: Theme.of(context).colorScheme.onSecondaryContainer,
child: Icon(route.icon),
),
title: Text(route.name),
subtitle: Text(route.description),
onTap: onTap,
),
);
}
}
class _SpecialActionSearchResult extends StatelessWidget {
final SpecialAction action;
final bool isFocused;
final VoidCallback onTap;
const _SpecialActionSearchResult({
required this.action,
required this.isFocused,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: isFocused
? Theme.of(context).colorScheme.surfaceContainerHighest
: null,
borderRadius: const BorderRadius.all(Radius.circular(24)),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.tertiaryContainer,
foregroundColor: Theme.of(context).colorScheme.onTertiaryContainer,
child: Icon(action.icon),
),
title: Text(action.name),
subtitle: Text(action.description),
onTap: onTap,
),
);
}
}
class _FallbackSearchResult extends StatelessWidget {
final FallbackAction action;
final bool isFocused;
final VoidCallback onTap;
const _FallbackSearchResult({
required this.action,
required this.isFocused,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: isFocused
? Theme.of(context).colorScheme.surfaceContainerHighest
: null,
borderRadius: const BorderRadius.all(Radius.circular(24)),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
child: Icon(action.icon),
),
title: Text(action.name),
subtitle: Text(action.description),
onTap: onTap,
),
);
}
}
class _ChatRoomSearchResult extends HookConsumerWidget {
final SnChatRoom room;
final bool isFocused;
final VoidCallback onTap;
const _ChatRoomSearchResult({
required this.room,
required this.isFocused,
required this.onTap,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userInfo = ref.watch(userInfoProvider);
final summary = ref
.watch(chatSummaryProvider)
.whenData((summaries) => summaries[room.id]);
var validMembers = room.members ?? [];
if (validMembers.isNotEmpty && userInfo.value != null) {
validMembers = validMembers
.where((e) => e.accountId != userInfo.value!.id)
.toList();
}
String titleText;
if (room.type == 1 && room.name == null) {
if (room.members?.isNotEmpty ?? false) {
titleText = validMembers.map((e) => e.account.nick).join(', ');
} else {
titleText = 'Direct Message';
}
} else {
titleText = room.name ?? '';
}
Widget buildSubtitle() {
return summary.when(
data: (data) => data == null
? (room.type == 1 && room.description == null
? Text(
validMembers.map((e) => '@${e.account.name}').join(', '),
)
: Text(room.description ?? ''))
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (data.unreadCount > 0)
Text(
'unreadMessages'.plural(data.unreadCount),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
if (data.lastMessage == null)
room.type == 1 && room.description == null
? Text(
validMembers
.map((e) => '@${e.account.name}')
.join(', '),
)
: Text(room.description ?? '')
else
Row(
spacing: 4,
children: [
Badge(
label: Text(data.lastMessage!.sender.account.nick),
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(
context,
).colorScheme.primary,
),
Expanded(
child: Text(
(data.lastMessage!.content?.isNotEmpty ?? false)
? data.lastMessage!.content!
: 'messageNone'.tr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
),
Align(
alignment: Alignment.centerRight,
child: Text(
RelativeTime(
context,
).format(data.lastMessage!.createdAt),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
],
),
loading: () => room.type == 1 && room.description == null
? Text(validMembers.map((e) => '@${e.account.name}').join(', '))
: Text(room.description ?? ''),
error: (_, _) => room.type == 1 && room.description == null
? Text(validMembers.map((e) => '@${e.account.name}').join(', '))
: Text(room.description ?? ''),
);
}
final isDirect = room.type == 1;
return Container(
decoration: BoxDecoration(
color: isFocused
? Theme.of(context).colorScheme.surfaceContainerHighest
: null,
borderRadius: const BorderRadius.all(Radius.circular(24)),
),
child: ListTile(
leading: ChatRoomAvatar(
room: room,
isDirect: isDirect,
summary: summary,
validMembers: validMembers,
),
title: Text(titleText),
subtitle: buildSubtitle(),
onTap: onTap,
),
);
}
}

View File

@@ -9,8 +9,11 @@ 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/profile_decoration.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:island/widgets/data_saving_gate.dart';
import 'file_viewer_contents.dart';
@@ -258,17 +261,15 @@ class CloudFileWidget extends HookConsumerWidget {
var content = switch (item.mimeType?.split('/').firstOrNull) {
'image' => AspectRatio(
aspectRatio: ratio,
child:
(useInternalGate && dataSaving && !unlocked.value)
? dataPlaceHolder(Symbols.image)
: cloudImage(),
child: (useInternalGate && dataSaving && !unlocked.value)
? dataPlaceHolder(Symbols.image)
: cloudImage(),
),
'video' => AspectRatio(
aspectRatio: ratio,
child:
(useInternalGate && dataSaving && !unlocked.value)
? dataPlaceHolder(Symbols.play_arrow)
: cloudVideo(),
child: (useInternalGate && dataSaving && !unlocked.value)
? dataPlaceHolder(Symbols.play_arrow)
: cloudVideo(),
),
'audio' => AudioFileContent(item: item, uri: uri),
_ => Builder(
@@ -383,10 +384,9 @@ class CloudVideoWidget extends HookConsumerWidget {
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;
var ratio = item.fileMeta?['ratio'] is num
? item.fileMeta!['ratio'].toDouble()
: 1.0;
if (ratio == 0) ratio = 1.0;
if (open.value) {
@@ -533,10 +533,9 @@ class CloudImageWidget extends ConsumerWidget {
return AspectRatio(
aspectRatio: aspectRatio,
child:
file != null
? CloudFileWidget(item: file!, fit: fit)
: UniversalImage(uri: uri, blurHash: blurHash, fit: fit),
child: file != null
? CloudFileWidget(item: file!, fit: fit)
: UniversalImage(uri: uri, blurHash: blurHash, fit: fit),
);
}
@@ -545,10 +544,9 @@ class CloudImageWidget extends ConsumerWidget {
required String serverUrl,
bool original = false,
}) {
final uri =
original
? '$serverUrl/drive/files/$fileId?original=true'
: '$serverUrl/drive/files/$fileId';
final uri = original
? '$serverUrl/drive/files/$fileId?original=true'
: '$serverUrl/drive/files/$fileId';
return CachedNetworkImageProvider(uri);
}
}
@@ -560,6 +558,7 @@ class ProfilePictureWidget extends ConsumerWidget {
final double? borderRadius;
final IconData? fallbackIcon;
final Color? fallbackColor;
final ProfileDecoration? decoration;
const ProfilePictureWidget({
super.key,
this.fileId,
@@ -568,6 +567,7 @@ class ProfilePictureWidget extends ConsumerWidget {
this.borderRadius,
this.fallbackIcon,
this.fallbackColor,
this.decoration,
});
@override
@@ -575,36 +575,49 @@ class ProfilePictureWidget extends ConsumerWidget {
final serverUrl = ref.watch(serverUrlProvider);
final String? id = file?.id ?? fileId;
final fallback =
Icon(
fallbackIcon ?? Symbols.account_circle,
size: radius,
color:
fallbackColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
).center();
final fallback = Icon(
fallbackIcon ?? Symbols.account_circle,
size: radius,
color: fallbackColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
).center();
final image = id == null
? fallback
: DataSavingGate(
bypass: true,
placeholder: fallback,
content: () => UniversalImage(
uri: '$serverUrl/drive/files/$id',
fit: BoxFit.cover,
),
);
Widget content = Container(
width: radius * 2,
height: radius * 2,
color: Theme.of(context).colorScheme.primaryContainer,
child: decoration != null
? Stack(
fit: StackFit.expand,
children: [
image,
CustomPaint(
painter: _ProfileDecorationPainter(
text: decoration!.text,
color: decoration!.color,
textColor: decoration!.textColor ?? Colors.white,
),
),
],
)
: image,
);
return ClipRRect(
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:
id == null
? fallback
: DataSavingGate(
bypass: true,
placeholder: fallback,
content:
() => UniversalImage(
uri: '$serverUrl/drive/files/$id',
fit: BoxFit.cover,
),
),
),
borderRadius: borderRadius == null
? BorderRadius.all(Radius.circular(radius))
: BorderRadius.all(Radius.circular(borderRadius!)),
child: content,
);
}
}
@@ -716,32 +729,29 @@ class SplitAvatarWidget extends ConsumerWidget {
),
),
Expanded(
child:
filesId.length > 4
? Container(
color:
Theme.of(
child: filesId.length > 4
? Container(
color: Theme.of(
context,
).colorScheme.primaryContainer,
child: Center(
child: Text(
'+${filesId.length - 3}',
style: TextStyle(
fontSize: radius * 0.4,
color: Theme.of(
context,
).colorScheme.primaryContainer,
child: Center(
child: Text(
'+${filesId.length - 3}',
style: TextStyle(
fontSize: radius * 0.4,
color:
Theme.of(
context,
).colorScheme.onPrimaryContainer,
),
).colorScheme.onPrimaryContainer,
),
),
)
: _buildQuadrant(
context,
filesId[3],
ref,
radius,
),
)
: _buildQuadrant(
context,
filesId[3],
ref,
radius,
),
),
],
),
@@ -765,14 +775,12 @@ class SplitAvatarWidget extends ConsumerWidget {
width: radius,
height: radius,
color: Theme.of(context).colorScheme.primaryContainer,
child:
Icon(
fallbackIcon,
size: radius * 0.6,
color:
fallbackColor ??
Theme.of(context).colorScheme.onPrimaryContainer,
).center(),
child: Icon(
fallbackIcon,
size: radius * 0.6,
color:
fallbackColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
).center(),
);
}
@@ -786,3 +794,106 @@ class SplitAvatarWidget extends ConsumerWidget {
);
}
}
class _ProfileDecorationPainter extends CustomPainter {
final String text;
final Color color;
final Color textColor;
_ProfileDecorationPainter({
required this.text,
required this.color,
required this.textColor,
});
@override
void paint(Canvas canvas, Size size) {
if (text.isEmpty) return;
final radius = size.width / 2;
final center = Offset(size.width / 2, size.height / 2);
final strokeWidth = radius * 0.4; // Increased thickness
final centerAngle = 3 * math.pi / 4;
final sweepAngle = math.pi / 1;
final startAngle = centerAngle - (sweepAngle / 2);
final arcRadius = radius - (strokeWidth / 2);
final rect = Rect.fromCircle(center: center, radius: arcRadius);
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..shader = SweepGradient(
startAngle: startAngle,
endAngle: startAngle + sweepAngle,
colors: [color.withOpacity(0), color, color, color.withOpacity(0)],
stops: const [0.0, 0.25, 0.75, 1.0],
).createShader(rect);
canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
_drawTextOnArc(canvas, center, arcRadius, text, centerAngle);
}
void _drawTextOnArc(
Canvas canvas,
Offset center,
double radius,
String text,
double centerAngle,
) {
final textStyle = TextStyle(
color: textColor,
fontSize: radius * 0.28,
fontWeight: FontWeight.bold,
);
double totalAngle = 0;
List<double> charAngles = [];
// Calculate total angle occupied by text
for (int i = 0; i < text.length; i++) {
final char = text[i];
final span = TextSpan(text: char, style: textStyle);
final tp = TextPainter(text: span, textDirection: ui.TextDirection.ltr);
tp.layout();
final charWidth = tp.width;
final angle = charWidth / radius;
charAngles.add(angle);
totalAngle += angle;
}
// Start from "Left" of the center (High angle)
// We want to traverse from centerAngle + total/2 to centerAngle - total/2
double currentAngle = centerAngle + (totalAngle / 2);
for (int i = 0; i < text.length; i++) {
final char = text[i];
final span = TextSpan(text: char, style: textStyle);
final tp = TextPainter(text: span, textDirection: ui.TextDirection.ltr);
tp.layout();
final charAngle = charAngles[i];
final midCharAngle = currentAngle - charAngle / 2;
final x = center.dx + radius * math.cos(midCharAngle);
final y = center.dy + radius * math.sin(midCharAngle);
canvas.save();
canvas.translate(x, y);
canvas.rotate(midCharAngle - math.pi / 2);
tp.paint(canvas, Offset(-tp.width / 2, -tp.height / 2));
canvas.restore();
currentAngle -= charAngle;
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

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