Compare commits

..

312 Commits

Author SHA1 Message Date
dfcbfcb31e 🎨 Continued to rearrange core folders content 2026-02-06 00:57:17 +08:00
862e3b451b 🎨 Use feature based folder structure 2026-02-06 00:37:02 +08:00
62a3ea26e3 🔀 Merge branch 'features/fitness' into v3 2026-02-05 20:59:37 +08:00
5414dfd4f0 👽 Changes due to the lotteries data has been moved to wallet 2026-02-05 16:36:45 +08:00
f2819a9d92 🚀 Launch 3.5.0+165 2026-02-05 12:56:26 +08:00
bc02eb6923 🐛 Fix conditional app wrapper. close #246 2026-02-05 02:42:19 +08:00
dd6bc3cc99 Solar Network Fitness infra 2026-02-05 02:33:14 +08:00
863a84a2a0 ⬆️ Upgrade dependecies 2026-02-05 00:22:59 +08:00
4b1c9b5820 👽 Update the API request path due to the sepration of the wallet service 2026-02-05 00:14:18 +08:00
0237e457fc 💄 Optimize universal search screen 2026-02-04 01:08:16 +08:00
f6f1c99da7 ♻️ Mixed accounts search 2026-02-02 22:15:19 +08:00
583902ad52 🎨 Optimize the null check in list 2026-02-02 15:48:48 +08:00
cfd2a47064 ♻️ Optimize realm discovery 2026-02-02 13:30:33 +08:00
5cdeb7bd22 🐛 Adding fab to chat wide screen mode, close #244 2026-02-02 03:13:27 +08:00
6e5255550a 🐛 Avoiding show duplicate items in timeline 2026-02-02 02:55:09 +08:00
8a9de6f43f 👽 Update the notify due to the API changes 2026-02-02 02:54:41 +08:00
c9c362d3d1 ⬆️ Upgrade dependecies 2026-02-02 02:44:14 +08:00
617d22847b ⬆️ Upgrade the dependecies 2026-01-30 00:40:30 +08:00
LittleSheep
3c77a4d5be 🔀 Merge pull request #243 from BluewhaleYF/jap
🌐 添加日语本地化
2026-01-24 18:53:25 +08:00
LittleSheep
f9061a3dc6 🔀 Merge pull request #242 from BluewhaleYF/translate
🌐 文言界面翻译改进
2026-01-24 18:52:58 +08:00
Flint Scophire
e12a26d5c0 🌐 添加日语本地化 2026-01-24 13:21:37 +08:00
Flint Scophire
6b31d28cd3 🌐 文言界面翻译改进 2026-01-24 12:36:30 +08:00
bff301fdb3 ⬆️ Upgrade dependecies and flutter 2026-01-21 22:50:48 +08:00
LittleSheep
2049ee4401 🔀 Merge pull request #238 from BluewhaleYF/v3
彻底完成文言界面翻译
2026-01-18 23:53:00 +08:00
18c071826f 💄 Play sfx on confirm dialog as well 2026-01-18 18:17:47 +08:00
5bfc301088 🗑️ Clean unused code 2026-01-18 14:25:11 +08:00
a39853ba5a :drunk: Wrote some useless code 2026-01-18 14:16:29 +08:00
639417e952 :drunk: Werid miniapp runtime 2026-01-18 13:31:45 +08:00
Flint Scophire
9de4def4d4 Add files via upload
彻底完成文言界面翻译
2026-01-18 11:27:44 +08:00
fa0051b606 Sound effects on alert 2026-01-18 01:25:07 +08:00
9d867fd888 🍱 Update audio assets 2026-01-18 01:14:06 +08:00
f572f5b462 🚀 Launch 3.5.0+164 2026-01-17 23:36:09 +08:00
a02f2fe3c6 🐛 Fix iOS 2026-01-17 23:36:03 +08:00
bc4fddc164 💄 Optimize post detail 2026-01-17 22:33:27 +08:00
3d7e7951a2 💄 Localizable file list 2026-01-17 22:31:51 +08:00
41df5f3907 🐛 Fix bugs in file list 2026-01-17 22:04:20 +08:00
6487a1ff65 💄 Lift file list search to appbar 2026-01-17 21:58:27 +08:00
2e90d243de 💄 Optimize file list UI 2026-01-17 21:38:35 +08:00
LittleSheep
1eca95fa78 🔀 Merge pull request #237 from BluewhaleYF/v3
改进文言界面翻译
2026-01-17 21:05:48 +08:00
Flint Scophire
96b87f75f9 Add files via upload
改进文言界面翻译
2026-01-17 18:02:25 +08:00
e832f3fd93 🐛 Fix dashboard customize issue 2026-01-17 16:04:18 +08:00
aeea90226a 🐛 Fix some bugs in dashboard 2026-01-17 16:02:56 +08:00
c36a5eaa73 💄 Optimize compose toolbar styling 2026-01-17 15:51:45 +08:00
6840054a41 💄 Better dashboard customization 2026-01-17 15:51:34 +08:00
6a0f351114 💄 Customizable dashboard 2026-01-17 15:32:08 +08:00
c5d667ecf3 💄 Optimize the article editor 2026-01-17 14:50:16 +08:00
09767e113f 💄 Better article editor 2026-01-17 14:42:45 +08:00
9ca5c63afd :drunk: Messing around werid stuff 2026-01-17 02:05:24 +08:00
fd3c08e054 🍱 Localized app intents 2026-01-16 21:30:59 +08:00
72f23a504e ⬆️ Upgrade dependecies
 Remove some unused deps
2026-01-16 21:21:51 +08:00
4898825124 ♻️ Use fully native implmentation of app intents on iOS 2026-01-16 21:17:44 +08:00
387d19d85c 🚀 Launch 3.5.0+163 (SNAPSHOT) 2026-01-16 01:50:27 +08:00
f7b991663f Render post article thumbnail 2026-01-16 01:44:41 +08:00
3a57f4265b Editing article thumbnail 2026-01-16 01:30:46 +08:00
d1ee2e5160 💄 Optimize the notification overlay styling 2026-01-16 00:56:57 +08:00
bcd6753ed2 💄 Updating compose toolbar styling 2026-01-16 00:47:32 +08:00
321ea4458b 💄 Optimize the notification overlays 2026-01-16 00:21:07 +08:00
8ad31dad58 Notification stacks on the mobile 2026-01-16 00:08:56 +08:00
269c17d068 💄 The sfx can be played multiple times 2026-01-15 23:05:39 +08:00
a9abd777e1 💫 Better notification overlay animation 2026-01-15 23:02:21 +08:00
e24b1fc135 💫 Optimize the notification animation 2026-01-15 19:34:03 +08:00
d5feea52fa 💄 Optimize notification item 2026-01-15 19:31:11 +08:00
491252bba9 💫 Optimize the overlay animation of notifications 2026-01-15 01:39:48 +08:00
4f569fbefd ♻️ New notification overlay basis 2026-01-15 01:31:09 +08:00
476da28b5e ♻️ Improved new notifcation style 2026-01-15 01:04:15 +08:00
d639df7623 ♻️ Optimize the sfx playing 2026-01-15 00:44:38 +08:00
e1fc5311d2 🐛 Fix some bugs 2026-01-15 00:09:41 +08:00
d0e4fde6c2 🐛 Added some platform checks 2026-01-14 22:50:06 +08:00
9437339b0f 💄 Give name for pasted image 2026-01-14 22:40:10 +08:00
dd7696132c 👽 Authorized audio, video access 2026-01-14 22:34:17 +08:00
95daa3c28d 💄 Use unlimited variant of the fortune card, close #234 2026-01-14 01:53:24 +08:00
ac5193e1f6 🐛 Fix webfeed issues, close #235 2026-01-14 01:49:27 +08:00
0328a7736a Optimize retry logic 2026-01-14 01:06:34 +08:00
03b332f677 🐛 Adjust analytics service initialization 2026-01-14 00:56:28 +08:00
91b2797fb9 👽 Authorized image load request
💄 Optimize image styling
2026-01-13 23:15:36 +08:00
b66640c6df 🚀 Launch 3.5.0+162 2026-01-12 01:13:55 +08:00
4ac10706ae iOS push notification custom sound 2026-01-12 01:08:52 +08:00
bf2844162d 💄 Optimize post detail layout 2026-01-12 01:04:03 +08:00
23c11a2fbd 🗑️ Remove the file references 2026-01-12 00:25:19 +08:00
2b99f54bc5 🐛 Fix upload task 2026-01-12 00:22:28 +08:00
ab2fc1013b 💄 Scrollable alert 2026-01-12 00:08:51 +08:00
LittleSheep
9a41ff26ef 🔀 Merge pull request #228 from Texas0295/v3
[FIX] linux: defer AnalyticsService init to avoid Firebase crash
2026-01-11 23:49:05 +08:00
63a55658ab Sound effects on notifications 2026-01-11 23:32:00 +08:00
Texas0295
3122ae4cc2 [FIX] linux: defer AnalyticsService init to avoid Firebase crash
Defer FirebaseAnalytics initialization to constructor to avoid crashes on
unsupported platforms (notably Linux). Replace class-level initialization
with nullable field and guarded calls.

Changes:
- remove class-level initialization of _analytics
- add private _init() invoked in constructor
- make _analytics nullable
- add null checks in logEvent() and setUserId()
- emit warning instead of throwing
- Linux builds no longer crash on startup

Signed-off-by: Texas0295 <kimura@texas0295.top>
2026-01-11 20:31:27 +08:00
00e063e99f 🐛 Dozens of bug fixes 2026-01-11 18:41:05 +08:00
532bb30c6a Sfx on notification 2026-01-11 18:32:23 +08:00
d8c33b576f Attachment video preview 2026-01-11 16:56:41 +08:00
a984cba2fa 💫 Animated attachment preview progress bar 2026-01-11 13:42:55 +08:00
b7d5aa5dfb Better attachment preview progress 2026-01-11 13:40:35 +08:00
826238a374 💄 Optimize task overlay 2026-01-11 13:22:13 +08:00
88c4d648d5 ♻️ Better task overlay progress 2026-01-11 13:11:15 +08:00
bf59108569 🐛 Add well defined mounted check in messages notifier 2026-01-11 12:53:57 +08:00
eec181da55 💄 Optimize task overlay styles 2026-01-11 02:48:44 +08:00
c93b543da9 💄 Animated image gates 2026-01-10 23:20:16 +08:00
2fd93246c7 💄 Improve the image don't animated the opacity if cached 2026-01-10 23:02:31 +08:00
1b2620e957 💫 Optimize the chat message animation and performance 2026-01-10 22:56:08 +08:00
d443343052 🐛 Fix serval issues in past commits 2026-01-10 22:23:42 +08:00
9957905212 ⬆️ Upgrade dependecies 2026-01-10 21:21:31 +08:00
e36d694397 💫 Optimize the chat room animation 2026-01-10 16:10:55 +08:00
3847581f1f ♻️ Decouple the room.dart 2026-01-10 14:18:59 +08:00
64903bf1f3 📈 Tracking data's analytics service 2026-01-10 13:43:31 +08:00
a449fbb58a More app intents available on iOS 2026-01-10 13:06:09 +08:00
6a4a50b81a 🚀 Launch 3.5.0+161 (SNAPSHOT) 2026-01-09 02:02:52 +08:00
5e328b736f App shortcuts 2026-01-09 01:58:29 +08:00
d44e481a0d 💄 Optimize the two parts split avatar displaying 2026-01-09 01:41:21 +08:00
e6fb38572c App intents (aims to support Siri on iOS) (untested) 2026-01-09 01:39:08 +08:00
64c5ee0fe9 🐛 Trying to fix NSE attachment doesn't work as expected on the default types 2026-01-08 23:10:22 +08:00
9cba568e47 💄 New media post layout 2026-01-07 23:30:48 +08:00
38fc4e969f 💄 Improved pinned post 2026-01-07 22:52:46 +08:00
0e100c3e64 💄 Better pinned post view 2026-01-07 22:46:36 +08:00
735724ae4d 🐛 Fix some bugs within relationship forgot 2026-01-07 22:31:16 +08:00
6c31337fbc 🐛 Optimize the disposal check in message notifier, close #211 2026-01-07 19:46:07 +08:00
d2806fdc3a 💄 Optimize timeline cursor picking 2026-01-07 19:44:53 +08:00
4ebbda1b14 🐛 Trying to fix timeline duplicate posts, close #227 2026-01-07 01:55:10 +08:00
3e7007f58f 🐛 Trying to fix app scaffold focus conflict 2026-01-07 01:52:25 +08:00
d96629a7f1 🐛 Fix universal image still got state changes after unmounted 2026-01-07 01:48:20 +08:00
1b6ccccf32 💫 Animated profile card 2026-01-07 01:43:46 +08:00
910dafaa43 🐛 Fix rename didn't commited 2026-01-07 01:43:03 +08:00
c6fd342072 ♻️ Refactored the relationship screen according to server updates 2026-01-07 01:42:20 +08:00
757f1b880c 🐛 Fix explore pagination placeholder maxWidth 2026-01-07 01:22:28 +08:00
67805b413e 🐛 Fix regex in markdown 2026-01-06 01:08:09 +08:00
e1eb0f705e Prevent the non-user name got rendered as mention chip, close #226 2026-01-05 22:46:40 +08:00
1e8e785e5f Adjust riverpod auto retries logic 2026-01-05 22:35:52 +08:00
d9dfea6b8b 🐛 Fix creation date parsing in iOS code 2026-01-05 22:14:29 +08:00
d29f0b1170 Shuffle post iOS widget 2026-01-05 22:13:26 +08:00
479a79c7f6 Circular accessory on iOS 2026-01-04 23:03:18 +08:00
1a74f2b3e9 🚀 Launch 3.5.0+160 (SNAPSHOT) 2026-01-04 01:21:58 +08:00
97a5e951e1 💄 Optimize iOS notifications widget 2026-01-04 01:20:50 +08:00
9071ac44fe 🐛 Fix iOS widget catalog issue 2026-01-04 01:14:47 +08:00
6abee8d8bd iOS notification widget 2026-01-04 01:04:49 +08:00
8cf03683dc 💄 Better iOS widget styling 2026-01-03 21:42:30 +08:00
f34d80b7d4 🚚 Rename iOS check in widget file name to ready add more 2026-01-03 21:30:23 +08:00
b6d7e52148 iOS check in widget with notable days support 2026-01-03 21:29:35 +08:00
978b7b32fd iOS check in widget 2026-01-03 18:50:54 +08:00
35a9c9ff4b 🐛 Fix android biometric issue 2026-01-03 18:50:46 +08:00
e5cb296367 Due to server changes, rollback the meta field rename 2026-01-03 13:49:52 +08:00
cf3a2b6340 🎉 Create the iOS extension widget target 2026-01-03 13:06:10 +08:00
f568baf14d 💄 Adjust post subscription filter 2026-01-03 12:47:00 +08:00
703335429a 🐛 Fix post metadata uses wrong key to deserialize 2026-01-03 12:45:28 +08:00
188b6821a2 🚀 Launch 3.5.0+159 2026-01-02 20:46:18 +08:00
0ebbe0bd5a 💄 Optimize shuffle post 2026-01-02 20:34:09 +08:00
46a826ff86 👽 Update the articles url to new 2026-01-02 20:33:04 +08:00
1d99ac6441 💄 Adjust post detail screen 2026-01-02 19:57:16 +08:00
e2efdc4064 🐛 Fix chat input didn't count safe area 2026-01-02 19:27:17 +08:00
cba1a3884b 💄 Change EXIF info overlay alignment 2026-01-02 19:26:58 +08:00
7147ce1efa Better EXIF overlay 2026-01-02 19:22:47 +08:00
78c1a284a5 ♻️ Better image loading animation and more commonly used blurhash 2026-01-02 18:32:37 +08:00
f1f5113b01 💄 Swipe style post shuffle 2026-01-02 17:49:14 +08:00
a44552f105 🗑️ Remove unused activitypub screens 2026-01-02 17:02:04 +08:00
8c1ad94555 🐛 Fix route bug 2026-01-02 16:59:46 +08:00
84f5677260 🐛 Fix reference post displaying issue 2026-01-02 16:24:23 +08:00
aa1ffdbf10 ⬆️ Upgrade dependecies 2026-01-02 16:24:14 +08:00
c24d13461b 🐛 Fixes in chat subscribe 2026-01-02 16:14:55 +08:00
3b60fcb87c 💄 Optimize universal search 2026-01-02 16:14:24 +08:00
3605b997b1 💄 Optimize floating action button 2026-01-02 14:44:36 +08:00
800815c721 💄 Optimize unauthorized status 2026-01-02 14:33:41 +08:00
3b13a63e7b 💄 Optimize fediverse mention rendering, etc 2026-01-02 14:04:16 +08:00
81d69ce10f 🗑️ Remove unused stuff 2026-01-02 12:46:42 +08:00
c16d8a5912 🚀 Launch 3.5.0+158 (SNAPSHOT) 2026-01-02 03:13:37 +08:00
cb9eca0424 Better fediverse content 2026-01-02 03:08:50 +08:00
d7858bab67 🐛 Fix some bugs 2026-01-02 02:34:45 +08:00
5ce590029b 👽 Fix realm chat API didn't move 2026-01-02 00:15:32 +08:00
4d92dec45c 👽 Changes to API path since sphere no longer handle messages 2026-01-01 23:59:21 +08:00
0a4e797eec ♻️ Update the way to render total counts of data in UI, close #220 2026-01-01 11:44:10 +08:00
38dffa414f ♻️ Rebuilt fetching state machine 2026-01-01 11:40:28 +08:00
eea56a742e 🐛 Fix the paging refreshing issue 2026-01-01 11:10:04 +08:00
56b7ee1d69 🐛 Fix style mismatch 2026-01-01 02:44:25 +08:00
3b564f7e7f Able to render fediverse reactions (likes) 2026-01-01 02:37:05 +08:00
93d2670063 Able to manage publisher actor 2026-01-01 02:29:27 +08:00
788165ac5b 🐛 Fix poll feedback, close #224 2026-01-01 02:02:12 +08:00
ec71125fa9 Able to render offsite media 2026-01-01 02:00:09 +08:00
adb231278c Able to render fediverse posts 2026-01-01 01:47:09 +08:00
b3ae4ab36f 👽 Update the models to match API 2025-12-31 01:43:23 +08:00
3670fe0f10 👽 Update according to AP api changes 2025-12-29 01:43:00 +08:00
bb1a5155ed ActivityPub service impl basis 2025-12-29 01:01:47 +08:00
eb90dbbc5a 🐛 Fix fab 2025-12-28 17:19:27 +08:00
a73d9f8ec0 ♻️ Improve file viewing experience 2025-12-28 15:32:00 +08:00
4c8f2e3251 🐛 Fix tabs padding cause background color missing, close #221 2025-12-28 14:10:42 +08:00
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
d7746d14e4 🚀 Launch 3.5.0+151 2025-12-06 21:52:30 +08:00
648d5225f6 🐛 Ensure mobile site management request permission 2025-12-06 21:48:16 +08:00
9d4d0f2e48 🐛 Fix inconsistence alert 2025-12-06 21:44:43 +08:00
fe386163f4 💄 Optimize designs in developer hub 2025-12-06 21:39:50 +08:00
ac2cee10e5 💄 Hub now shows loading stautus of publishers / developers 2025-12-06 21:28:19 +08:00
9c370647dd 🐛 Fix some bugs in creator hub 2025-12-06 21:26:00 +08:00
7516e197fe 💄 Fix post replies skeleton inconststent 2025-12-06 21:15:32 +08:00
71c372ab6c Prefer auto dispose riverpods 2025-12-06 21:13:25 +08:00
25f23f7f93 🐛 Fix serval bugs during the changes 2025-12-06 21:05:29 +08:00
51853698b9 🐛 Fix serval bugs 2025-12-06 20:53:24 +08:00
39ed5393ab 💄 Dedicated notification skeleton 2025-12-06 20:49:54 +08:00
782b3f1b08 🐛 Fix article edit shows the post edit sheet 2025-12-06 20:45:47 +08:00
3ef2f13dd3 💄 Redesign the post tags and categories page 2025-12-06 20:40:28 +08:00
36b0f55a47 🐛 Fix inconsistent of margin in post silver list 2025-12-06 20:24:54 +08:00
bc7a6e865e 🐛 Fix some issues 2025-12-06 20:20:54 +08:00
2ff60fc4ff 💫 List loading state switch animation 2025-12-06 19:54:34 +08:00
ea93aa144e 🐛 Fix some bugs in post search UI 2025-12-06 19:47:36 +08:00
e4cd0c99df 💄 Optimize skeleton effect 2025-12-06 19:01:40 +08:00
dff84dde58 Post list now supports initial filter to prevent some mismatch 2025-12-06 18:47:50 +08:00
16c7b7e764 ♻️ Refactored post loading 2025-12-06 18:20:47 +08:00
240509ceff 🚚 Update files layout of pods 2025-12-06 17:31:12 +08:00
91da9768c1 💄 Adjust the style of the post reply preview 2025-12-06 15:26:23 +08:00
60b8e2bcad 💄 Optimize post reply preview 2025-12-06 13:53:22 +08:00
504e4d55ad 💄 Post list skeleton 2025-12-06 13:31:17 +08:00
38a15bb62a Better loading animation in paginationed list 2025-12-06 13:13:30 +08:00
9d03faf594 ♻️ Migrated to riverpod v3 2025-12-06 13:00:30 +08:00
fd79c11d18 ♻️ Replaced all list with own pagination list 2025-12-06 02:29:11 +08:00
c4ac256896 ♻️ Continued to migrate list pagination 2025-12-06 01:32:46 +08:00
c1fc8ea3fe ♻️ Continued to move riverpod paging utils to own pagination utils 2025-12-06 00:33:06 +08:00
29574ada88 💄 New sticker marketplace card 2025-12-05 01:57:10 +08:00
7369f5d88c Better sticker picker 2025-12-05 01:35:16 +08:00
5b3c138ebe Sticker pack set icon 2025-12-05 01:25:38 +08:00
562bdf62e9 💄 Optimize list and credits 2025-12-05 01:18:24 +08:00
a73672925e 🐛 Fix dozens bugs 2025-12-05 01:03:42 +08:00
c585522c35 🐛 Fixes in new pagination list 2025-12-05 00:10:25 +08:00
6aba84e506 ⚗️ Testing out new own pagination utils 2025-12-04 23:43:35 +08:00
c6f104afc7 Keep alive of the chat summary 2025-12-04 22:35:45 +08:00
4181fd0090 🐛 Fix some bugs in new chat loading 2025-12-04 22:26:12 +08:00
84bca9601a ♻️ Move the data part out of the chat list UI 2025-12-04 22:20:03 +08:00
31b83b2d27 ♻️ Refactored the chat loading to use more local data 2025-12-04 22:10:07 +08:00
dfcb089c69 Embed images tappable 2025-12-04 01:15:13 +08:00
fe365e8c6d Logout a single session of a authorized device 2025-12-04 01:07:40 +08:00
b5262137ad 💄 Better authorized device page 2025-12-04 01:00:07 +08:00
11e93314c7 🐛 Fix entering room white screen 2025-12-03 22:19:08 +08:00
c8658bc0ca ⬆️ Upgrade cocoapod 2025-12-03 21:15:49 +08:00
b2f689693b ⬆️ Upgrade depenedcies 2025-12-03 21:06:38 +08:00
770 changed files with 76824 additions and 61662 deletions

View File

@@ -12,9 +12,12 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<application
android:label="Solian"

View File

@@ -5,8 +5,9 @@ import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterActivity()
class MainActivity : FlutterFragmentActivity()
{
private val CHANNEL = "dev.solsynth.solian/notifications"

Binary file not shown.

BIN
assets/audio/alert.wav Normal file

Binary file not shown.

BIN
assets/audio/messages.wav Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -23,8 +23,6 @@
"next": "Next",
"createAccount": "Create an Account",
"createAccountDescription": "New to here? We got you covered!",
"affiliationSpell": "Affiliation Spell",
"affiliationSpellHint": "If you have an affiliation spell, enter it here.",
"nickname": "Nickname",
"email": "Email",
"bio": "Bio",
@@ -160,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",
@@ -257,6 +255,24 @@
"walletCurrencyShortGolds": "NSD",
"retry": "Retry",
"creatorHubUnselectedHint": "Pick / create a publisher to get started.",
"publisherFediverse": "Fediverse Actor",
"publisherFediverseDescription": "Configure your publisher's ActivityPub actor for federated social networking",
"publisherFediverseEnabled": "Enabled",
"publisherFediverseDisabled": "Disabled",
"publisherFediverseNotConfigured": "Not configured",
"publisherFediverseEnableHint": "Enable your publisher to interact with fediverse",
"publisherFediverseDisableHint": "Disable your publisher's fediverse actor",
"publisherFediverseEnableConfirm": "Enable fediverse actor?",
"publisherFediverseDisableConfirm": "Disable fediverse actor?",
"publisherFediverseEnabledSuccess": "Fediverse actor enabled successfully",
"publisherFediverseDisabledSuccess": "Fediverse actor disabled successfully",
"publisherFediverseFailedToEnable": "Failed to enable fediverse actor",
"publisherFediverseFailedToDisable": "Failed to disable fediverse actor",
"publisherFediverseWhatIs": "What is Fediverse?",
"publisherFediverseAbout": "The fediverse is a federated network of social platforms. Enabling this feature allows your publisher to interact with users across different ActivityPub-compatible services like Mastodon, PeerTube, and more.",
"publisherFediverseActorUri": "Actor URI",
"publisherFediverseFollowerCount": "Followers",
"publisherFediverseNoFollowers": "No followers yet",
"relationships": "Relationships",
"addFriend": "Send a Friend Request",
"addFriendShort": "Add as Friend",
@@ -267,14 +283,17 @@
"relationshipStatusBlocked": "Blocked",
"blockUser": "Block User",
"unblockUser": "Unblock User",
"forgotRelationship": "Forgot Relationship",
"forgotRelationshipConfirm": "Are you sure you want to forget your relationship with {}? This only changes your relationship status with she / her, she / her might not able to see your friend only posts and use other friend only features. But they can send the friend request again.",
"relationshipDeleted": "Relationship deleted",
"friendRequestAccepted": "Accepted friend request from {}",
"friendRequestDeclined": "Declined friend request from {}",
"requestExpiredIn": "Expired in {}",
"friendSentRequest": "Sent Friend Requests",
"friendSentRequestEmpty": "No sent friend requests",
"friendSentRequestHint": {
"one": "{} friend request sent",
"other": "{} friend requests sent"
"friendRequests": "Friend Requests",
"friendRequestsEmpty": "No related friend requests",
"friendRequestsHint": {
"one": "{} friend request",
"other": "{} friend requests"
},
"levelingProgress": "Leveling Progress",
"levelingProgressExperience": "{} EXP",
@@ -316,7 +335,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",
@@ -343,7 +361,7 @@
"accountSettingsHelp": "Account Settings Help",
"accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support.",
"unauthorized": "Unauthorized",
"unauthorizedHint": "You're not signed in or session expired, please sign in again.",
"unauthorizedHint": "You're not signed in or session expired, please sign in and try again.",
"publisherBelongsTo": "Belongs to {}",
"postContent": "Content",
"postSettings": "Settings",
@@ -684,9 +702,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",
@@ -707,7 +725,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}",
@@ -909,6 +927,7 @@
"fileHash": "File Hash",
"exifData": "EXIF Data",
"postShuffle": "Shuffle Posts",
"swipeToExplore": "Swipe to explore",
"leveling": "Leveling",
"levelingHistory": "Leveling History",
"stellarProgram": "Stellar Program",
@@ -1307,7 +1326,6 @@
"aiThought": "AI Thought",
"aiThoughtTitle": "Let sn-chan think",
"postReferenceUnavailable": "Referenced post is unavailable",
"fabLocation": "FAB Location",
"activities": "Activities",
"presenceTypeGaming": "Playing",
"presenceTypeMusic": "Listening to Music",
@@ -1342,6 +1360,32 @@
"orCreateWith": "Or\ncreate with",
"unindexedFiles": "Unindexed files",
"folder": "Folder",
"rootDirectory": "Root Directory",
"pathSeparator": " / ",
"selectAll": "Select All",
"deselectAll": "Deselect All",
"createDirectory": "Create Directory",
"thisDirectoryIsEmpty": "This directory is empty",
"emptyDirectoryHint": "Upload files or create subdirectories to populate this path.\nDirectories are created implicitly when you upload files to them.",
"noUnindexedFiles": "No unindexed files",
"noUnindexedFilesHint": "All files have been assigned to paths.\nFiles without paths will appear here.",
"clearAllRecycledFiles": "Clear All Recycled Files",
"clearRecycledFilesDescription": "Permanently delete all marked recycled files to free up space.",
"allFiles": "All files",
"confirmDeleteSelectedFiles": "Are you sure you want to delete the selected files?",
"deleteSelectedFiles": "Delete Selected Files",
"confirmClearRecycledFiles": "Are you sure you want to clear all recycled files?",
"clearRecycledFiles": "Clear Recycled Files",
"failedToUploadFile": "Failed to upload file: {}",
"deletedFilesCount": "Deleted {} files.",
"failedToDeleteSelectedFiles": "Failed to delete selected files.",
"clearedRecycledFilesCount": "Cleared {} recycled files.",
"failedToClearRecycledFiles": "Failed to clear recycled files.",
"root": "Root",
"searchFiles": "Search files...",
"selectedCount": "{} selected",
"filesSelected": "{} files selected",
"fileSelected": "{} file selected",
"clearCompleted": "Clear Completed",
"uploadSuccess": "Upload successful!",
"wouldYouLikeToViewFile": "Would you like to view the file?",
@@ -1477,6 +1521,8 @@
"lotteryMultiplierRequired": "Please enter a multiplier",
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
"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.",
@@ -1486,8 +1532,200 @@
"createAccountAgreeTerms": "I've read these terms and agree to the terms of service.",
"createAccountProfile": "Create your profile",
"createAccountToS": "Review Terms & Conditions",
"accountActivationAlert": "Remember to activate your account",
"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"
"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",
"noSubscription": "No subscriptions",
"embedLinks": {
"one": "{} link",
"other": "{} links"
},
"searchFediverse": "Search Fediverse",
"searchFediverseHint": "Search by address, e.g. {}",
"searchFediverseEmpty": "Search for users on other ActivityPub instances",
"searchFediverseNoResults": "No users found for this search",
"fediverseUsers": "Fediverse Users",
"following": "Following",
"followers": "Followers",
"follow": "Follow",
"unfollow": "Unfollow",
"followedUser": "Followed @{}",
"unfollowedUser": "Unfollowed @{}",
"followingEmpty": "You're not following anyone yet",
"followersEmpty": "No followers yet",
"followingEmptyHint": "Start by searching for users or explore other instances",
"fediversePost": "Fediverse Post",
"fediversePostDescribe": "Post from the Fediverse Network",
"settingsShowFediverseContent": "Show Fediverse Content",
"universalSearch": "Universal Search",
"universalSearchDescription": "Search content across the Solar Network and the fediverse network.",
"tasks": "Tasks",
"tasksCount": {
"one": "{} task",
"other": "{} tasks"
},
"setAsThumbnail": "Set as thumbnail",
"unsetAsThumbnail": "Unset as thumbnail",
"sidebar": "Sidebar",
"dropFilesHere": "Drop your files here",
"dragAndDropToAttach": "Drag your files here to attach it",
"customize": "Customize",
"dashboardCustomizeTitle": "Customize Dashboard",
"dashboardTabVertical": "Vertical",
"dashboardTabHorizontal": "Horizontal",
"dashboardLayoutVertical": "Vertical Layout",
"dashboardLayoutHorizontal": "Horizontal Layout",
"dashboardAvailableCards": "Available Cards",
"dashboardDisplaySettings": "Display Settings",
"dashboardShowSearchBar": "Show Search Bar",
"dashboardShowClockAndCountdown": "Show Clock and Countdown",
"dashboardResetToDefaults": "Reset to Defaults",
"dashboardResetToDefaultsSubtitle": "Restore default dashboard layout and settings",
"dashboardResetConfirmMessage": "This will restore the dashboard to its default layout and settings. This action cannot be undone.",
"dashboardResetConfirmTitle": "Reset Dashboard",
"dashboardCardCheckIn": "Check In",
"dashboardCardFortuneGraph": "Fortune Graph",
"dashboardCardFortune": "Fortune",
"dashboardCardFeaturedPosts": "Featured Posts",
"dashboardCardFriends": "Friends",
"dashboardCardNotifications": "Notifications",
"dashboardCardChats": "Chats",
"dashboardCardActivityColumn": "Activity Column",
"dashboardCardPostsColumn": "Posts Column",
"dashboardCardSocialColumn": "Social Column",
"dashboardCardChatsColumn": "Chats Column",
"dashboardCardActivityColumnDescription": "Check In, Fortune Graph & Fortune",
"dashboardCardPostsColumnDescription": "Featured Posts",
"dashboardCardSocialColumnDescription": "Friends & Notifications",
"dashboardCardChatsColumnDescription": "Recent Chats",
"searchAccountsHint": "Search across the Solar Network and fediverse network.",
"fitnessActivity": "Fitness Activity",
"loadingFitnessData": "Loading fitness data...",
"fitnessDataNotAvailable": "Fitness Data Not Available",
"fitnessDataNotAvailableDescription": "Fitness data is only available on iOS and Android devices.",
"fitnessPermissionRequired": "Permission Required",
"fitnessPermissionRequiredDescription": "To access your fitness data, we need permission to read your health information.",
"requestPermission": "Request Permission",
"noFitnessData": "No Fitness Data",
"noFitnessDataDescription": "No fitness data found. Start tracking your workouts to see them here.",
"totalWorkouts": "Total Workouts",
"totalDuration": "Total Duration",
"totalCalories": "Total Calories",
"noWorkoutsOnDate": "No workouts on {}",
"noWorkoutsOnDateDescription": "No workouts found for this date.",
"workoutsOnDate": "Workouts on {}",
"refresh": "Refresh",
"fitnessWorkoutTypeBadminton": "Badminton",
"fitnessWorkoutTypeBaseball": "Baseball",
"fitnessWorkoutTypeBasketball": "Basketball",
"fitnessWorkoutTypeBiking": "Biking",
"fitnessWorkoutTypeCalisthenics": "Calisthenics",
"fitnessWorkoutTypeCricket": "Cricket",
"fitnessWorkoutTypeDancing": "Dancing",
"fitnessWorkoutTypeElliptical": "Elliptical",
"fitnessWorkoutTypeFencing": "Fencing",
"fitnessWorkoutTypeFrisbeeDisc": "Frisbee Disc",
"fitnessWorkoutTypeGolf": "Golf",
"fitnessWorkoutTypeGymnastics": "Gymnastics",
"fitnessWorkoutTypeHiking": "Hiking",
"fitnessWorkoutTypeHockey": "Hockey",
"fitnessWorkoutTypeJumpRope": "Jump Rope",
"fitnessWorkoutTypeKickboxing": "Kickboxing",
"fitnessWorkoutTypeLacrosse": "Lacrosse",
"fitnessWorkoutTypeMartialArts": "Martial Arts",
"fitnessWorkoutTypeMindAndBody": "Mind and Body",
"fitnessWorkoutTypePilates": "Pilates",
"fitnessWorkoutTypeRowing": "Rowing",
"fitnessWorkoutTypeRowingMachine": "Rowing Machine",
"fitnessWorkoutTypeRugby": "Rugby",
"fitnessWorkoutTypeRunning": "Running",
"fitnessWorkoutTypeSailing": "Sailing",
"fitnessWorkoutTypeSkiing": "Skiing",
"fitnessWorkoutTypeSnowSports": "Snow Sports",
"fitnessWorkoutTypeSoftball": "Softball",
"fitnessWorkoutTypeStairClimbing": "Stair Climbing",
"fitnessWorkoutTypeStairClimbingMachine": "Stair Climbing Machine",
"fitnessWorkoutTypeStepTraining": "Step Training",
"fitnessWorkoutTypeSurfing": "Surfing",
"fitnessWorkoutTypeSwimming": "Swimming",
"fitnessWorkoutTypeTableTennis": "Table Tennis",
"fitnessWorkoutTypeTennis": "Tennis",
"fitnessWorkoutTypeCrossTraining": "Cross Training",
"fitnessWorkoutTypeCurling": "Curling",
"fitnessWorkoutTypeCrossCountrySkiing": "Cross Country Skiing",
"fitnessWorkoutTypeEquestrianSports": "Equestrian Sports",
"fitnessWorkoutTypeFishing": "Fishing",
"fitnessWorkoutTypeFunctionalStrengthTraining": "Functional Strength Training",
"fitnessWorkoutTypeHandCycling": "Hand Cycling",
"fitnessWorkoutTypeMixedCardio": "Mixed Cardio",
"fitnessWorkoutTypeOther": "Other",
"fitnessWorkoutTypePaddleSports": "Paddle Sports",
"fitnessWorkoutTypePickleball": "Pickleball",
"fitnessWorkoutTypeRacquetball": "Racquetball",
"fitnessWorkoutTypeRockClimbing": "Rock Climbing",
"fitnessWorkoutTypeSkating": "Skating",
"fitnessWorkoutTypeSnowboarding": "Snowboarding",
"fitnessWorkoutTypeSoccer": "Soccer",
"fitnessWorkoutTypeSquash": "Squash",
"fitnessWorkoutTypeStrengthTraining": "Strength Training",
"fitnessWorkoutTypeVolleyball": "Volleyball",
"fitnessWorkoutTypeWalking": "Walking",
"fitnessWorkoutTypeWeightlifting": "Weightlifting",
"fitnessWorkoutTypeYoga": "Yoga",
"fitnessWorkoutTypeDefault": "Fitness"
}

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",
@@ -265,6 +265,9 @@
"relationshipStatusBlocked": "Blocked",
"blockUser": "Block User",
"unblockUser": "Unblock User",
"forgotRelationship": "Olvidar Relación",
"forgotRelationshipConfirm": "¿Estás seguro de que quieres olvidar tu relación con {}?",
"relationshipDeleted": "Relación eliminada",
"friendRequestAccepted": "Accepted friend request from {}",
"friendRequestDeclined": "Declined friend request from {}",
"requestExpiredIn": "Expired in {}",
@@ -314,7 +317,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 +684,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 +707,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 +1476,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

@@ -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",
@@ -265,6 +265,9 @@
"relationshipStatusBlocked": "Blocked",
"blockUser": "Block User",
"unblockUser": "Unblock User",
"forgotRelationship": "관계 잊기",
"forgotRelationshipConfirm": "{}와의 관계를 잊으시겠습니까?",
"relationshipDeleted": "관계가 삭제되었습니다",
"friendRequestAccepted": "Accepted friend request from {}",
"friendRequestDeclined": "Declined friend request from {}",
"requestExpiredIn": "Expired in {}",
@@ -314,7 +317,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 +684,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 +707,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 +1476,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

@@ -64,8 +64,8 @@
"authFactorTOTPDescription": "由 TOTP 验证器生成的一次性验证码。",
"authFactorInAppNotify": "应用内通知",
"authFactorInAppNotifyDescription": "通过应用内通知发送的一次性验证码。",
"authFactorPin": "Pin 码",
"authFactorPinDescription": "它由6位数字组成。它不能用于登录。 当执行一些危险的操作时,系统要求输入此 PIN 进行确认。",
"authFactorPin": "PIN 码",
"authFactorPinDescription": "此 PIN 码由 6 位数字组成,不可用于登录。执行高风险操作时,系统要求输入此 PIN 进行确认。",
"realms": "领域",
"createRealm": "创建领域",
"createRealmHint": "结识志同道合的朋友、建立社区等等。",
@@ -191,6 +191,7 @@
"statusActivityEndedTitle": "{} 正在 {} {} 直到 {}",
"appSettings": "应用设置",
"accountSettings": "账号设置",
"accounts": "账号",
"settings": "设置",
"language": "语言",
"accountLanguageHint": "此语言将用于电子邮件和推送通知。",
@@ -265,6 +266,9 @@
"relationshipStatusBlocked": "已屏蔽",
"blockUser": "屏蔽用户",
"unblockUser": "解除屏蔽用户",
"forgotRelationship": "忘记关系",
"forgotRelationshipConfirm": "确定要忘记与 {} 的关系吗?这只会改变您对他/她的关系,他可能看到不到您的友限帖,但是您仍然可以看到他/她的。他/她也可以再次提出好友申请。",
"relationshipDeleted": "关系已删除",
"friendRequestAccepted": "已接受 {} 的好友请求",
"friendRequestDeclined": "已拒绝 {} 的好友请求",
"requestExpiredIn": "{} 后过期",
@@ -314,7 +318,6 @@
"settingsAutoTranslate": "自动翻译",
"settingsHideBottomNav": "隐藏底部导航",
"settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "愚人节功能",
"settingsEnterToSend": "按下 Enter 发送",
"settingsTransparentAppBar": "使用完全透明的状态栏",
"settingsCustomFonts": "自定义字体",
@@ -411,7 +414,7 @@
"contactMethodAddressHint": "输入您的现实地址",
"contactMethodEmailDescription": "您的电子邮件将用于帐户恢复和通知",
"contactMethodPhoneDescription": "您的电话号码将用于帐户恢复和通知",
"contactMethodAddressDescription": "您的实际地址将用于运输和计费目的。",
"contactMethodAddressDescription": "您的地址将用于物流和账单。",
"contactMethodVerified": "已验证",
"contactMethodUnverified": "未认证",
"contactMethodVerify": "验证联系方式",
@@ -456,6 +459,7 @@
"checkInResultT2": "中平",
"checkInResultT3": "吉",
"checkInResultT4": "大吉",
"checkInResultT5": "特殊",
"accountProfileView": "查看个人资料",
"unspecified": "未指定",
"added": "已添加",
@@ -469,8 +473,8 @@
"description": "描述",
"pinCode": "PIN 码",
"biometric": "生物识别",
"enterPinToConfirm": "请输入您的6位数字 PIN 以确认付款",
"clearPin": "清除 PIN",
"enterPinToConfirm": "请输入您的 6 位数字 PIN 以确认付款",
"clearPin": "清除 PIN",
"useBiometricToConfirm": "使用生物特征认证来确认付款",
"touchSensorToAuthenticate": "触摸传感器进行身份验证",
"authenticating": "认证中……",
@@ -479,7 +483,7 @@
"processingPayment": "处理付款中……",
"pleaseWait": "请稍候",
"paymentFailed": "付款失败,请重试。",
"invalidPin": "错误的 PIN。请再试一次。",
"invalidPin": "PIN 码错误,请重试。",
"biometricAuthFailed": "生物识别身份验证失败。请重试。",
"paymentSuccess": "付款成功完成!",
"membershipPurchaseSuccess": "好耶,会员购买成功!",
@@ -523,7 +527,7 @@
"safetyReportReasonHint": "请提供更多证据……",
"safetyReportSubmit": "提交举报",
"safetyReportSubmitting": "提交中……",
"safetyReportSuccess": "举报成功,感谢您参与维护社区健康发展。",
"safetyReportSuccess": "举报提交成功,感谢您协助维护社区安全。",
"safetyReportError": "举报失败,请稍后重试。",
"safetyReportReasonRequired": "请提供举报证据",
"safetyReportTypeSpam": "垃圾或导向错误",
@@ -597,7 +601,7 @@
"abuseReportReason": "补充详情",
"abuseReportReasonHint": "请提供更多详情……",
"abuseReportSubmit": "提交举报",
"abuseReportSuccess": "举报提交成功,感谢你为社区维护作出贡献。",
"abuseReportSuccess": "举报提交成功,感谢您协助维护社区安全。",
"abuseReportError": "无法提交举报,请稍后再试。",
"abuseReportReasonRequired": "请提供关于此事件的细节",
"abuseReportSuccessTitle": "举报已提交",
@@ -682,9 +686,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": "举报",
@@ -705,7 +709,7 @@
"aboutScreenDeveloperSectionTitle": "开发者",
"aboutScreenContactUsTitle": "联系我们",
"aboutScreenLicenseTitle": "许可",
"aboutScreenLicenseContent": "无法翻译",
"aboutScreenLicenseContent": "GNU Affero 通用公共许可证 v3.0",
"aboutScreenCopyright": "版权所有 © Solsynth {}",
"aboutScreenMadeWith": "由 Solar Network 团队用 ❤︎️ 制作",
"aboutScreenFailedToLoadPackageInfo": "无法加载包信息:{error}",
@@ -935,8 +939,8 @@
"keyName": "密钥名称",
"newKeyGenerated": "新密钥已生成",
"copyKeyHint": "请安全的保存该密钥,你不会再次看到它。",
"rotateKey": "旋转密钥",
"rotateBotKey": "旋转密钥",
"rotateKey": "轮换密钥",
"rotateBotKey": "轮换机器人密钥",
"rotateBotKeyHint": "你确认要旋转这个密钥?久的密钥会立即失效,该操作无法撤销。",
"webFeedArticleCount": {
"zero": "没有文章",
@@ -1217,7 +1221,7 @@
"addRemark": "添加备注",
"transferRemark": "转账备注",
"addRemarkForTransfer": "为转账添加备注",
"enterPinToConfirmTransfer": "输入您的 6 位PIN码以确认转账",
"enterPinToConfirmTransfer": "输入您的 6 位 PIN 码以确认转账",
"transferCreatedSuccessfully": "转账成功创建!",
"postUpdate": "更新",
"fileMetadata": "文件元数据",
@@ -1269,7 +1273,7 @@
"cost": "花费",
"multiplier": "倍率",
"prizeWon": "获奖者",
"pending": "待开奖",
"pending": "准备中",
"drawn": "已开奖",
"won": "赢",
"lost": "输",
@@ -1476,18 +1480,72 @@
"lotteryMultiplierRange": "倍率必须在 1 到 10 之间",
"dropToShare": "拖到此处以分享",
"affiliationSpell": "邀请码",
"affiliationSpellHint": "如果您有邀请码,请在此处输入。",
"affiliationSpellHint": "如果您有邀请码,请在这里输入。",
"friendsOnline": "在线好友",
"createAccountAlmostThere": "就差一步",
"createAccountAlmostThereHint": "您距离加入 Solar Network 仅一步之遥!请解决接下来显示的验证码谜题。",
"createAccountNotice": "创建账户前需要了解的事项:",
"createAccountConfirmEmail": "账户创建后,您需要前往您的邮箱收件箱激活账户,以获得使用所有功能的权限。",
"createAccountNoAltAccounts": "Solar Network 禁止使用多个或替代账户,这违反我们的服务条款。",
"createAccountAgreeTerms": "我已阅读并同意服务条款。",
"createAccountAlmostThere": "即将完成",
"createAccountAlmostThereHint": "您距离加入 Solar Network 只差一步了!请完成接下来显示的人机验证。",
"createAccountNotice": "创建账户前需要了解的事项:",
"createAccountConfirmEmail": "账户创建后,您需要去邮箱激活您的账户,以获得使用所有功能的权限。",
"createAccountNoAltAccounts": "Solar Network 禁止使用多个账户或替代账户,这违反我们的服务条款。",
"createAccountAgreeTerms": "我已阅读并同意这些服务协议。",
"createAccountProfile": "创建您的个人资料",
"createAccountToS": "查看条款与条件",
"accountActivationAlert": "请记住激活您的账户",
"accountActivationAlertHint": "未激活的账户可能会导致各种权限问题,请点击我们发送到您邮箱收件箱的链接来激活您的账户。",
"accountActivationResendHint": "没收到?请尝试点击下方按钮重新发送。如果您在账户未激活期间需要更新邮箱,请随时联系我们的客服。",
"accountActivationResend": "重新发送"
"createAccountToS": "查看用户协议 & 服务条款",
"updateYourProfileDescription": "在 Solar Network 上完善你的个人形象。",
"realmsDescription": "管理您已加入的领域。",
"exploreDescription": "探索 Solar Network 上的内容。",
"accountDescription": "关于您账户的信息。",
"chatDescription": "群组聊天与私聊",
"connectionServerDown": "无法连接",
"appSettingsDescription": "自定义您的应用程序。",
"accountSettingsDescription": "管理您在 Solar Network 上的偏好设置。",
"walletDescription": "您的源点钱包。",
"relationshipsDescription": "好友与联系人。",
"notificationsDescription": "查看最近与您相关的事情。",
"settingsFestivalFeatures": "节日限定功能",
"categoriesAndTags": "类别 & 标签",
"webArticlesStandDescription": "浏览外部网站文章。",
"aboutDescription": "了解更多有关 Solar Network 的信息。",
"abuseReportsDescription": "查看并管理滥用报告。",
"stickerMarketplaceDescription": "浏览并从 Solar Network 市场添加贴纸包。",
"webFeedsDescription": "浏览并订阅 Solar Network 的网页订阅源。",
"discoverRealmsDescription": "发现领域并加入它们。",
"postShuffleDescription": "打乱帖子以随机查看帖子。",
"levelingDescription": "查看您的升级进度和历史记录。",
"notableDayToday": "{} 就是今天!",
"authSessionLogout": "注销登录",
"authSessionLogoutHint": "确认要退出登录吗?此操作将仅结束当前浏览会话,不会影响其他已登录的设备。",
"filesDescription": "在 Solar Network Drive 管理您的文件。",
"postComposeDescription": "撰写一篇新帖子",
"searchPostsDescription": "输入标题、正文或关键词进行搜索。",
"accountActivationAlert": "激活您的账号",
"accountActivationAlertHint": "账户未激活将无法使用完整功能,请查收邮件并点击链接完成激活。",
"accountActivationResendHint": "没收到邮件?请点击下方按钮重新发送。若需在账户未激活时更新邮箱,请联系我们的客服。",
"accountActivationResend": "重新发送",
"ipAddress": "IP 地址",
"noFurtherData": "没有更多数据",
"searchAnything": "搜索任何内容……",
"tapToViewAllNotifications": "轻点显示所有通知",
"mostRecent": "最近",
"noNotificationsYet": "没有通知",
"recentChats": "最近聊天",
"noFeaturedPostsAvailable": "没有可用的精选文章",
"searchChatsAndPages": "搜索聊天或页面……",
"dashboard": "仪表板",
"dashboardDescription": "在一处整合您的所有数据。",
"postTagsCategories": "文章标签和分类",
"postTagsCategoriesDescription": "按类别和标签浏览帖子。",
"debugLogs": "调试日志",
"debugLogsDescription": "查看调试日志以进行故障排除。",
"pinChatRoom": "置顶聊天室",
"pinChatRoomDescription": "将此聊天室固定到顶部。",
"chatRoomPinned": "聊天室已置顶。",
"chatRoomUnpinned": "聊天室已取消置顶。",
"pinnedChatRoom": "已置顶的聊天室",
"settingsGroupedChatList": "启用群组聊天列表分组",
"settingsNotifyWithHaptic": "通知触感反馈",
"settingsDashSearchEngine": "网络搜索引擎",
"settingsDashSearchEngineHelper": "请使用 %s 作为搜索词的占位符。",
"settingsDefaultScreen": "默认起始页面",
"notableDayChristmas": "圣诞节",
"notableDayNewYear": "新年"
}

File diff suppressed because it is too large Load Diff

View File

@@ -265,6 +265,9 @@
"relationshipStatusBlocked": "已屏蔽",
"blockUser": "屏蔽用戶",
"unblockUser": "解除屏蔽用戶",
"forgotRelationship": "忘記關係",
"forgotRelationshipConfirm": "確定要忘記與 {} 的關係嗎?",
"relationshipDeleted": "關係已刪除",
"friendRequestAccepted": "已接受 {} 的好友請求",
"friendRequestDeclined": "已拒絕 {} 的好友請求",
"requestExpiredIn": "{} 後過期",
@@ -314,7 +317,6 @@
"settingsAutoTranslate": "自動翻譯",
"settingsHideBottomNav": "隱藏底部導航",
"settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "愚人節功能",
"settingsEnterToSend": "按下 Enter 發送",
"settingsTransparentAppBar": "使用完全透明的狀態欄",
"settingsCustomFonts": "自定義字體",
@@ -456,6 +458,7 @@
"checkInResultT2": "中平",
"checkInResultT3": "吉",
"checkInResultT4": "大吉",
"checkInResultT5": "特殊",
"accountProfileView": "查看個人資料",
"unspecified": "未指定",
"added": "已添加",
@@ -682,9 +685,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 +1063,7 @@
"failedToDeleteRecycledFiles": "已回收檔案刪除失敗",
"upload": "上傳",
"deleteMessage": "刪除訊息",
"deleteMessageConfirmation": "確定要刪除此郵件嗎?",
"deleteMessageConfirmation": "確定要刪除此郵件嗎",
"customReaction": "自訂反應",
"customReactions": "自訂反應",
"stickerPlaceholder": "貼紙佔位符",
@@ -1131,348 +1134,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"
}

Binary file not shown.

View File

@@ -1,4 +1,4 @@
platform :ios, '15.0'
platform :ios, '16.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -1,5 +1,7 @@
PODS:
- Alamofire (5.10.2)
- Alamofire (5.11.1)
- audio_session (0.0.1):
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- croppy (0.0.1):
@@ -42,83 +44,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.8.0):
- FirebaseCore (~> 12.8.0)
- Firebase/Crashlytics (12.8.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 12.4.0)
- Firebase/Messaging (12.4.0):
- FirebaseCrashlytics (~> 12.8.0)
- Firebase/Messaging (12.8.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0)
- firebase_analytics (12.0.4):
- FirebaseMessaging (~> 12.8.0)
- firebase_analytics (12.1.1):
- firebase_core
- FirebaseAnalytics (= 12.4.0)
- FirebaseAnalytics (= 12.8.0)
- Flutter
- firebase_core (4.2.1):
- Firebase/CoreOnly (= 12.4.0)
- firebase_core (4.4.0):
- Firebase/CoreOnly (= 12.8.0)
- Flutter
- firebase_crashlytics (5.0.5):
- Firebase/Crashlytics (= 12.4.0)
- firebase_crashlytics (5.0.7):
- Firebase/Crashlytics (= 12.8.0)
- firebase_core
- Flutter
- firebase_messaging (16.0.4):
- Firebase/Messaging (= 12.4.0)
- firebase_messaging (16.1.1):
- Firebase/Messaging (= 12.8.0)
- firebase_core
- Flutter
- FirebaseAnalytics (12.4.0):
- FirebaseAnalytics/Default (= 12.4.0)
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- FirebaseAnalytics (12.8.0):
- FirebaseAnalytics/Default (= 12.8.0)
- FirebaseCore (~> 12.8.0)
- FirebaseInstallations (~> 12.8.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.8.0):
- FirebaseCore (~> 12.8.0)
- FirebaseInstallations (~> 12.8.0)
- GoogleAppMeasurement/Default (= 12.8.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.8.0):
- FirebaseCoreInternal (~> 12.8.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseCoreInternal (12.4.0):
- FirebaseCoreExtension (12.8.0):
- FirebaseCore (~> 12.8.0)
- FirebaseCoreInternal (12.8.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.8.0):
- FirebaseCore (~> 12.8.0)
- FirebaseInstallations (~> 12.8.0)
- FirebaseRemoteConfigInterop (~> 12.8.0)
- FirebaseSessions (~> 12.8.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.8.0):
- FirebaseCore (~> 12.8.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.8.0):
- FirebaseCore (~> 12.8.0)
- FirebaseInstallations (~> 12.8.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.8.0)
- FirebaseSessions (12.8.0):
- FirebaseCore (~> 12.8.0)
- FirebaseCoreExtension (~> 12.8.0)
- FirebaseInstallations (~> 12.8.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
@@ -140,8 +142,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 +156,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.8.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.8.0):
- GoogleAdsOnDeviceConversion (~> 3.2.0)
- GoogleAppMeasurement/Core (= 12.8.0)
- GoogleAppMeasurement/IdentitySupport (= 12.8.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.8.0):
- GoogleAppMeasurement/Core (= 12.8.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
@@ -210,10 +213,17 @@ PODS:
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- health (13.1.4):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- in_app_review (2.0.0):
- Flutter
- irondash_engine_context (0.0.1):
- Flutter
- just_audio (0.0.1):
- Flutter
- FlutterMacOS
- KeychainAccess (4.2.2)
- Kingfisher (8.6.2)
- KingfisherWebP (1.7.2):
@@ -254,9 +264,8 @@ PODS:
- Flutter
- pasteboard (0.0.1):
- Flutter
- path_provider_foundation (0.0.1):
- permission_handler_apple (9.3.0):
- Flutter
- FlutterMacOS
- pointer_interceptor_ios (0.0.1):
- Flutter
- PromisesObjC (2.4.0)
@@ -264,13 +273,17 @@ PODS:
- PromisesObjC (= 2.4.0)
- protocol_handler_ios (0.0.1):
- Flutter
- quick_actions_ios (0.0.1):
- Flutter
- receive_sharing_intent (1.8.1):
- Flutter
- record_ios (1.1.0):
- record_ios (1.2.0):
- Flutter
- SDWebImage (5.21.5):
- SDWebImage/Core (= 5.21.5)
- SDWebImage/Core (5.21.5)
- sensors_plus (0.0.1):
- Flutter
- SDWebImage (5.21.3):
- SDWebImage/Core (= 5.21.3)
- SDWebImage/Core (5.21.3)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -281,25 +294,25 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.50.4):
- sqlite3/common (= 3.50.4)
- sqlite3/common (3.50.4)
- sqlite3/dbstatvtab (3.50.4):
- sqlite3 (3.51.1):
- sqlite3/common (= 3.51.1)
- sqlite3/common (3.51.1)
- sqlite3/dbstatvtab (3.51.1):
- sqlite3/common
- sqlite3/fts5 (3.50.4):
- sqlite3/fts5 (3.51.1):
- sqlite3/common
- sqlite3/math (3.50.4):
- sqlite3/math (3.51.1):
- sqlite3/common
- sqlite3/perf-threadsafe (3.50.4):
- sqlite3/perf-threadsafe (3.51.1):
- sqlite3/common
- sqlite3/rtree (3.50.4):
- sqlite3/rtree (3.51.1):
- sqlite3/common
- sqlite3/session (3.50.4):
- sqlite3/session (3.51.1):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.50.4)
- sqlite3 (~> 3.51.1)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
@@ -313,12 +326,16 @@ PODS:
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
- video_thumbnail (0.0.1):
- Flutter
- libwebp
- wakelock_plus (0.0.1):
- Flutter
- WebRTC-SDK (137.7151.04)
DEPENDENCIES:
- Alamofire
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
@@ -334,13 +351,16 @@ 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`)
- health (from `.symlinks/plugins/health/ios`)
- 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`)
- just_audio (from `.symlinks/plugins/just_audio/darwin`)
- Kingfisher (~> 8.0)
- KingfisherWebP
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
@@ -350,11 +370,13 @@ DEPENDENCIES:
- native_exif (from `.symlinks/plugins/native_exif/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
- quick_actions_ios (from `.symlinks/plugins/quick_actions_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`)
@@ -363,6 +385,7 @@ DEPENDENCIES:
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
@@ -398,6 +421,8 @@ SPEC REPOS:
- WebRTC-SDK
EXTERNAL SOURCES:
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
croppy:
@@ -428,8 +453,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:
@@ -438,10 +463,16 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_webrtc/ios"
gal:
:path: ".symlinks/plugins/gal/darwin"
health:
:path: ".symlinks/plugins/health/ios"
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"
just_audio:
:path: ".symlinks/plugins/just_audio/darwin"
livekit_client:
:path: ".symlinks/plugins/livekit_client/ios"
local_auth_darwin:
@@ -456,16 +487,20 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
pasteboard:
:path: ".symlinks/plugins/pasteboard/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
pointer_interceptor_ios:
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
protocol_handler_ios:
:path: ".symlinks/plugins/protocol_handler_ios/ios"
quick_actions_ios:
:path: ".symlinks/plugins/quick_actions_ios/ios"
receive_sharing_intent:
: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:
@@ -482,11 +517,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_thumbnail:
:path: ".symlinks/plugins/video_thumbnail/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
Alamofire: eec6cd8f73b242b59e34153a606a909eb9864b14
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
@@ -494,37 +532,40 @@ 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: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
firebase_analytics: b5a19eaf3e4bf4187b0815ef4850b8916e2bc549
firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398
firebase_crashlytics: 28b8f39df8104131376393e6af658b8b77dd120f
firebase_messaging: 343de01a8d3e18b60df0c6d37f7174c44ae38e02
FirebaseAnalytics: f20bbad8cb7f65d8a5eaefeb424ae8800a31bdfc
FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c
FirebaseCoreExtension: 6605938d51f765d8b18bfcafd2085276a252bee2
FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21
FirebaseCrashlytics: fb31c6907e5b52aa252668394d3f1ab326df1511
FirebaseInstallations: 6a14ab3d694ebd9f839c48d330da5547e9ca9dc0
FirebaseMessaging: 7f42cfd10ec64181db4e01b305a613791c8e782c
FirebaseRemoteConfigInterop: 869ddca16614f979e5c931ece11fbb0b8729ed41
FirebaseSessions: d614ca154c63dbbc6c10d6c38259c2162c4e7c9b
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: 72c9a682fec6290327ea5e3c4b829b247fcb2c17
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
health: 32d2fbc7f26f9a2388d1a514ce168adbfa5bda65
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
Kingfisher: 23d18f54677d973b713e54ce6a8f5eef6e7056ba
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
@@ -538,27 +579,30 @@ SPEC CHECKSUMS:
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
record_ios: 412daca2350b228e698fffcd08f1f94ceb1e3844
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
sensors_plus: 6a11ed0c2e1d0bd0b20b4029d3bad27d96e0c65b
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
PODFILE CHECKSUM: 585198f58dca90ac6492607c83a8d17045ab3852
PODFILE CHECKSUM: 5a3652182a0f0e1093d3ac36dcabc72f6cc46b3a
COCOAPODS: 1.16.2

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
@@ -11,7 +11,14 @@
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
5D8143680678FCD1D1827271 /* Pods_Solian_Watch_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */; };
7301DB032F08D99C008390F3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7301DB022F08D99C008390F3 /* WidgetKit.framework */; };
7301DB052F08D99C008390F3 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7301DB042F08D99C008390F3 /* SwiftUI.framework */; };
7301DB102F08D99D008390F3 /* SolianWidgetExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7301DB012F08D99C008390F3 /* SolianWidgetExtensionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
7310A7DF2EB10963002C0FD3 /* Solian Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 7310A7D42EB10962002C0FD3 /* Solian Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
73595B1B2F17FF8000AAD53C /* SfxMessage.caf in Resources */ = {isa = PBXBuildFile; fileRef = 73595B162F17FF8000AAD53C /* SfxMessage.caf */; };
73595B1C2F17FF8000AAD53C /* SfxNotification.caf in Resources */ = {isa = PBXBuildFile; fileRef = 73595B172F17FF8000AAD53C /* SfxNotification.caf */; };
73595B832F1803D300AAD53C /* SfxNotification.caf in Resources */ = {isa = PBXBuildFile; fileRef = 73595B172F17FF8000AAD53C /* SfxNotification.caf */; };
73595B842F1803D300AAD53C /* SfxMessage.caf in Resources */ = {isa = PBXBuildFile; fileRef = 73595B162F17FF8000AAD53C /* SfxMessage.caf */; };
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; };
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -36,6 +43,13 @@
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
7301DB0E2F08D99D008390F3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 7301DB002F08D99C008390F3;
remoteInfo = SolianWidgetExtensionExtension;
};
73ACDFC12E3D0E6100B63535 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
@@ -77,6 +91,7 @@
dstPath = "";
dstSubfolderSpec = 13;
files = (
7301DB102F08D99D008390F3 /* SolianWidgetExtensionExtension.appex in Embed Foundation Extensions */,
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */,
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */,
73CDD6812DEC00480059D95D /* SolianNotificationService.appex in Embed Foundation Extensions */,
@@ -117,7 +132,13 @@
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
7301DB012F08D99C008390F3 /* SolianWidgetExtensionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianWidgetExtensionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
7301DB022F08D99C008390F3 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
7301DB042F08D99C008390F3 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
7301DB162F08D9A5008390F3 /* SolianWidgetExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SolianWidgetExtensionExtension.entitlements; sourceTree = "<group>"; };
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Solian Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
73595B162F17FF8000AAD53C /* SfxMessage.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = SfxMessage.caf; sourceTree = "<group>"; };
73595B172F17FF8000AAD53C /* SfxNotification.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = SfxNotification.caf; sourceTree = "<group>"; };
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
@@ -151,6 +172,13 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
7301DB142F08D99D008390F3 /* Exceptions for "SolianWidgetExtension" folder in "SolianWidgetExtensionExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 7301DB002F08D99C008390F3 /* SolianWidgetExtensionExtension */;
};
73ACDFCA2E3D0E6100B63535 /* Exceptions for "SolianBroadcastExtension" folder in "SolianBroadcastExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
@@ -177,16 +205,23 @@
membershipExceptions = (
CloudFile.swift,
DataExchange.swift,
GroupDefaultSync.swift,
);
target = 73CDD6792DEC00480059D95D /* SolianNotificationService */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
7310A7D52EB10962002C0FD3 /* Solian Watch App */ = {
7301DB062F08D99C008390F3 /* SolianWidgetExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
7301DB142F08D99D008390F3 /* Exceptions for "SolianWidgetExtension" folder in "SolianWidgetExtensionExtension" target */,
);
path = SolianWidgetExtension;
sourceTree = "<group>";
};
7310A7D52EB10962002C0FD3 /* Solian Watch App */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = "Solian Watch App";
sourceTree = "<group>";
};
@@ -233,6 +268,15 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
7301DAFE2F08D99C008390F3 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7301DB052F08D99C008390F3 /* SwiftUI.framework in Frameworks */,
7301DB032F08D99C008390F3 /* WidgetKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
7310A7D12EB10962002C0FD3 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -295,6 +339,8 @@
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */,
73ACDFB82E3D0E6100B63535 /* UIKit.framework */,
C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */,
7301DB022F08D99C008390F3 /* WidgetKit.framework */,
7301DB042F08D99C008390F3 /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -341,17 +387,21 @@
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
7301DB162F08D9A5008390F3 /* SolianWidgetExtensionExtension.entitlements */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
7310A7D52EB10962002C0FD3 /* Solian Watch App */,
7301DB062F08D99C008390F3 /* SolianWidgetExtension */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
91E124CE95BCB4DCD890160D /* Pods */,
498A09270B73B217F0279168 /* Frameworks */,
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */,
73595B162F17FF8000AAD53C /* SfxMessage.caf */,
73595B172F17FF8000AAD53C /* SfxNotification.caf */,
);
sourceTree = "<group>";
};
@@ -364,6 +414,7 @@
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */,
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */,
7301DB012F08D99C008390F3 /* SolianWidgetExtensionExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -408,6 +459,26 @@
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
7301DB002F08D99C008390F3 /* SolianWidgetExtensionExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 7301DB152F08D99D008390F3 /* Build configuration list for PBXNativeTarget "SolianWidgetExtensionExtension" */;
buildPhases = (
7301DAFD2F08D99C008390F3 /* Sources */,
7301DAFE2F08D99C008390F3 /* Frameworks */,
7301DAFF2F08D99C008390F3 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
7301DB062F08D99C008390F3 /* SolianWidgetExtension */,
);
name = SolianWidgetExtensionExtension;
productName = SolianWidgetExtensionExtension;
productReference = 7301DB012F08D99C008390F3 /* SolianWidgetExtensionExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
7310A7D32EB10962002C0FD3 /* Solian Watch App */ = {
isa = PBXNativeTarget;
buildConfigurationList = 7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "Solian Watch App" */;
@@ -515,6 +586,7 @@
73CDD6802DEC00480059D95D /* PBXTargetDependency */,
73C305D72E0BE878009035B9 /* PBXTargetDependency */,
73ACDFC22E3D0E6100B63535 /* PBXTargetDependency */,
7301DB0F2F08D99D008390F3 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
73268D272DEB012A0076E970 /* Services */,
@@ -531,7 +603,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 2600;
LastSwiftUpdateCheck = 2620;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@@ -539,6 +611,9 @@
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
7301DB002F08D99C008390F3 = {
CreatedOnToolsVersion = 26.2;
};
7310A7D32EB10962002C0FD3 = {
CreatedOnToolsVersion = 26.0.1;
};
@@ -563,6 +638,11 @@
knownRegions = (
en,
Base,
"zh-Hans",
es,
ja,
ko,
"zh-Hant",
);
mainGroup = 97C146E51CF9000F007C117D;
preferredProjectObjectVersion = 77;
@@ -576,6 +656,7 @@
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */,
7310A7D32EB10962002C0FD3 /* Solian Watch App */,
7301DB002F08D99C008390F3 /* SolianWidgetExtensionExtension */,
);
};
/* End PBXProject section */
@@ -588,6 +669,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
7301DAFF2F08D99C008390F3 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
7310A7D22EB10962002C0FD3 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -613,6 +701,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
73595B1B2F17FF8000AAD53C /* SfxMessage.caf in Resources */,
73595B1C2F17FF8000AAD53C /* SfxNotification.caf in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -621,6 +711,8 @@
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
73595B832F1803D300AAD53C /* SfxNotification.caf in Resources */,
73595B842F1803D300AAD53C /* SfxMessage.caf in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
@@ -677,10 +769,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -738,10 +834,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -792,10 +892,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks.sh\"\n";
@@ -852,6 +956,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
7301DAFD2F08D99C008390F3 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
7310A7D02EB10962002C0FD3 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -898,6 +1009,11 @@
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
7301DB0F2F08D99D008390F3 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 7301DB002F08D99C008390F3 /* SolianWidgetExtensionExtension */;
targetProxy = 7301DB0E2F08D99D008390F3 /* PBXContainerItemProxy */;
};
73ACDFC22E3D0E6100B63535 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */;
@@ -1002,7 +1118,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1080,13 +1196,145 @@
};
name = Profile;
};
7301DB112F08D99D008390F3 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = SolianWidgetExtensionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SolianWidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianWidgetExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
7301DB122F08D99D008390F3 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = SolianWidgetExtensionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SolianWidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianWidgetExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
7301DB132F08D99D008390F3 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = SolianWidgetExtensionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SolianWidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianWidgetExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
7310A7E02EB10963002C0FD3 /* Debug */ = {
isa = XCBuildConfiguration;
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 +1384,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 +1433,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 +1480,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 +1522,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 +1561,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 +1601,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;
@@ -1371,7 +1619,7 @@
INFOPLIST_FILE = SolianShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1398,7 +1646,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;
@@ -1416,7 +1664,7 @@
INFOPLIST_FILE = SolianShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1440,7 +1688,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;
@@ -1458,7 +1706,7 @@
INFOPLIST_FILE = SolianShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1482,7 +1730,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 +1772,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 +1811,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;
@@ -1724,7 +1972,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1755,7 +2003,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1785,6 +2033,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
7301DB152F08D99D008390F3 /* Build configuration list for PBXNativeTarget "SolianWidgetExtensionExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7301DB112F08D99D008390F3 /* Debug */,
7301DB122F08D99D008390F3 /* Release */,
7301DB132F08D99D008390F3 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "Solian Watch App" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7310A7D32EB10962002C0FD3"
BuildableName = "Solian Watch App.app"
BlueprintName = "Solian Watch App"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7310A7D32EB10962002C0FD3"
BuildableName = "Solian Watch App.app"
BlueprintName = "Solian Watch App"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7310A7D32EB10962002C0FD3"
BuildableName = "Solian Watch App.app"
BlueprintName = "Solian Watch App"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "73ACDFAA2E3D0E6100B63535"
BuildableName = "SolianBroadcastExtension.appex"
BlueprintName = "SolianBroadcastExtension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "73CDD6792DEC00480059D95D"
BuildableName = "SolianNotificationService.appex"
BlueprintName = "SolianNotificationService"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "73C305CD2E0BE878009035B9"
BuildableName = "SolianShareExtension.appex"
BlueprintName = "SolianShareExtension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7301DB002F08D99C008390F3"
BuildableName = "SolianWidgetExtensionExtension.appex"
BlueprintName = "SolianWidgetExtensionExtension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7310A7D32EB10962002C0FD3"
BuildableName = "Solian Watch App.app"
BlueprintName = "Solian Watch App"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "_XCWidgetKind"
value = ""
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetDefaultView"
value = "timeline"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetFamily"
value = "systemMedium"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,6 +1,8 @@
import Flutter
import WidgetKit
import UIKit
import WatchConnectivity
import AppIntents
@main
@objc class AppDelegate: FlutterAppDelegate {
@@ -11,6 +13,9 @@ import WatchConnectivity
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
syncDefaultsToGroup()
WidgetCenter.shared.reloadAllTimelines()
UNUserNotificationCenter.current().delegate = notifyDelegate
let replyableMessageCategory = UNNotificationCategory(
@@ -29,7 +34,8 @@ import WatchConnectivity
GeneratedPluginRegistrant.register(with: self)
// Always initialize and retain a strong reference
setupWidgetSyncChannel()
if WCSession.isSupported() {
AppDelegate.sharedWatchConnectivityService = WatchConnectivityService.shared
} else {
@@ -38,6 +44,30 @@ import WatchConnectivity
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func setupWidgetSyncChannel() {
let controller = window?.rootViewController as? FlutterViewController
let channel = FlutterMethodChannel(name: "dev.solsynth.solian/widget", binaryMessenger: controller!.binaryMessenger)
channel.setMethodCallHandler { [weak self] (call, result) in
if call.method == "syncToWidget" {
syncDefaultsToGroup()
WidgetCenter.shared.reloadAllTimelines()
result(true)
} else {
result(FlutterMethodNotImplemented)
}
}
}
override func applicationDidEnterBackground(_ application: UIApplication) {
syncDefaultsToGroup()
WidgetCenter.shared.reloadAllTimelines()
}
override func applicationWillTerminate(_ application: UIApplication) {
syncDefaultsToGroup()
}
}
final class WatchConnectivityService: NSObject, WCSessionDelegate {
@@ -51,8 +81,6 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
session.activate()
}
// MARK: - WCSessionDelegate
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print("[iOS] WCSession activation failed: \(error.localizedDescription)")
@@ -76,7 +104,7 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
let token = UserDefaults.standard.getFlutterToken()
let serverUrl = UserDefaults.standard.getServerUrl()
var data: [String: Any] = ["serverUrl": serverUrl ?? ""]
var data: [String: Any] = ["serverUrl": serverUrl]
if let token = token {
data["token"] = token
}
@@ -94,7 +122,7 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
let token = UserDefaults.standard.getFlutterToken()
let serverUrl = UserDefaults.standard.getServerUrl()
var data: [String: Any] = ["serverUrl": serverUrl ?? ""]
var data: [String: Any] = ["serverUrl": serverUrl]
if let token = token {
data["token"] = token
}

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

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<dict>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<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,9 +52,19 @@
<key>CLIENT_ID</key>
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false />
<false/>
<key>LSRequiresIPhoneOS</key>
<true />
<true/>
<key>NSAppIntentsConfiguration</key>
<dict>
<key>NSAppIntentsPackage</key>
<string>dev.solsynth.solian</string>
</dict>
<key>NSAppIntentsMetadata</key>
<dict>
<key>NSAppIntentsSupported</key>
<true/>
</dict>
<key>NSCalendarsUsageDescription</key>
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
<key>NSCameraUsageDescription</key>
@@ -62,12 +72,18 @@
<key>NSFaceIDUsageDescription</key>
<string>Allow the Solar Network verify your ownership of the logged in account and continue
your action quickly.</string>
<key>NSHealthShareUsageDescription</key>
<string>Allow us to share your fitness data with your friends.</string>
<key>NSHealthUpdateUsageDescription</key>
<string>Allow us to update your fitness data with your friends.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Solian uses speech recognition for Siri integration</string>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
@@ -78,7 +94,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,15 +107,13 @@
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false />
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>WKCompanionAppBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
@@ -107,5 +121,7 @@
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
</dict>
<key>WKCompanionAppBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</dict>
</plist>

View File

@@ -34,7 +34,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
}
let serverUrl = UserDefaults.standard.getServerUrl()
let url = "\(serverUrl)/sphere/chat/\(metadata["room_id"] ?? "")/messages"
let url = "\(serverUrl)/messager/chat/\(metadata["room_id"] ?? "")/messages"
let parameters: [String: Any?] = [
"content": textResponse.userText,

View File

@@ -15,6 +15,8 @@
</array>
<key>com.apple.developer.device-information.user-assigned-device-name</key>
<true/>
<key>com.apple.developer.healthkit</key>
<true/>
<key>com.apple.developer.usernotifications.communication</key>
<true/>
<key>com.apple.security.application-groups</key>

View File

@@ -0,0 +1,84 @@
//
// AppIntentConfiguration.swift
// Runner
//
// Created by LittleSheep on 2026/1/16.
//
import AppIntents
@available(iOS 16.0, *)
struct AppShortcuts: AppShortcutsProvider {
@AppShortcutsBuilder static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: OpenChatIntent(),
phrases: [
"Open chat with \(.applicationName)",
"Go to chat using \(.applicationName)",
"Show chat in \(.applicationName)"
]
)
AppShortcut(
intent: OpenPostIntent(),
phrases: [
"Open post with \(.applicationName)",
"Show post using \(.applicationName)"
]
)
AppShortcut(
intent: OpenComposeIntent(),
phrases: [
"Open compose with \(.applicationName)",
"New post using \(.applicationName)",
"Write post in \(.applicationName)"
]
)
AppShortcut(
intent: SearchContentIntent(),
phrases: [
"Search in \(.applicationName)",
"Find content using \(.applicationName)"
]
)
AppShortcut(
intent: CheckNotificationsIntent(),
phrases: [
"Check notifications with \(.applicationName)",
"Get notifications using \(.applicationName)",
"Do I have notifications in \(.applicationName)"
]
)
AppShortcut(
intent: SendMessageIntent(),
phrases: [
"Send message with \(.applicationName)",
"Post message using \(.applicationName)",
"Send text using \(.applicationName)"
]
)
AppShortcut(
intent: ReadMessagesIntent(),
phrases: [
"Read messages with \(.applicationName)",
"Get chat using \(.applicationName)",
"Show messages with \(.applicationName)"
]
)
AppShortcut(
intent: CheckUnreadChatsIntent(),
phrases: [
"Check unread chats with \(.applicationName)",
"Do I have messages using \(.applicationName)",
"Get unread messages with \(.applicationName)"
]
)
AppShortcut(
intent: MarkNotificationsReadIntent(),
phrases: [
"Mark notifications read with \(.applicationName)",
"Clear notifications using \(.applicationName)",
"Mark all read with \(.applicationName)"
]
)
}
}

View File

@@ -0,0 +1,296 @@
//
// AppIntentHandlers.swift
// Runner
//
// Created by LittleSheep on 2026/1/16.
//
import AppIntents
import UIKit
@available(iOS 16.0, *)
struct OpenChatIntent: AppIntent {
static var title: LocalizedStringResource = "intent_open_chat_title"
static var description = IntentDescription("intent_open_chat_desc")
static var isDiscoverable = true
static var openAppWhenRun = true
@Parameter(title: "Channel ID")
var channelId: String?
func perform() async throws -> some IntentResult & OpensIntent {
guard let channelId = channelId, !channelId.isEmpty else {
throw AppIntentError.requiredParameter("Channel ID")
}
DeepLinkHandler.shared.handle(url: URL(string: "solian://chat/\(channelId)")!)
return .result(value: "Opening chat \(channelId)")
}
}
@available(iOS 16.0, *)
struct OpenPostIntent: AppIntent {
static var title: LocalizedStringResource = "intent_open_post_title"
static var description = IntentDescription("intent_open_post_desc")
static var isDiscoverable = true
static var openAppWhenRun = true
@Parameter(title: "Post ID")
var postId: String?
func perform() async throws -> some IntentResult & OpensIntent {
guard let postId = postId, !postId.isEmpty else {
throw AppIntentError.requiredParameter("Post ID")
}
DeepLinkHandler.shared.handle(url: URL(string: "solian://posts/\(postId)")!)
return .result(value: "Opening post \(postId)")
}
}
@available(iOS 16.0, *)
struct OpenComposeIntent: AppIntent {
static var title: LocalizedStringResource = "intent_open_compose_title"
static var description = IntentDescription("intent_open_compose_desc")
static var isDiscoverable = true
static var openAppWhenRun = true
func perform() async throws -> some IntentResult & OpensIntent {
DeepLinkHandler.shared.handle(url: URL(string: "solian://compose")!)
return .result(value: "Opening compose screen")
}
}
@available(iOS 16.0, *)
struct ComposePostIntent: AppIntent {
static var title: LocalizedStringResource = "intent_compose_post_title"
static var description = IntentDescription("intent_compose_post_desc")
static var isDiscoverable = true
static var openAppWhenRun = true
func perform() async throws -> some IntentResult & OpensIntent {
DeepLinkHandler.shared.handle(url: URL(string: "solian://compose")!)
return .result(value: "Opening compose screen")
}
}
@available(iOS 16.0, *)
struct SearchContentIntent: AppIntent {
static var title: LocalizedStringResource = "intent_search_title"
static var description = IntentDescription("intent_search_desc")
static var isDiscoverable = true
static var openAppWhenRun = true
@Parameter(title: "Search Query")
var query: String?
func perform() async throws -> some IntentResult & OpensIntent {
guard let query = query, !query.isEmpty else {
throw AppIntentError.requiredParameter("Search Query")
}
let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query
DeepLinkHandler.shared.handle(url: URL(string: "solian://search?q=\(encodedQuery)")!)
return .result(value: "Searching for \"\(query)\"")
}
}
@available(iOS 16.0, *)
struct ViewNotificationsIntent: AppIntent {
static var title: LocalizedStringResource = "intent_notifications_title"
static var description = IntentDescription("intent_notifications_desc")
static var isDiscoverable = true
static var openAppWhenRun = true
func perform() async throws -> some IntentResult & OpensIntent {
DeepLinkHandler.shared.handle(url: URL(string: "solian://notifications")!)
return .result(value: "Opening notifications")
}
}
@available(iOS 16.0, *)
struct CheckNotificationsIntent: AppIntent {
static var title: LocalizedStringResource = "intent_check_notifications_title"
static var description = IntentDescription("intent_check_notifications_desc")
static var isDiscoverable = true
static var openAppWhenRun = false
func perform() async throws -> some IntentResult & ProvidesDialog {
do {
let count = try await NetworkService.shared.getNotificationCount()
let message: String
if count == 0 {
message = "You have no new notifications"
} else if count == 1 {
message = "You have 1 new notification"
} else {
message = "You have \(count) new notifications"
}
return .result(
value: message,
dialog: "\(message)"
)
} catch {
throw AppIntentError.networkError("Failed to check notifications: \(error.localizedDescription)")
}
}
}
@available(iOS 16.0, *)
struct SendMessageIntent: AppIntent {
static var title: LocalizedStringResource = "intent_send_message_title"
static var description = IntentDescription("intent_send_message_desc")
static var isDiscoverable = true
static var openAppWhenRun = false
@Parameter(title: "Channel ID")
var channelId: String?
@Parameter(title: "Message Content")
var content: String?
func perform() async throws -> some IntentResult & ProvidesDialog {
guard let channelId = channelId, !channelId.isEmpty else {
throw AppIntentError.requiredParameter("Channel ID")
}
guard let content = content, !content.isEmpty else {
throw AppIntentError.requiredParameter("Message Content")
}
do {
try await NetworkService.shared.sendMessage(channelId: channelId, content: content)
return .result(
value: "Message sent to channel \(channelId)",
dialog: "Message sent successfully"
)
} catch {
throw AppIntentError.networkError("Failed to send message: \(error.localizedDescription)")
}
}
}
@available(iOS 16.0, *)
struct ReadMessagesIntent: AppIntent {
static var title: LocalizedStringResource = "intent_read_messages_title"
static var description = IntentDescription("intent_read_messages_desc")
static var isDiscoverable = true
static var openAppWhenRun = false
@Parameter(title: "Channel ID")
var channelId: String?
@Parameter(title: "Number of Messages", default: "5")
var limit: String?
func perform() async throws -> some IntentResult & ProvidesDialog {
guard let channelId = channelId, !channelId.isEmpty else {
throw AppIntentError.requiredParameter("Channel ID")
}
let limitValue = Int(limit ?? "5") ?? 5
let safeLimit = max(1, min(20, limitValue))
do {
let messages = try await NetworkService.shared.getMessages(
channelId: channelId,
offset: 0,
take: safeLimit
)
if messages.isEmpty {
return .result(
value: "No messages found in channel \(channelId)",
dialog: "No messages found"
)
}
let formattedMessages = messages.compactMap { message -> String? in
let senderName = message.sender?.account?.name ?? "Unknown"
let content = message.content ?? ""
return "\(senderName): \(content)"
}.joined(separator: "\n")
return .result(
value: formattedMessages,
dialog: "Found \(messages.count) messages"
)
} catch {
throw AppIntentError.networkError("Failed to read messages: \(error.localizedDescription)")
}
}
}
@available(iOS 16.0, *)
struct CheckUnreadChatsIntent: AppIntent {
static var title: LocalizedStringResource = "intent_unread_chats_title"
static var description = IntentDescription("intent_unread_chats_desc")
static var isDiscoverable = true
static var openAppWhenRun = false
func perform() async throws -> some IntentResult & ProvidesDialog {
do {
let count = try await NetworkService.shared.getUnreadChatsCount()
let message: String
if count == 0 {
message = "You have no unread messages"
} else if count == 1 {
message = "You have 1 unread message"
} else {
message = "You have \(count) unread messages"
}
return .result(
value: message,
dialog: "\(message)"
)
} catch {
throw AppIntentError.networkError("Failed to check unread chats: \(error.localizedDescription)")
}
}
}
@available(iOS 16.0, *)
struct MarkNotificationsReadIntent: AppIntent {
static var title: LocalizedStringResource = "intent_mark_read_title"
static var description = IntentDescription("intent_mark_read_desc")
static var isDiscoverable = true
static var openAppWhenRun = false
func perform() async throws -> some IntentResult & ProvidesDialog {
do {
try await NetworkService.shared.markNotificationsRead()
return .result(
value: "All notifications marked as read",
dialog: "All notifications marked as read"
)
} catch {
throw AppIntentError.networkError("Failed to mark notifications: \(error.localizedDescription)")
}
}
}
enum AppIntentError: Error, CustomLocalizedStringResourceConvertible {
case requiredParameter(String)
case networkError(String)
var localizedStringResource: LocalizedStringResource {
switch self {
case .requiredParameter(let param):
return "\(param) is required"
case .networkError(let message):
return "Network error: \(message)"
}
}
}

View File

@@ -0,0 +1,103 @@
//
// DeepLinkHandler.swift
// Runner
//
// Created by LittleSheep on 2026/1/16.
//
import Foundation
import UIKit
final class DeepLinkHandler {
static let shared = DeepLinkHandler()
private init() {}
func handle(url: URL) -> Bool {
guard url.scheme == SharedConstants.urlScheme else {
return false
}
let host = url.host ?? ""
let path = url.path
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems
switch host {
case "chat":
if let channelId = url.pathComponents.count > 1 ? url.pathComponents[1] : nil {
openUrl("solian://chat/\(channelId)")
return true
}
case "posts":
if let postId = url.pathComponents.count > 1 ? url.pathComponents[1] : nil {
openUrl("solian://posts/\(postId)")
return true
}
case "search":
if let query = queryItems?.first(where: { $0.name == "query" })?.value {
let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query
openUrl("solian://search?q=\(encodedQuery)")
return true
}
case "notifications":
openUrl("solian://notifications")
return true
case "compose":
openUrl("solian://compose")
return true
default:
if path.hasPrefix("/chat/") {
let channelId = path.replacingOccurrences(of: "/chat/", with: "")
openUrl("solian://chat/\(channelId)")
return true
}
if path.hasPrefix("/posts/") {
let postId = path.replacingOccurrences(of: "/posts/", with: "")
openUrl("solian://posts/\(postId)")
return true
}
if path.hasPrefix("/search") {
let query = queryItems?.first(where: { $0.name == "q" })?.value ?? ""
let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query
openUrl("solian://search?q=\(encodedQuery)")
return true
}
if path == "/notifications" {
openUrl("solian://notifications")
return true
}
if path.hasPrefix("/compose") || path == "/compose" {
openUrl("solian://compose")
return true
}
if path.hasPrefix("/dashboard") {
openUrl("solian://dashboard")
return true
}
}
return false
}
private func openUrl(_ urlString: String) {
guard let url = URL(string: urlString) else {
print("[DeepLinkHandler] Invalid URL: \(urlString)")
return
}
DispatchQueue.main.async {
UIApplication.shared.open(url) { success in
if success {
print("[DeepLinkHandler] Opened URL: \(urlString)")
} else {
print("[DeepLinkHandler] Failed to open URL: \(urlString)")
}
}
}
}
}

View File

@@ -0,0 +1,41 @@
//
// GroupDefaultSync.swift
// Runner
//
// Created by LittleSheep on 2026/1/3.
//
import Foundation
private let flutterKeyPrefix = "flutter."
private let flutterKeysToSync: [String] = [
"dyn_user_tk",
"app_server_url"
]
func syncDefaultsToGroup() {
print("[iOS] syncDefaultsToGroup() called")
let standard = UserDefaults.standard
let shared = UserDefaults(suiteName: "group.solsynth.solian")
guard let shared else {
print("[iOS] App Group UserDefaults not available")
return
}
for key in flutterKeysToSync {
let prefixedKey = key.starts(with: flutterKeyPrefix) ? key : flutterKeyPrefix + key
if let value = standard.object(forKey: prefixedKey) {
print("[iOS] Syncing key to App Group: \(prefixedKey)")
shared.set(value, forKey: prefixedKey)
} else {
print("[iOS] Key \(prefixedKey) was not found in the app data, skipping...")
}
}
shared.synchronize()
print("[iOS] Sync completed")
}

View File

@@ -0,0 +1,177 @@
//
// NetworkService.swift
// Runner
//
// Created by LittleSheep on 2026/1/16.
//
import Foundation
final class NetworkService {
static let shared = NetworkService()
private let session: URLSession
private let decoder: JSONDecoder
private init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 10
config.timeoutIntervalForResource = 30
self.session = URLSession(configuration: config)
self.decoder = JSONDecoder()
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
}
private var baseUrl: String {
UserDefaults.shared.getServerUrl()
}
private var authHeaders: [String: String] {
var headers = [
"Accept": "application/json",
"Content-Type": "application/json"
]
if let token = UserDefaults.shared.getAuthToken() {
headers["Authorization"] = "AtField \(token)"
}
return headers
}
func getNotificationCount() async throws -> Int {
let url = try buildUrl(path: SharedConstants.API.notificationsCount)
let response: NotificationCountResponse = try await get(url: url)
return response.count
}
func markNotificationsRead() async throws {
let url = try buildUrl(path: SharedConstants.API.notificationsMarkRead)
let _: EmptyResponse = try await post(url: url)
}
func getUnreadChatsCount() async throws -> Int {
let url = try buildUrl(path: SharedConstants.API.unreadChats)
let response: UnreadChatsResponse = try await get(url: url)
return response.unreadCount
}
func getMessages(channelId: String, offset: Int = 0, take: Int = 5) async throws -> [MessageResponse] {
let path = String(format: SharedConstants.API.messages, channelId)
let url = try buildUrl(path: path, queryItems: [
URLQueryItem(name: "offset", value: String(offset)),
URLQueryItem(name: "take", value: String(take))
])
let response: MessagesResponse = try await get(url: url)
return response.messages
}
func sendMessage(channelId: String, content: String) async throws {
let path = String(format: SharedConstants.API.sendMessage, channelId)
let url = try buildUrl(path: path)
let body = SendMessageBody(content: content, nonce: generateNonce())
let _: EmptyResponse = try await post(url: url, body: body)
}
private func buildUrl(path: String, queryItems: [URLQueryItem]? = nil) throws -> URL {
var components = URLComponents(string: baseUrl + path)
if let queryItems = queryItems, !queryItems.isEmpty {
components?.queryItems = queryItems
}
guard let url = components?.url else {
throw NetworkError.invalidUrl
}
return url
}
private func get<T: Decodable>(url: URL) async throws -> T {
var request = URLRequest(url: url)
authHeaders.forEach { request.setValue($1, forHTTPHeaderField: $0) }
let (data, response) = try await session.data(for: request)
try validateResponse(response)
return try decoder.decode(T.self, from: data)
}
private func post<T: Decodable>(url: URL) async throws -> T {
var request = URLRequest(url: url)
request.httpMethod = "POST"
authHeaders.forEach { request.setValue($1, forHTTPHeaderField: $0) }
let (data, response) = try await session.data(for: request)
try validateResponse(response)
if T.self == EmptyResponse.self {
return EmptyResponse() as! T
}
return try decoder.decode(T.self, from: data)
}
private func post<T: Decodable, B: Encodable>(url: URL, body: B) async throws -> T {
var request = URLRequest(url: url)
request.httpMethod = "POST"
authHeaders.forEach { request.setValue($1, forHTTPHeaderField: $0) }
request.httpBody = try JSONEncoder().encode(body)
let (data, response) = try await session.data(for: request)
try validateResponse(response)
if T.self == EmptyResponse.self {
return EmptyResponse() as! T
}
return try decoder.decode(T.self, from: data)
}
private func validateResponse(_ response: URLResponse) throws {
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw NetworkError.httpError(statusCode: httpResponse.statusCode)
}
}
private func generateNonce() -> String {
"\(Date().timeIntervalSince1970)"
}
}
enum NetworkError: Error {
case invalidUrl
case invalidResponse
case httpError(statusCode: Int)
}
struct NotificationCountResponse: Decodable {
let count: Int
}
struct UnreadChatsResponse: Decodable {
let unreadCount: Int
}
struct MessagesResponse: Decodable {
let messages: [MessageResponse]
}
struct MessageResponse: Decodable {
let content: String?
let sender: SenderResponse?
struct SenderResponse: Decodable {
let account: AccountResponse?
struct AccountResponse: Decodable {
let name: String?
}
}
}
struct SendMessageBody: Encodable {
let content: String
let nonce: String
}
struct EmptyResponse: Decodable {}

View File

@@ -0,0 +1,45 @@
//
// SharedConstants.swift
// Runner
//
// Created by LittleSheep on 2026/1/16.
//
import Foundation
enum SharedConstants {
static let appGroupId = "group.solsynth.solian"
static let urlScheme = "solian"
static let serverUrlKey = "flutter.app_server_url"
static let tokenKey = "flutter.dyn_user_tk"
static let defaultServerUrl = "https://api.solian.app"
enum API {
static let notificationsCount = "/ring/notifications/count"
static let notificationsMarkRead = "/ring/notifications/all/read"
static let unreadChats = "/messager/chat/unread"
static let messages = "/messager/chat/%@/messages"
static let sendMessage = "/messager/chat/%@/messages"
}
}
extension UserDefaults {
static let shared: UserDefaults = {
UserDefaults(suiteName: SharedConstants.appGroupId) ?? UserDefaults.standard
}()
func getServerUrl() -> String {
string(forKey: SharedConstants.serverUrlKey) ?? SharedConstants.defaultServerUrl
}
func getAuthToken() -> String? {
guard let jsonString = string(forKey: SharedConstants.tokenKey),
let data = jsonString.data(using: .utf8),
let jsonObject = try? JSONSerialization.jsonObject(with: data),
let jsonDict = jsonObject as? [String: Any],
let token = jsonDict["token"] as? String else {
return nil
}
return token
}
}

View File

@@ -0,0 +1,33 @@
/* App Intents - Chat */
"intent_open_chat_title" = "Open Chat";
"intent_open_chat_desc" = "Open a specific chat room";
/* App Intents - Post */
"intent_open_post_title" = "Open Post";
"intent_open_post_desc" = "Open a specific post";
/* App Intents - Compose */
"intent_open_compose_title" = "Open Compose";
"intent_open_compose_desc" = "Open compose post screen";
"intent_compose_post_title" = "Compose Post";
"intent_compose_post_desc" = "Create a new post";
/* App Intents - Search */
"intent_search_title" = "Search Content";
"intent_search_desc" = "Search for content";
/* App Intents - Notifications */
"intent_notifications_title" = "View Notifications";
"intent_notifications_desc" = "View notifications";
"intent_check_notifications_title" = "Check Notifications";
"intent_check_notifications_desc" = "Check notification count";
/* App Intents - Messages */
"intent_send_message_title" = "Send Message";
"intent_send_message_desc" = "Send a message to a chat channel";
"intent_read_messages_title" = "Read Messages";
"intent_read_messages_desc" = "Read recent messages from a chat channel";
"intent_unread_chats_title" = "Check Unread Chats";
"intent_unread_chats_desc" = "Check number of unread chat messages";
"intent_mark_read_title" = "Mark Notifications Read";
"intent_mark_read_desc" = "Mark all notifications as read";

View File

@@ -0,0 +1,33 @@
/* App Intents - Chat */
"intent_open_chat_title" = "Abrir chat";
"intent_open_chat_desc" = "Abrir sala de chat específica";
/* App Intents - Post */
"intent_open_post_title" = "Abrir publicación";
"intent_open_post_desc" = "Abrir publicación específica";
/* App Intents - Compose */
"intent_open_compose_title" = "Abrir compose";
"intent_open_compose_desc" = "Abrir pantalla de publicación";
"intent_compose_post_title" = "Redactar publicación";
"intent_compose_post_desc" = "Crear nueva publicación";
/* App Intents - Search */
"intent_search_title" = "Buscar contenido";
"intent_search_desc" = "Buscar contenido";
/* App Intents - Notifications */
"intent_notifications_title" = "Ver notificaciones";
"intent_notifications_desc" = "Ver notificaciones";
"intent_check_notifications_title" = "Ver notificaciones";
"intent_check_notifications_desc" = "Ver número de notificaciones";
/* App Intents - Messages */
"intent_send_message_title" = "Enviar mensaje";
"intent_send_message_desc" = "Enviar mensaje a canal de chat";
"intent_read_messages_title" = "Leer mensajes";
"intent_read_messages_desc" = "Leer mensajes recientes de un canal de chat";
"intent_unread_chats_title" = "Ver chats no leídos";
"intent_unread_chats_desc" = "Ver mensajes no leídos";
"intent_mark_read_title" = "Marcar notificaciones leídas";
"intent_mark_read_desc" = "Marcar todas como leídas";

View File

@@ -0,0 +1,33 @@
/* App Intents - Chat */
"intent_open_chat_title" = "チャットを開く";
"intent_open_chat_desc" = "特定のチャットルームを開く";
/* App Intents - Post */
"intent_open_post_title" = "投稿を開く";
"intent_open_post_desc" = "特定の投稿を開く";
/* App Intents - Compose */
"intent_open_compose_title" = "作成を開く";
"intent_open_compose_desc" = "投稿作成画面を開く";
"intent_compose_post_title" = "投稿を作成する";
"intent_compose_post_desc" = "新規投稿を作成する";
/* App Intents - Search */
"intent_search_title" = "検索";
"intent_search_desc" = "コンテンツを検索";
/* App Intents - Notifications */
"intent_notifications_title" = "通知を見る";
"intent_notifications_desc" = "通知を見る";
"intent_check_notifications_title" = "通知を確認";
"intent_check_notifications_desc" = "通知数を確認";
/* App Intents - Messages */
"intent_send_message_title" = "メッセージを送信";
"intent_send_message_desc" = "チャットチャンネルにメッセージを送信";
"intent_read_messages_title" = "メッセージを読む";
"intent_read_messages_desc" = "チャットチャンネルから最近のメッセージを読む";
"intent_unread_chats_title" = "未読チャットを確認";
"intent_unread_chats_desc" = "未読チャットメッセージ数を確認";
"intent_mark_read_title" = "通知を既読にする";
"intent_mark_read_desc" = "すべての通知を既読にする";

View File

@@ -0,0 +1,33 @@
/* App Intents - Chat */
"intent_open_chat_title" = "채팅 열기";
"intent_open_chat_desc" = "특정 채팅방 열기";
/* App Intents - Post */
"intent_open_post_title" = "게시물 열기";
"intent_open_post_desc" = "특정 게시물 열기";
/* App Intents - Compose */
"intent_open_compose_title" = "글쓰기 열기";
"intent_open_compose_desc" = "글쓰기 화면 열기";
"intent_compose_post_title" = "게시물 작성";
"intent_compose_post_desc" = "새 게시물 작성";
/* App Intents - Search */
"intent_search_title" = "검색";
"intent_search_desc" = "콘텐츠 검색";
/* App Intents - Notifications */
"intent_notifications_title" = "알림 보기";
"intent_notifications_desc" = "알림 보기";
"intent_check_notifications_title" = "알림 확인";
"intent_check_notifications_desc" = "알림 수 확인";
/* App Intents - Messages */
"intent_send_message_title" = "메시지 보내기";
"intent_send_message_desc" = "채팅 채널에 메시지 보내기";
"intent_read_messages_title" = "메시지 읽기";
"intent_read_messages_desc" = "채팅 채널에서 최근 메시지 읽기";
"intent_unread_chats_title" = "읽지 않은 채팅 확인";
"intent_unread_chats_desc" = "읽지 않은 채팅 메시지 수 확인";
"intent_mark_read_title" = "알림 읽음 처리";
"intent_mark_read_desc" = "모든 알림을 읽음으로 처리";

View File

@@ -0,0 +1,33 @@
/* App Intents - Chat */
"intent_open_chat_title" = "打开聊天";
"intent_open_chat_desc" = "打开特定聊天室";
/* App Intents - Post */
"intent_open_post_title" = "打开帖子";
"intent_open_post_desc" = "打开特定帖子";
/* App Intents - Compose */
"intent_open_compose_title" = "打开撰写";
"intent_open_compose_desc" = "打开撰写帖子页面";
"intent_compose_post_title" = "撰写帖子";
"intent_compose_post_desc" = "创建新帖子";
/* App Intents - Search */
"intent_search_title" = "搜索内容";
"intent_search_desc" = "搜索内容";
/* App Intents - Notifications */
"intent_notifications_title" = "查看通知";
"intent_notifications_desc" = "查看通知";
"intent_check_notifications_title" = "检查通知";
"intent_check_notifications_desc" = "检查通知数量";
/* App Intents - Messages */
"intent_send_message_title" = "发送消息";
"intent_send_message_desc" = "发送消息到聊天频道";
"intent_read_messages_title" = "读取消息";
"intent_read_messages_desc" = "从聊天频道读取最近消息";
"intent_unread_chats_title" = "检查未读聊天";
"intent_unread_chats_desc" = "检查未读聊天消息数量";
"intent_mark_read_title" = "标记通知已读";
"intent_mark_read_desc" = "将所有通知标记为已读";

View File

@@ -0,0 +1,33 @@
/* App Intents - Chat */
"intent_open_chat_title" = "開啟聊天";
"intent_open_chat_desc" = "開啟特定聊天室";
/* App Intents - Post */
"intent_open_post_title" = "開啟貼文";
"intent_open_post_desc" = "開啟特定貼文";
/* App Intents - Compose */
"intent_open_compose_title" = "開啟撰寫";
"intent_open_compose_desc" = "開啟撰寫貼文頁面";
"intent_compose_post_title" = "撰寫貼文";
"intent_compose_post_desc" = "建立新貼文";
/* App Intents - Search */
"intent_search_title" = "搜尋內容";
"intent_search_desc" = "搜尋內容";
/* App Intents - Notifications */
"intent_notifications_title" = "查看通知";
"intent_notifications_desc" = "查看通知";
"intent_check_notifications_title" = "檢查通知";
"intent_check_notifications_desc" = "檢查通知數量";
/* App Intents - Messages */
"intent_send_message_title" = "傳送訊息";
"intent_send_message_desc" = "傳送訊息到聊天頻道";
"intent_read_messages_title" = "讀取訊息";
"intent_read_messages_desc" = "從聊天頻道讀取最近訊息";
"intent_unread_chats_title" = "檢查未讀聊天";
"intent_unread_chats_desc" = "檢查未讀聊天訊息數量";
"intent_mark_read_title" = "標記通知已讀";
"intent_mark_read_desc" = "將所有通知標記為已讀";

BIN
ios/SfxMessage.caf Normal file

Binary file not shown.

BIN
ios/SfxNotification.caf Normal file

Binary file not shown.

View File

@@ -261,7 +261,7 @@ class NetworkService {
guard let baseURL = URL(string: serverUrl) else {
throw URLError(.badURL)
}
let url = baseURL.appendingPathComponent("/sphere/chat")
let url = baseURL.appendingPathComponent("/messager/chat")
var request = URLRequest(url: url)
request.httpMethod = "GET"
@@ -283,7 +283,7 @@ class NetworkService {
guard let baseURL = URL(string: serverUrl) else {
throw URLError(.badURL)
}
let url = baseURL.appendingPathComponent("/sphere/chat/\(identifier)")
let url = baseURL.appendingPathComponent("/messager/chat/\(identifier)")
var request = URLRequest(url: url)
request.httpMethod = "GET"
@@ -308,7 +308,7 @@ class NetworkService {
guard let baseURL = URL(string: serverUrl) else {
throw URLError(.badURL)
}
let url = baseURL.appendingPathComponent("/sphere/chat/invites")
let url = baseURL.appendingPathComponent("/messager/chat/invites")
var request = URLRequest(url: url)
request.httpMethod = "GET"
@@ -330,7 +330,7 @@ class NetworkService {
guard let baseURL = URL(string: serverUrl) else {
throw URLError(.badURL)
}
let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/accept")
let url = baseURL.appendingPathComponent("/messager/chat/invites/\(chatRoomId)/accept")
var request = URLRequest(url: url)
request.httpMethod = "POST"
@@ -351,7 +351,7 @@ class NetworkService {
guard let baseURL = URL(string: serverUrl) else {
throw URLError(.badURL)
}
let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/decline")
let url = baseURL.appendingPathComponent("/messager/chat/invites/\(chatRoomId)/decline")
var request = URLRequest(url: url)
request.httpMethod = "POST"
@@ -375,9 +375,9 @@ class NetworkService {
throw URLError(.badURL)
}
// Try a different pattern: /sphere/chat/messages with roomId as query param
// Try a different pattern: /messager/chat/messages with roomId as query param
var components = URLComponents(
url: baseURL.appendingPathComponent("/sphere/chat/\(chatRoomId)/messages"),
url: baseURL.appendingPathComponent("/messager/chat/\(chatRoomId)/messages"),
resolvingAgainstBaseURL: false
)!
var queryItems = [

View File

@@ -455,7 +455,7 @@ struct ChatRoomView: View {
]
// Create the URL
guard let url = URL(string: "\(serverUrl)/sphere/chat/\(room.id)/messages") else {
guard let url = URL(string: "\(serverUrl)/messager/chat/\(room.id)/messages") else {
throw URLError(.badURL)
}

View File

@@ -5,7 +5,7 @@
// Created by LittleSheep on 2025/5/31.
//
import UserNotifications
@preconcurrency import UserNotifications
import Intents
import Kingfisher
import UniformTypeIdentifiers
@@ -53,8 +53,10 @@ class NotificationService: UNNotificationServiceExtension {
private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws {
switch content.userInfo["type"] as? String {
case "messages.new":
content.sound = UNNotificationSound(named: UNNotificationSoundName("SfxMessage.caf"))
try handleMessagingNotification(request: request, content: content)
default:
content.sound = UNNotificationSound(named: UNNotificationSoundName("SfxNotification.caf"))
try handleDefaultNotification(content: content)
}
}
@@ -149,7 +151,7 @@ class NotificationService: UNNotificationServiceExtension {
let dispatchGroup = DispatchGroup()
var attachments: [UNNotificationAttachment] = []
let lock = NSLock() // To synchronize access to the attachments array
let queue = DispatchQueue(label: "dev.solsynth.solian.nse")
for attachmentUrl in attachmentUrls {
guard let remoteUrl = URL(string: attachmentUrl) else {
@@ -163,7 +165,7 @@ class NotificationService: UNNotificationServiceExtension {
.processor(scaleProcessor)
] : nil) { [weak self] result in
defer { dispatchGroup.leave() }
guard let self = self else { return }
guard self != nil else { return }
switch result {
case .success(let retrievalResult):
@@ -176,12 +178,12 @@ class NotificationService: UNNotificationServiceExtension {
try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
if let attachment = try? UNNotificationAttachment(identifier: attachmentUrl, url: cachedFileUrl, options: [
UNNotificationAttachmentOptionsTypeHintKey: type?.identifier as Any,
UNNotificationAttachmentOptionsTypeHintKey: UTType.png.identifier,
UNNotificationAttachmentOptionsThumbnailHiddenKey: 0,
]) {
lock.lock()
queue.async {
attachments.append(attachment)
lock.unlock()
}
}
} catch {
print("Failed to write media to temporary file: \(error.localizedDescription)")

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"images" : [
{
"filename" : "icon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "icon-dark.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"filename" : "icon 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "icon-dark.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,720 @@
//
// SolianWidgetExtension.swift
// SolianWidgetExtension
//
// Created by LittleSheep on 2026/1/3.
//
import WidgetKit
import SwiftUI
struct CheckInTip: Codable {
let isPositive: Bool
let title: String
let content: String
enum CodingKeys: String, CodingKey {
case isPositive = "is_positive"
case title
case content
}
}
struct CheckInAccount: Codable {
let id: String
let nick: String?
let profile: CheckInProfile?
}
struct CheckInProfile: Codable {
let picture: String?
}
struct CheckInResult: Codable {
let id: String
let level: Int
let rewardPoints: Int
let rewardExperience: Int
let tips: [CheckInTip]
let accountId: String
let account: CheckInAccount?
let createdAt: String
let updatedAt: String
let deletedAt: String?
enum CodingKeys: String, CodingKey {
case id
case level
case rewardPoints = "reward_points"
case rewardExperience = "reward_experience"
case tips
case accountId = "account_id"
case account
case createdAt = "created_at"
case updatedAt = "updated_at"
case deletedAt = "deleted_at"
}
var createdDate: Date? {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = formatter.date(from: createdAt) {
return date
}
// Fallback for timestamps without fractional seconds
formatter.formatOptions = [.withInternetDateTime]
return formatter.date(from: createdAt)
}
}
struct NotableDay: Codable {
let date: String
let localName: String
let globalName: String
let countryCode: String?
let localizableKey: String?
let holidays: [Int]?
enum CodingKeys: String, CodingKey {
case date
case localName = "local_name"
case globalName = "global_name"
case countryCode = "country_code"
case localizableKey = "localizable_key"
case holidays
}
var notableDate: Date? {
ISO8601DateFormatter().date(from: date)
}
var isToday: Bool {
guard let notableDate = notableDate else { return false }
let calendar = Calendar.current
return calendar.isDateInToday(notableDate)
}
}
class CheckInService {
private let networkService = WidgetNetworkService()
func fetchCheckInResult() async throws -> CheckInResult? {
return try await networkService.makeRequest(path: "/pass/accounts/me/check-in")
}
}
class NotableDayService {
private let networkService = WidgetNetworkService()
func fetchRecentNotableDay() async throws -> NotableDay? {
print("[WidgetKit] [NotableDayService] Fetching recent notable day...")
do {
let result: [NotableDay]? = try await networkService.makeRequest(path: "/pass/notable/me/recent")
print("[WidgetKit] [NotableDayService] Result: \(String(describing: result))")
guard let result = result else {
print("[WidgetKit] [NotableDayService] Result is nil")
return nil
}
print("[WidgetKit] [NotableDayService] Result count: \(result.count)")
guard result.isEmpty == false else {
print("[WidgetKit] [NotableDayService] No notable days found")
return nil
}
let firstDay = result.first!
print("[WidgetKit] [NotableDayService] First notable day: \(firstDay.localName), date: \(firstDay.date)")
return firstDay
} catch let decodingError as DecodingError {
print("[WidgetKit] [NotableDayService] Decoding error, trying as single object...")
print("[WidgetKit] [NotableDayService] Error: \(decodingError.localizedDescription)")
switch decodingError {
case .typeMismatch(let type, let context):
print("[WidgetKit] [NotableDayService] Type mismatch: expected \(type), context: \(context.debugDescription)")
case .valueNotFound(let type, let context):
print("[WidgetKit] [NotableDayService] Value not found: type \(type), context: \(context.debugDescription)")
case .keyNotFound(let key, let context):
print("[WidgetKit] [NotableDayService] Key not found: \(key), context: \(context.debugDescription)")
case .dataCorrupted(let context):
print("[WidgetKit] [NotableDayService] Data corrupted: \(context.debugDescription)")
@unknown default:
print("[WidgetKit] [NotableDayService] Unknown decoding error")
}
do {
let singleResult: NotableDay? = try await networkService.makeRequest(path: "/pass/notable/me/recent")
print("[WidgetKit] [NotableDayService] Single object decode succeeded: \(singleResult?.localName ?? "nil")")
return singleResult
} catch {
print("[WidgetKit] [NotableDayService] Single object decode also failed: \(error.localizedDescription)")
throw decodingError
}
} catch {
print("[WidgetKit] [NotableDayService] Error fetching notable day: \(error.localizedDescription)")
print("[WidgetKit] [NotableDayService] Error type: \(type(of: error))")
throw error
}
}
}
struct CheckInEntry: TimelineEntry {
let date: Date
let result: CheckInResult?
let notableDay: NotableDay?
let error: String?
let isLoading: Bool
static func placeholder() -> CheckInEntry {
CheckInEntry(date: Date(), result: nil, notableDay: nil, error: nil, isLoading: true)
}
}
struct Provider: TimelineProvider {
private let apiService = CheckInService()
func placeholder(in context: Context) -> CheckInEntry {
CheckInEntry.placeholder()
}
func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) {
Task {
print("[WidgetKit] [Provider] Getting snapshot...")
async let checkInResult = try? await apiService.fetchCheckInResult()
async let notableDay = try? await NotableDayService().fetchRecentNotableDay()
let result = try? await checkInResult
let day = try? await notableDay
print("[WidgetKit] [Provider] Snapshot - CheckIn: \(result != nil ? "Found" : "Not found"), NotableDay: \(day != nil ? "Found" : "Not found")")
let entry = CheckInEntry(date: Date(), result: result, notableDay: day, error: nil, isLoading: false)
completion(entry)
}
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
Task {
let currentDate = Date()
print("[WidgetKit] [Provider] Getting timeline at \(currentDate)...")
do {
async let checkInResult = try await apiService.fetchCheckInResult()
async let notableDay = try await NotableDayService().fetchRecentNotableDay()
let result = try await checkInResult
let day = try await notableDay
print("[WidgetKit] [Provider] Timeline - CheckIn: \(result != nil ? "Found" : "Not found"), NotableDay: \(day != nil ? "Found" : "Not found")")
let entry = CheckInEntry(date: currentDate, result: result, notableDay: day, error: nil, isLoading: false)
let nextUpdateDate: Date
if let result = result, let createdDate = result.createdDate {
let calendar = Calendar.current
if let tomorrow = calendar.date(byAdding: .day, value: 1, to: createdDate) {
nextUpdateDate = min(tomorrow, calendar.date(byAdding: .hour, value: 1, to: currentDate)!)
} else {
nextUpdateDate = calendar.date(byAdding: .hour, value: 1, to: currentDate)!
}
} else {
nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
}
print("[WidgetKit] [Provider] Next update at: \(nextUpdateDate)")
let timeline = Timeline(entries: [entry], policy: .after(nextUpdateDate))
completion(timeline)
} catch {
print("[WidgetKit] [Provider] Error in getTimeline: \(error.localizedDescription)")
let entry = CheckInEntry(date: currentDate, result: nil, notableDay: nil, error: error.localizedDescription, isLoading: false)
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 10, to: currentDate)!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
}
}
struct CheckInWidgetEntryView: View {
var entry: Provider.Entry
@Environment(\.widgetFamily) var family
var body: some View {
if let result = entry.result {
CheckedInView(result: result, notableDay: entry.notableDay)
} else if entry.isLoading {
LoadingView()
} else if let error = entry.error {
ErrorView(error: error)
} else {
NotCheckedInView(notableDay: entry.notableDay)
}
}
private func getLevelName(for level: Int) -> String {
let key = "checkInResultT\(level)"
return NSLocalizedString(key, comment: "Check-in result level name")
}
@ViewBuilder
private func NotableDayView(notableDay: NotableDay) -> some View {
VStack(alignment: .leading, spacing: 4) {
if !notableDay.isToday {
Text(NSLocalizedString("notableDayUpcoming", comment: "Upcoming"))
.font(.caption2)
.foregroundStyle(.secondary)
}
HStack(spacing: isAccessory ? 8 : 6) {
Image(systemName: "sparkles")
.foregroundColor(.orange)
.font(isAccessory ? .caption : .subheadline)
VStack(alignment: .leading, spacing: 2) {
if notableDay.isToday {
Text(String(format: NSLocalizedString("notableDayToday", comment: "{name} is today!"), notableDay.localName))
.font(isAccessory ? .caption : .footnote)
.fontWeight(.bold)
.foregroundColor(.primary)
.lineLimit(2)
} else {
if let notableDate = notableDay.notableDate {
let dateString = isCompact ? formatDateCompact(notableDate) : formatDateRegular(notableDate)
Text(String(format: NSLocalizedString("notableDayIs", comment: "{date} is {name}"), dateString, notableDay.localName))
.font(isAccessory ? .caption : .footnote)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(2)
} else {
Text(notableDay.localName)
.font(isAccessory ? .caption : .footnote)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(2)
}
}
}
Spacer()
}
if notableDay.isToday && !isAccessory {
HStack(spacing: 4) {
Image(systemName: "star.fill")
.font(.caption2)
.foregroundColor(.orange)
Text(NSLocalizedString("today", comment: "Today"))
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
}
private var isCompact: Bool {
family == .systemSmall || isAccessory
}
private func formatDateCompact(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "M/d"
return formatter.string(from: date)
}
private func formatDateRegular(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "MMM d, yyyy"
return formatter.string(from: date)
}
@ViewBuilder
private func CheckedInView(result: CheckInResult, notableDay: NotableDay?) -> some View {
Link(destination: URL(string: "solian://dashboard")!) {
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
HStack(spacing: 8) {
Image(systemName: "flame.fill")
.foregroundColor(.orange)
.font(isAccessory ? .caption : .title3)
Text(getLevelName(for: result.level))
.font(isAccessory ? .caption2 : .headline)
.fontWeight(.bold)
Spacer()
}
if !result.tips.isEmpty {
if isAccessory {
let positiveTips = result.tips.filter { $0.isPositive }
let negativeTips = result.tips.filter { !$0.isPositive }
HStack(spacing: 2) {
if let positiveTip = positiveTips.first {
HStack(spacing: 8) {
Image(systemName: "hand.thumbsup.fill")
.font(.caption2)
.foregroundColor(.secondary)
Text(positiveTip.title)
.font(.caption2)
.foregroundColor(.primary)
.lineLimit(1)
}
}
if let negativeTip = negativeTips.first {
HStack(spacing: 8) {
Image(systemName: "hand.thumbsdown.fill")
.font(.caption2)
.foregroundColor(.secondary)
Text(negativeTip.title)
.font(.caption2)
.foregroundColor(.primary)
.lineLimit(1)
}
}
Spacer()
}
} else if family == .systemSmall {
let positiveTips = result.tips.filter { $0.isPositive }
let negativeTips = result.tips.filter { !$0.isPositive }
VStack(alignment: .leading, spacing: 4) {
if let positiveTip = positiveTips.first {
HStack(spacing: 4) {
Image(systemName: "hand.thumbsup.fill")
.font(.caption)
.foregroundColor(.green.opacity(0.8))
Text(positiveTip.title)
.font(.caption)
.foregroundColor(.primary)
Spacer()
}
}
if let negativeTip = negativeTips.first {
HStack(spacing: 4) {
Image(systemName: "hand.thumbsdown.fill")
.font(.caption)
.foregroundColor(.red.opacity(0.8))
Text(negativeTip.title)
.font(.caption)
.foregroundColor(.primary)
Spacer()
}
}
}
} else {
let positiveTips = result.tips.filter { $0.isPositive }
let negativeTips = result.tips.filter { !$0.isPositive }
VStack(alignment: .leading, spacing: 4) {
if !positiveTips.isEmpty {
HStack(spacing: 6) {
Image(systemName: "hand.thumbsup.fill")
.font(.caption)
.foregroundColor(.green.opacity(0.8))
ForEach(Array(positiveTips.prefix(3)), id: \.title) { tip in
Text(tip.title)
.font(.caption)
.foregroundColor(.primary)
if tip.title != positiveTips.last?.title {
Text("")
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
}
}
if !negativeTips.isEmpty {
HStack(spacing: 6) {
Image(systemName: "hand.thumbsdown.fill")
.font(.caption)
.foregroundColor(.red.opacity(0.8))
ForEach(Array(negativeTips.prefix(3)), id: \.title) { tip in
Text(tip.title)
.font(.caption)
.foregroundColor(.primary)
if tip.title != negativeTips.last?.title {
Text("")
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
}
}
}
}
} else if !isAccessory && family != .systemSmall {
Text("No fortune today")
.font(.caption)
.foregroundColor(.secondary)
}
if let notableDay = notableDay {
NotableDayView(notableDay: notableDay)
}
if family == .systemLarge {
Spacer()
WidgetFooter()
}
}
.padding(isAccessory ? 0 : (family == .systemSmall ? 6 : 12))
}
}
@ViewBuilder
private func WidgetFooter() -> some View {
HStack {
Text("Solian")
.font(.caption2)
.fontWeight(.medium)
.foregroundColor(.secondary)
Spacer()
}
}
private var isAccessory: Bool {
if #available(iOS 16.0, *) {
if case .accessoryRectangular = family {
return true
}
}
return false
}
@ViewBuilder
private func NotCheckedInView(notableDay: NotableDay?) -> some View {
Link(destination: URL(string: "solian://dashboard")!) {
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
HStack(spacing: 8) {
Image(systemName: "flame.fill")
.foregroundColor(.secondary)
.font(isAccessory ? .caption : .title3)
Text(NSLocalizedString("checkIn", comment: "Check In"))
.font(isAccessory ? .caption2 : .headline)
.fontWeight(.bold)
Spacer()
}
if !isAccessory {
Text(NSLocalizedString("tapToCheckIn", comment: "Tap to check in today"))
.font(.caption)
.foregroundColor(.secondary)
if let notableDay = notableDay {
NotableDayView(notableDay: notableDay)
}
Spacer()
WidgetFooter()
} else if let notableDay = notableDay {
NotableDayView(notableDay: notableDay)
}
}
.padding(isAccessory ? 0 : (family == .systemSmall ? 6 : 12))
}
}
@ViewBuilder
private func LoadingView() -> some View {
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
HStack(spacing: 4) {
ProgressView()
.scaleEffect(isAccessory ? 0.6 : 0.8)
Text(NSLocalizedString("loading", comment: "Loading..."))
.font(isAccessory ? .caption2 : .caption)
.foregroundColor(.secondary)
Spacer()
}
if !isAccessory {
Spacer()
WidgetFooter()
}
}
.padding(isAccessory ? 0 : 12)
}
@ViewBuilder
private func ErrorView(error: String) -> some View {
Link(destination: URL(string: "solian://dashboard")!) {
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
HStack(spacing: 4) {
Image(systemName: "exclamationmark.triangle")
.foregroundColor(.secondary)
.font(isAccessory ? .caption : .title3)
Text(NSLocalizedString("error", comment: "Error"))
.font(isAccessory ? .caption2 : .headline)
Spacer()
}
if !isAccessory {
Text(NSLocalizedString("openAppToRefresh", comment: "Open app to refresh"))
.font(.caption)
.foregroundColor(.secondary)
Text(error)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(nil)
.multilineTextAlignment(.leading)
Spacer()
WidgetFooter()
}
}
.padding(isAccessory ? 0 : (family == .systemSmall ? 6 : 12))
}
}
}
struct CheckInWidgetRootView: View {
var entry: Provider.Entry
@Environment(\.colorScheme) var colorScheme
var body: some View {
if #available(iOS 17.0, *) {
ZStack {
CheckInWidgetEntryView(entry: entry)
if entry.result != nil || entry.notableDay != nil {
GeometryReader { geometry in
Image(colorScheme == .dark ? "CloudyLambDark" : "CloudyLamb")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(
width: geometry.size.width * 0.9,
height: geometry.size.width * 0.9
)
.opacity(0.18)
.mask(
LinearGradient(
gradient: Gradient(colors: [
Color.white,
Color.white,
Color.clear
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.position(
x: geometry.size.width * 0.9,
y: 20
)
}
.allowsHitTesting(false)
}
}
.containerBackground(.fill.tertiary, for: .widget)
.padding(.vertical, 8)
} else {
CheckInWidgetEntryView(entry: entry)
.padding()
.background()
}
}
}
struct SolianCheckInWidget: Widget {
let kind: String = "SolianCheckInWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
CheckInWidgetRootView(entry: entry)
}
.configurationDisplayName("Check In")
.description("View your daily check-in status")
.supportedFamilies(supportedFamilies)
}
private var supportedFamilies: [WidgetFamily] {
#if os(iOS)
return [.systemSmall, .systemMedium, .systemLarge, .accessoryRectangular]
#else
return [.systemSmall, .systemMedium, .systemLarge]
#endif
}
}
#Preview(as: .systemSmall) {
SolianCheckInWidget()
} timeline: {
CheckInEntry(date: .now, result: nil, notableDay: nil, error: nil, isLoading: false)
}
#Preview(as: .systemMedium) {
SolianCheckInWidget()
} timeline: {
CheckInEntry(
date: .now,
result: CheckInResult(
id: "test-id",
level: 2,
rewardPoints: 10,
rewardExperience: 100,
tips: [
CheckInTip(isPositive: true, title: "Good Luck", content: "Great day"),
CheckInTip(isPositive: true, title: "Creative", content: "Inspiration"),
CheckInTip(isPositive: false, title: "Shopping", content: "Expensive")
],
accountId: "account-id",
account: nil,
createdAt: ISO8601DateFormatter().string(from: Date()),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
),
notableDay: NotableDay(
date: ISO8601DateFormatter().string(from: Calendar.current.date(byAdding: .day, value: 5, to: Date())!),
localName: "Christmas",
globalName: "Christmas",
countryCode: nil,
localizableKey: nil,
holidays: []
),
error: nil,
isLoading: false
)
}
#if os(iOS)
#Preview(as: .accessoryRectangular) {
SolianCheckInWidget()
} timeline: {
CheckInEntry(
date: .now,
result: CheckInResult(
id: "test-id",
level: 4,
rewardPoints: 50,
rewardExperience: 500,
tips: [
CheckInTip(isPositive: true, title: "Lucky", content: "Great fortune"),
CheckInTip(isPositive: true, title: "Success", content: "Opportunity")
],
accountId: "account-id",
account: nil,
createdAt: ISO8601DateFormatter().string(from: Date()),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
),
notableDay: NotableDay(
date: ISO8601DateFormatter().string(from: Date()),
localName: "New Year",
globalName: "New Year",
countryCode: nil,
localizableKey: nil,
holidays: []
),
error: nil,
isLoading: false
)
}
#endif

View File

@@ -0,0 +1,937 @@
//
// SolianNotificationWidget.swift
// Runner
//
// Created by LittleSheep on 2026/1/4.
//
import Foundation
import WidgetKit
import SwiftUI
// MARK: - Notification Widget
struct NotificationMeta: Codable {
let pfp: String?
let images: [String]?
let actionUri: String?
enum CodingKeys: String, CodingKey {
case pfp
case images
case actionUri = "action_uri"
}
}
struct SnNotification: Codable, Identifiable {
let id: String
let topic: String
let title: String
let subtitle: String
let content: String
let meta: NotificationMeta?
let priority: Int
let viewedAt: String?
let accountId: String
let createdAt: String
let updatedAt: String
let deletedAt: String?
enum CodingKeys: String, CodingKey {
case id
case topic
case title
case subtitle
case content
case meta
case priority
case viewedAt = "viewed_at"
case accountId = "account_id"
case createdAt = "created_at"
case updatedAt = "updated_at"
case deletedAt = "deleted_at"
}
var createdDate: Date? {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = formatter.date(from: createdAt) {
return date
}
// Fallback for timestamps without fractional seconds
formatter.formatOptions = [.withInternetDateTime]
return formatter.date(from: createdAt)
}
var isUnread: Bool {
return viewedAt == nil
}
func getTopicIcon() -> String {
switch topic {
case "post.replies":
return "arrow.uturn.backward"
case "wallet.transactions":
return "wallet.pass"
case "relationships.friends.request":
return "person.badge.plus"
case "invites.chat":
return "message.badge"
case "invites.realm":
return "globe"
case "auth.login":
return "arrow.right.square"
case "posts.new":
return "doc.badge.plus"
case "wallet.orders.paid":
return "baggage"
case "posts.reactions.new":
return "face.smiling"
default:
return "bell"
}
}
}
struct NotificationEntry: TimelineEntry {
let date: Date
let notifications: [SnNotification]?
let unreadCount: Int
let error: String?
let isLoading: Bool
static func placeholder() -> NotificationEntry {
NotificationEntry(date: Date(), notifications: nil, unreadCount: 0, error: nil, isLoading: true)
}
}
class NotificationService {
private let networkService = WidgetNetworkService()
private lazy var session: URLSession = {
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 10.0
configuration.timeoutIntervalForResource = 10.0
configuration.waitsForConnectivity = false
return URLSession(configuration: configuration)
}()
func fetchRecentNotifications(take: Int = 5) async throws -> [SnNotification] {
if take == 0 {
return []
}
guard let token = networkService.token else {
throw RemoteError.missingCredentials
}
let baseURL = networkService.baseURL
guard let url = URL(string: "\(baseURL)/ring/notifications?unmark=true&take=\(take)") else {
throw RemoteError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.timeoutInterval = 10.0
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw RemoteError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
let decoder = JSONDecoder()
let notifications = try decoder.decode([SnNotification].self, from: data)
return notifications
case 404:
return []
default:
throw RemoteError.httpError(httpResponse.statusCode)
}
}
func fetchUnreadCount() async throws -> Int {
guard let token = networkService.token else {
throw RemoteError.missingCredentials
}
let baseURL = networkService.baseURL
guard let url = URL(string: "\(baseURL)/ring/notifications/count") else {
throw RemoteError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.timeoutInterval = 10.0
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw RemoteError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
if let count = try? JSONSerialization.jsonObject(with: data) as? Int {
return count
} else if let count = try? JSONSerialization.jsonObject(with: data) as? Double {
return Int(count)
}
return 0
case 404:
return 0
default:
throw RemoteError.httpError(httpResponse.statusCode)
}
}
}
struct NotificationProvider: TimelineProvider {
private let notificationService = NotificationService()
func placeholder(in context: Context) -> NotificationEntry {
NotificationEntry.placeholder()
}
func getSnapshot(in context: Context, completion: @escaping (NotificationEntry) -> ()) {
Task {
print("[WidgetKit] [NotificationProvider] Getting snapshot...")
async let notifications = try? await notificationService.fetchRecentNotifications(take: 5)
async let unreadCount = try? await notificationService.fetchUnreadCount()
let notifs = try? await notifications
let unread = (try? await unreadCount) ?? 0
print("[WidgetKit] [NotificationProvider] Snapshot - Notifications: \(notifs?.count ?? 0), Unread: \(unread)")
let entry = NotificationEntry(date: Date(), notifications: notifs, unreadCount: unread, error: nil, isLoading: false)
completion(entry)
}
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
Task {
let currentDate = Date()
print("[WidgetKit] [NotificationProvider] Getting timeline at \(currentDate)...")
do {
let takeLimit: Int
switch context.family {
case .systemSmall:
takeLimit = 0
case .systemMedium:
takeLimit = 1
case .systemLarge:
takeLimit = 3
default:
takeLimit = 0
}
async let notifications = try await notificationService.fetchRecentNotifications(take: takeLimit)
async let unreadCount = try await notificationService.fetchUnreadCount()
let notifs = try await notifications
let unread = try await unreadCount
print("[WidgetKit] [NotificationProvider] Timeline - Notifications: \(notifs.count), Unread: \(unread)")
let entry = NotificationEntry(date: currentDate, notifications: notifs, unreadCount: unread, error: nil, isLoading: false)
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)!
print("[WidgetKit] [NotificationProvider] Next update at: \(nextUpdate)")
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
} catch {
print("[WidgetKit] [NotificationProvider] Error in getTimeline: \(error.localizedDescription)")
let entry = NotificationEntry(date: currentDate, notifications: nil, unreadCount: 0, error: error.localizedDescription, isLoading: false)
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
}
}
struct NotificationWidgetEntryView: View {
var entry: NotificationProvider.Entry
@Environment(\.widgetFamily) var family
var body: some View {
if let notifications = entry.notifications, !notifications.isEmpty {
HasNotificationsView(notifications: notifications, unreadCount: entry.unreadCount)
} else if entry.isLoading {
LoadingView()
} else if let error = entry.error {
ErrorView(error: error)
} else {
EmptyView()
}
}
private var isCompact: Bool {
family == .systemSmall || isAccessory
}
private var isAccessory: Bool {
if #available(iOS 16.0, *) {
if case .accessoryRectangular = family {
return true
}
if case .accessoryCircular = family {
return true
}
}
return false
}
private var isCircular: Bool {
if #available(iOS 16.0, *) {
if case .accessoryCircular = family {
return true
}
}
return false
}
@ViewBuilder
private func HasNotificationsView(notifications: [SnNotification], unreadCount: Int) -> some View {
Link(destination: URL(string: "solian://notifications")!) {
if isCompact {
if isAccessory {
if isCircular {
ZStack {
Image(systemName: "bell.fill")
.font(.system(size: 20))
.foregroundColor(unreadCount > 0 ? .orange : .gray)
if unreadCount > 0 {
Text("\(min(unreadCount, 99))")
.font(.system(size: 10, weight: .bold))
.foregroundColor(.white)
.padding(4)
.background(Circle().fill(Color.blue))
.offset(x: 12, y: -12)
}
}
} else {
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 4) {
Image(systemName: "bell.fill")
.font(.caption2)
.foregroundColor(.orange)
.padding(.leading, 1.5)
Text(NSLocalizedString("notifications", comment: "Notifications"))
.font(.caption2)
.fontWeight(.bold)
Spacer()
}
if unreadCount > 0 {
HStack(spacing: 4) {
Text("\(unreadCount)")
.font(.caption2)
.fontWeight(.bold)
.padding(.horizontal, 6)
.background(
Capsule()
.fill(Color.blue.opacity(0.5))
)
Text(NSLocalizedString("unread", comment: "unread"))
.font(.caption2)
}
}
Text("on the Solar Network")
.font(.caption2)
.foregroundColor(.secondary)
.padding(.horizontal, 1.5)
}
}
} else {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 6) {
Image(systemName: "bell.fill")
.font(.subheadline)
.foregroundColor(.orange)
Text(NSLocalizedString("notifications", comment: "Notifications"))
.font(.headline)
.fontWeight(.bold)
Spacer()
}.padding(.bottom, 8)
Spacer(minLength: 2)
if unreadCount > 0 {
VStack(alignment: .leading, spacing: 2) {
Text("\(unreadCount)")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.bold)
}
}
Spacer(minLength: 2)
if unreadCount > 0 {
Text(NSLocalizedString("unreadNotifications", comment: "unread notifications"))
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
} else {
Text(NSLocalizedString("noUnreadNotifications", comment: "no unread notifications"))
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(12)
}
} else {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 6) {
Image(systemName: "bell.fill")
.font(.subheadline)
.foregroundColor(.orange)
Text(NSLocalizedString("notifications", comment: "Notifications"))
.font(.headline)
.fontWeight(.bold)
Spacer()
if unreadCount > 0 {
Text("\(unreadCount)")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(.blue)
.clipShape(Capsule())
}
}.padding(.bottom, 8)
let displayCount = family == .systemMedium ? 1 : 5
let displayNotifications = Array(notifications.prefix(displayCount))
VStack(alignment: .leading, spacing: 4) {
ForEach(displayNotifications) { notification in
NotificationItemView(notification: notification, compact: false)
}
}
if family == .systemMedium {
Spacer()
} else {
Spacer()
Text(NSLocalizedString("tapToViewAll", comment: "Tap to view all notifications"))
.font(.caption)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(12)
}
}
}
@ViewBuilder
private func NotificationItemView(notification: SnNotification, compact: Bool) -> some View {
HStack(alignment: .top, spacing: compact ? 6 : 12) {
if compact {
Image(systemName: notification.getTopicIcon())
.font(.caption)
.foregroundColor(.secondary)
} else {
ZStack {
Circle()
.fill(Color.gray.opacity(0.2))
.frame(width: 32, height: 32)
Image(systemName: notification.getTopicIcon())
.font(.caption)
.foregroundColor(.primary)
}
}
VStack(alignment: .leading, spacing: 2) {
Text(notification.title)
.font(compact ? .caption : .subheadline)
.fontWeight(notification.isUnread ? .semibold : .regular)
.lineLimit(1)
if !compact && !notification.subtitle.isEmpty {
Text(notification.subtitle)
.font(.caption)
.lineLimit(1)
}
if !compact && !notification.content.isEmpty {
Text(notification.content)
.font(.caption2)
.lineLimit(2)
.multilineTextAlignment(.leading)
}
if let createdDate = notification.createdDate {
Text(formatRelativeTime(createdDate))
.font(.caption2)
.foregroundColor(.secondary)
.padding(.top, 2)
}
}
Spacer()
if notification.isUnread {
Circle()
.fill(Color.blue)
.frame(width: compact ? 6 : 8, height: compact ? 6 : 8)
.padding(.trailing, 6)
.padding(.top, 8)
}
}
.padding(.vertical, compact ? 2 : 4)
}
@ViewBuilder
private func NotificationCompactItem(notification: SnNotification) -> some View {
HStack(spacing: 4) {
Image(systemName: notification.getTopicIcon())
.font(.caption2)
.foregroundColor(.secondary)
Text(notification.title)
.font(.caption2)
.lineLimit(1)
.fontWeight(notification.isUnread ? .semibold : .regular)
Spacer()
}
}
@ViewBuilder
private func EmptyView() -> some View {
Link(destination: URL(string: "solian://notifications")!) {
if isCircular {
ZStack {
Image(systemName: "bell")
.font(.system(size: 20))
.foregroundColor(.gray)
}
} else {
VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) {
HStack(spacing: 6) {
Image(systemName: "bell")
.font(isAccessory ? .caption : .title3)
.foregroundColor(.secondary)
Text(NSLocalizedString("notifications", comment: "Notifications"))
.font(isAccessory ? .caption2 : .headline)
.fontWeight(.bold)
Spacer()
}
if !isAccessory {
Text(NSLocalizedString("noNotifications", comment: "No notifications yet"))
.font(.caption)
.foregroundColor(.secondary)
Spacer()
}
}
.padding(isAccessory ? 4 : 12)
}
}
}
@ViewBuilder
private func LoadingView() -> some View {
if isCircular {
ProgressView()
.scaleEffect(0.8)
} else {
VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) {
HStack(spacing: 6) {
ProgressView()
.scaleEffect(isAccessory ? 0.6 : 0.8)
Text(NSLocalizedString("loading", comment: "Loading..."))
.font(isAccessory ? .caption2 : .caption)
.foregroundColor(.secondary)
Spacer()
}
if !isAccessory {
Spacer()
}
}
.padding(isAccessory ? 4 : 12)
}
}
@ViewBuilder
private func ErrorView(error: String) -> some View {
Link(destination: URL(string: "solian://notifications")!) {
if isCircular {
ZStack {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 20))
.foregroundColor(.red)
}
} else {
VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle")
.foregroundColor(.secondary)
.font(isAccessory ? .caption : .title3)
Text(NSLocalizedString("error", comment: "Error"))
.font(isAccessory ? .caption2 : .headline)
Spacer()
}
if !isAccessory {
Text(NSLocalizedString("openAppToRefresh", comment: "Open app to refresh"))
.font(.caption)
.foregroundColor(.secondary)
Text(error)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(nil)
.multilineTextAlignment(.leading)
Spacer()
}
}
.padding(isAccessory ? 4 : 12)
}
}
}
private func formatRelativeTime(_ date: Date) -> String {
let now = Date()
let interval = now.timeIntervalSince(date)
if interval < 60 {
return NSLocalizedString("justNow", comment: "Just now")
} else if interval < 3600 {
let minutes = Int(interval / 60)
return String(format: NSLocalizedString("minutesAgo", comment: "%d min ago"), minutes)
} else if interval < 86400 {
let hours = Int(interval / 3600)
return String(format: NSLocalizedString("hoursAgo", comment: "%d hr ago"), hours)
} else {
let days = Int(interval / 86400)
return String(format: NSLocalizedString("daysAgo", comment: "%d d ago"), days)
}
}
}
struct NotificationWidgetRootView: View {
var entry: NotificationProvider.Entry
@Environment(\.colorScheme) var colorScheme
var body: some View {
if #available(iOS 17.0, *) {
ZStack {
NotificationWidgetEntryView(entry: entry)
if let notifications = entry.notifications, !notifications.isEmpty {
GeometryReader { geometry in
Image(colorScheme == .dark ? "CloudyLambDark" : "CloudyLamb")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(
width: geometry.size.width * 0.9,
height: geometry.size.width * 0.9
)
.opacity(0.12)
.mask(
LinearGradient(
gradient: Gradient(colors: [
Color.white,
Color.white,
Color.clear
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.position(
x: geometry.size.width * 0.85,
y: 20
)
}
.allowsHitTesting(false)
}
}
.containerBackground(.fill.tertiary, for: .widget)
.padding(.vertical, 8)
} else {
NotificationWidgetEntryView(entry: entry)
.padding()
.background()
}
}
}
struct SolianNotificationWidget: Widget {
let kind: String = "SolianNotificationWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: NotificationProvider()) { entry in
NotificationWidgetRootView(entry: entry)
}
.configurationDisplayName("Notifications")
.description("View your recent notifications")
.supportedFamilies(supportedFamilies)
}
private var supportedFamilies: [WidgetFamily] {
#if os(iOS)
return [.systemSmall, .systemMedium, .systemLarge, .accessoryRectangular, .accessoryCircular]
#else
return [.systemSmall, .systemMedium, .systemLarge]
#endif
}
}
#if os(iOS)
#Preview(as: .accessoryRectangular) {
SolianNotificationWidget()
} timeline: {
NotificationEntry(
date: .now,
notifications: [
SnNotification(
id: "1",
topic: "post.replies",
title: "New reply to your post",
subtitle: "Someone replied to your message",
content: "This is notification content",
meta: nil,
priority: 0,
viewedAt: nil,
accountId: "acc-1",
createdAt: ISO8601DateFormatter().string(from: Date()),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
),
SnNotification(
id: "2",
topic: "relationships.friends.request",
title: "New friend request",
subtitle: "You have a pending friend request",
content: "Someone wants to be your friend",
meta: nil,
priority: 0,
viewedAt: ISO8601DateFormatter().string(from: Date()),
accountId: "acc-1",
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-3600)),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
)
],
unreadCount: 1,
error: nil,
isLoading: false
)
}
#endif
#Preview(as: .systemSmall) {
SolianNotificationWidget()
} timeline: {
NotificationEntry(
date: .now,
notifications: [
SnNotification(
id: "1",
topic: "post.replies",
title: "New reply to your post",
subtitle: "Someone replied to your message",
content: "This is notification content",
meta: nil,
priority: 0,
viewedAt: nil,
accountId: "acc-1",
createdAt: ISO8601DateFormatter().string(from: Date()),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
),
SnNotification(
id: "2",
topic: "relationships.friends.request",
title: "New friend request",
subtitle: "You have a pending friend request",
content: "Someone wants to be your friend",
meta: nil,
priority: 0,
viewedAt: ISO8601DateFormatter().string(from: Date()),
accountId: "acc-1",
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-3600)),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
)
],
unreadCount: 1,
error: nil,
isLoading: false
)
}
#Preview(as: .systemMedium) {
SolianNotificationWidget()
} timeline: {
NotificationEntry(
date: .now,
notifications: [
SnNotification(
id: "1",
topic: "post.replies",
title: "New reply to your post",
subtitle: "Someone replied to your message",
content: "This is notification content",
meta: nil,
priority: 0,
viewedAt: nil,
accountId: "acc-1",
createdAt: ISO8601DateFormatter().string(from: Date()),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
),
SnNotification(
id: "2",
topic: "relationships.friends.request",
title: "New friend request",
subtitle: "You have a pending friend request",
content: "Someone wants to be your friend",
meta: nil,
priority: 0,
viewedAt: nil,
accountId: "acc-1",
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-3600)),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
),
SnNotification(
id: "3",
topic: "invites.chat",
title: "New chat invite",
subtitle: "You've been invited to a chat",
content: "Join the conversation",
meta: nil,
priority: 0,
viewedAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400)),
accountId: "acc-1",
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400)),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
)
],
unreadCount: 2,
error: nil,
isLoading: false
)
}
#if os(iOS)
#Preview(as: .systemLarge) {
SolianNotificationWidget()
} timeline: {
NotificationEntry(
date: .now,
notifications: [
SnNotification(
id: "1",
topic: "post.replies",
title: "New reply",
subtitle: "Someone replied",
content: "Content",
meta: nil,
priority: 0,
viewedAt: nil,
accountId: "acc-1",
createdAt: ISO8601DateFormatter().string(from: Date()),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
),
SnNotification(
id: "2",
topic: "relationships.friends.request",
title: "New friend request",
subtitle: "You have a pending friend request",
content: "Someone wants to be your friend",
meta: nil,
priority: 0,
viewedAt: nil,
accountId: "acc-1",
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-3600)),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
),
SnNotification(
id: "3",
topic: "invites.chat",
title: "New chat invite",
subtitle: "You've been invited to a chat",
content: "Join the conversation",
meta: nil,
priority: 0,
viewedAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400)),
accountId: "acc-1",
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400)),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
)
],
unreadCount: 3,
error: nil,
isLoading: false
)
}
#Preview(as: .accessoryCircular) {
SolianNotificationWidget()
} timeline: {
NotificationEntry(
date: .now,
notifications: [
SnNotification(
id: "1",
topic: "post.replies",
title: "New reply",
subtitle: "Someone replied",
content: "Content",
meta: nil,
priority: 0,
viewedAt: nil,
accountId: "acc-1",
createdAt: ISO8601DateFormatter().string(from: Date()),
updatedAt: ISO8601DateFormatter().string(from: Date()),
deletedAt: nil
)
],
unreadCount: 5,
error: nil,
isLoading: false
)
}
#endif

View File

@@ -0,0 +1,549 @@
//
// SolianPostShuffleWidget.swift
// SolianWidgetExtension
//
// Created by LittleSheep on 2026/1/4.
//
import Foundation
import WidgetKit
import SwiftUI
struct SnPostPublisher: Codable {
let id: String
let name: String
let nick: String?
let description: String?
let picture: SnCloudFile?
enum CodingKeys: String, CodingKey {
case id
case name
case nick
case description
case picture
}
}
struct SnCloudFile: Codable {
let id: String
let url: String?
let thumbnail: String?
let mimeType: String?
enum CodingKeys: String, CodingKey {
case id
case url
case thumbnail
case mimeType = "mimetype"
}
}
struct SnPost: Codable, Identifiable {
let id: String
let title: String?
let description: String?
let content: String?
let publisher: SnPostPublisher
let tags: [SnPostTag]?
let createdAt: String?
let updatedAt: String?
let attachments: [SnCloudFile]?
var createdDate: Date? {
guard let createdAt = createdAt else { return nil }
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = formatter.date(from: createdAt) {
return date
}
// Fallback for timestamps without fractional seconds
formatter.formatOptions = [.withInternetDateTime]
return formatter.date(from: createdAt)
}
var hasTitle: Bool {
guard let title = title else { return false }
return !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
var hasDescription: Bool {
guard let description = description else { return false }
return !description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
var hasContent: Bool {
guard let content = content else { return false }
return !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
var attachmentCount: Int {
attachments?.count ?? 0
}
enum CodingKeys: String, CodingKey {
case id
case title
case description
case content
case publisher
case tags
case createdAt = "created_at"
case updatedAt = "updated_at"
case attachments
}
}
struct SnPostTag: Codable {
let id: String
let slug: String
let name: String?
}
class PostShuffleService {
private let networkService = WidgetNetworkService()
private lazy var session: URLSession = {
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 10.0
configuration.timeoutIntervalForResource = 10.0
configuration.waitsForConnectivity = false
return URLSession(configuration: configuration)
}()
func fetchRandomPost() async throws -> SnPost? {
guard let token = networkService.token else {
throw RemoteError.missingCredentials
}
let baseURL = networkService.baseURL
guard let url = URL(string: "\(baseURL)/sphere/posts?shuffle=true&take=1") else {
throw RemoteError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.timeoutInterval = 10.0
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw RemoteError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
let decoder = JSONDecoder()
let posts = try decoder.decode([SnPost].self, from: data)
return posts.first
case 404:
return nil
default:
throw RemoteError.httpError(httpResponse.statusCode)
}
}
}
struct PostShuffleEntry: TimelineEntry {
let date: Date
let post: SnPost?
let error: String?
let isLoading: Bool
static func placeholder() -> PostShuffleEntry {
PostShuffleEntry(date: Date(), post: nil, error: nil, isLoading: true)
}
}
struct PostShuffleProvider: TimelineProvider {
private let postShuffleService = PostShuffleService()
func placeholder(in context: Context) -> PostShuffleEntry {
PostShuffleEntry.placeholder()
}
func getSnapshot(in context: Context, completion: @escaping (PostShuffleEntry) -> ()) {
Task {
print("[WidgetKit] [PostShuffleProvider] Getting snapshot...")
let post = try? await postShuffleService.fetchRandomPost()
print("[WidgetKit] [PostShuffleProvider] Snapshot - Post: \(post != nil ? "Found" : "Not found")")
let entry = PostShuffleEntry(date: Date(), post: post, error: nil, isLoading: false)
completion(entry)
}
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
Task {
let currentDate = Date()
print("[WidgetKit] [PostShuffleProvider] Getting timeline at \(currentDate)...")
do {
let post = try await postShuffleService.fetchRandomPost()
print("[WidgetKit] [PostShuffleProvider] Timeline - Post: \(post != nil ? "Found" : "Not found")")
let entry = PostShuffleEntry(date: currentDate, post: post, error: nil, isLoading: false)
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 10, to: currentDate)!
print("[WidgetKit] [PostShuffleProvider] Next update at: \(nextUpdate)")
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
} catch {
print("[WidgetKit] [PostShuffleProvider] Error in getTimeline: \(error.localizedDescription)")
let entry = PostShuffleEntry(date: currentDate, post: nil, error: error.localizedDescription, isLoading: false)
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
}
}
struct PostShuffleWidgetEntryView: View {
var entry: PostShuffleProvider.Entry
@Environment(\.widgetFamily) var family
var body: some View {
if let post = entry.post {
PostContentView(post: post)
} else if entry.isLoading {
LoadingView()
} else if let error = entry.error {
ErrorView(error: error)
} else {
EmptyView()
}
}
@ViewBuilder
private func PostContentView(post: SnPost) -> some View {
Link(destination: URL(string: "solian://posts/\(post.id)")!) {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .top, spacing: 8) {
if let avatarUrl = post.publisher.picture?.url ?? post.publisher.picture?.thumbnail {
AsyncImage(url: URL(string: avatarUrl)) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
case .failure(_):
Circle()
.fill(Color.gray.opacity(0.3))
default:
Circle()
.fill(Color.gray.opacity(0.3))
.overlay(
ProgressView()
.scaleEffect(0.5)
)
}
}
.frame(width: 40, height: 40)
.clipShape(Circle())
} else {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: 40, height: 40)
}
VStack(alignment: .leading, spacing: 2) {
Text(post.publisher.nick ?? post.publisher.name)
.font(.headline)
.fontWeight(.semibold)
.lineLimit(1)
if let createdDate = post.createdDate {
Text(formatRelativeTime(createdDate))
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
}
if post.attachmentCount > 0 {
HStack(spacing: 4) {
Image(systemName: "paperclip")
.font(.caption2)
.foregroundColor(.secondary)
Text("\(post.attachmentCount)")
.font(.caption2)
.foregroundColor(.secondary)
}
}
if post.hasTitle {
Text(post.title!)
.font(family == .systemMedium ? .subheadline : .body)
.fontWeight(.semibold)
.lineLimit(family == .systemMedium ? 2 : 3)
}
if post.hasDescription {
Text(post.description!)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(family == .systemMedium ? 2 : 4)
}
if post.hasContent {
Text(post.content!)
.font(.caption)
.foregroundColor(.primary)
.lineLimit(family == .systemMedium ? 3 : 8)
}
if let tags = post.tags, !tags.isEmpty {
let displayTags = Array(tags.prefix(family == .systemMedium ? 2 : 4))
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
ForEach(displayTags, id: \.id) { tag in
Text("#\(tag.name ?? tag.slug)")
.font(.caption2)
.foregroundColor(.blue)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.blue.opacity(0.1))
.clipShape(Capsule())
}
}
}
}
Spacer()
}
.padding(12)
}
}
@ViewBuilder
private func EmptyView() -> some View {
Link(destination: URL(string: "solian://posts/shuffle")!) {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 6) {
Image(systemName: "text.alignleft")
.font(.title3)
.foregroundColor(.secondary)
Spacer()
}
Text(NSLocalizedString("noPostsAvailable", comment: "No posts available"))
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
Text(NSLocalizedString("tapToRefresh", comment: "Tap to refresh"))
.font(.caption)
.foregroundColor(.secondary)
}
.padding(12)
}
}
@ViewBuilder
private func LoadingView() -> some View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 6) {
ProgressView()
.scaleEffect(0.8)
Text(NSLocalizedString("loadingPost", comment: "Loading post..."))
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
}
Spacer()
}
.padding(12)
}
@ViewBuilder
private func ErrorView(error: String) -> some View {
Link(destination: URL(string: "solian://posts/shuffle")!) {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle")
.font(.title3)
.foregroundColor(.secondary)
Text(NSLocalizedString("error", comment: "Error"))
.font(.headline)
Spacer()
}
Text(NSLocalizedString("openAppToRefresh", comment: "Open app to refresh"))
.font(.caption)
.foregroundColor(.secondary)
Text(error)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(5)
.multilineTextAlignment(.leading)
Spacer()
}
.padding(12)
}
}
private func formatRelativeTime(_ date: Date) -> String {
let now = Date()
let interval = now.timeIntervalSince(date)
if interval < 60 {
return NSLocalizedString("justNow", comment: "Just now")
} else if interval < 3600 {
let minutes = Int(interval / 60)
return String(format: NSLocalizedString("minutesAgo", comment: "%d min ago"), minutes)
} else if interval < 86400 {
let hours = Int(interval / 3600)
return String(format: NSLocalizedString("hoursAgo", comment: "%d hr ago"), hours)
} else {
let days = Int(interval / 86400)
return String(format: NSLocalizedString("daysAgo", comment: "%d d ago"), days)
}
}
}
struct PostShuffleWidgetRootView: View {
var entry: PostShuffleProvider.Entry
@Environment(\.colorScheme) var colorScheme
var body: some View {
if #available(iOS 17.0, *) {
ZStack {
PostShuffleWidgetEntryView(entry: entry)
if entry.post != nil {
GeometryReader { geometry in
Image(colorScheme == .dark ? "CloudyLambDark" : "CloudyLamb")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(
width: geometry.size.width * 0.9,
height: geometry.size.width * 0.9
)
.opacity(0.12)
.mask(
LinearGradient(
gradient: Gradient(colors: [
Color.white,
Color.white,
Color.clear
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.position(
x: geometry.size.width * 0.9,
y: 20
)
}
.allowsHitTesting(false)
}
}
.containerBackground(.fill.tertiary, for: .widget)
.padding(.vertical, 8)
} else {
PostShuffleWidgetEntryView(entry: entry)
.padding()
.background()
}
}
}
struct SolianPostShuffleWidget: Widget {
let kind: String = "SolianPostShuffleWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: PostShuffleProvider()) { entry in
PostShuffleWidgetRootView(entry: entry)
}
.configurationDisplayName("Random Post")
.description("Discover a random post from the network")
.supportedFamilies(supportedFamilies)
}
private var supportedFamilies: [WidgetFamily] {
return [.systemMedium, .systemLarge]
}
}
#Preview(as: .systemMedium) {
SolianPostShuffleWidget()
} timeline: {
PostShuffleEntry(
date: .now,
post: SnPost(
id: "test-post-id",
title: "Hello World!",
description: "This is a test post description",
content: "This is a content of a test post. It can be longer and show more text in the widget.",
publisher: SnPostPublisher(
id: "publisher-1",
name: "Test Publisher",
nick: "Testy",
description: "A test publisher",
picture: nil
),
tags: [
SnPostTag(id: "tag-1", slug: "test", name: "Test"),
SnPostTag(id: "tag-2", slug: "example", name: "Example")
],
createdAt: ISO8601DateFormatter().string(from: Date()),
updatedAt: ISO8601DateFormatter().string(from: Date()),
attachments: []
),
error: nil,
isLoading: false
)
}
#Preview(as: .systemLarge) {
SolianPostShuffleWidget()
} timeline: {
PostShuffleEntry(
date: .now,
post: SnPost(
id: "test-post-id",
title: "Welcome to the Solar Network!",
description: "This is a test post description that is a bit longer to demonstrate the widget layout with various content types",
content: "This is content of a test post. It can be longer and show more text in the widget. The large widget should display more content and allow for better reading experience. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
publisher: SnPostPublisher(
id: "publisher-1",
name: "Test Publisher",
nick: "Testy McTestface",
description: "A test publisher",
picture: nil
),
tags: [
SnPostTag(id: "tag-1", slug: "test", name: "Test"),
SnPostTag(id: "tag-2", slug: "example", name: "Example"),
SnPostTag(id: "tag-3", slug: "widget", name: "Widget"),
SnPostTag(id: "tag-4", slug: "ios", name: "iOS")
],
createdAt: ISO8601DateFormatter().string(from: Date()),
updatedAt: ISO8601DateFormatter().string(from: Date()),
attachments: [
SnCloudFile(id: "file-1", url: nil, thumbnail: nil, mimeType: "image/jpeg"),
SnCloudFile(id: "file-2", url: nil, thumbnail: nil, mimeType: "image/png")
]
),
error: nil,
isLoading: false
)
}

View File

@@ -0,0 +1,18 @@
//
// SolianWidgetExtensionBundle.swift
// SolianWidgetExtension
//
// Created by LittleSheep on 2026/1/3.
//
import WidgetKit
import SwiftUI
@main
struct SolianWidgetExtensionBundle: WidgetBundle {
var body: some Widget {
SolianCheckInWidget()
SolianNotificationWidget()
SolianPostShuffleWidget()
}
}

View File

@@ -0,0 +1,136 @@
//
// Networking.swift
// SolianWidgetExtensionExtension
//
// Created by LittleSheep on 2026/1/4.
//
import Foundation
enum RemoteError: Error {
case missingCredentials
case invalidURL
case invalidResponse
case httpError(Int)
case decodingError
}
extension RemoteError: LocalizedError {
var errorDescription: String? {
switch self {
case .missingCredentials:
return "Please open the app to sign in."
case .invalidURL:
return "Invalid server configuration."
case .invalidResponse:
return "Server returned an invalid response."
case .httpError(let code):
return "Server error (\(code))."
case .decodingError:
return "Failed to read server data."
}
}
}
struct TokenData: Codable {
let token: String
}
class WidgetNetworkService {
private let appGroup = "group.solsynth.solian"
private let tokenKey = "flutter.dyn_user_tk"
private let urlKey = "flutter.app_server_url"
private lazy var session: URLSession = {
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 10.0
configuration.timeoutIntervalForResource = 10.0
configuration.waitsForConnectivity = false
return URLSession(configuration: configuration)
}()
private var userDefaults: UserDefaults? {
UserDefaults(suiteName: appGroup)
}
var token: String? {
guard let tokenString = userDefaults?.string(forKey: tokenKey) else {
return nil
}
guard let data = tokenString.data(using: .utf8) else {
return nil
}
do {
let tokenData = try JSONDecoder().decode(TokenData.self, from: data)
return tokenData.token
} catch {
print("[WidgetKit] Failed to decode token: \(error)")
return nil
}
}
var baseURL: String {
return userDefaults?.string(forKey: urlKey) ?? "https://api.solian.app"
}
func makeRequest<T: Codable>(
path: String,
method: String = "GET",
headers: [String: String] = [:]
) async throws -> T? {
guard let token = token else {
throw RemoteError.missingCredentials
}
guard let url = URL(string: "\(baseURL)\(path)") else {
throw RemoteError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = method
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Accept")
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
request.timeoutInterval = 10.0
print("[WidgetKit] [Network] Requesting: \(baseURL)\(path)")
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw RemoteError.invalidResponse
}
print("[WidgetKit] [Network] Status: \(httpResponse.statusCode), Data length: \(data.count)")
if let jsonString = String(data: data, encoding: .utf8) {
print("[WidgetKit] [Network] Response: \(jsonString.prefix(500))")
}
switch httpResponse.statusCode {
case 200...299:
let decoder = JSONDecoder()
do {
let result = try decoder.decode(T.self, from: data)
print("[WidgetKit] [Network] Successfully decoded response")
return result
} catch {
print("[WidgetKit] [Network] Decoding error: \(error.localizedDescription)")
print("[WidgetKit] [Network] Expected type: \(String(describing: T.self))")
throw RemoteError.decodingError
}
case 404:
print("[WidgetKit] [Network] Resource not found (404)")
return nil
default:
print("[WidgetKit] [Network] HTTP Error: \(httpResponse.statusCode)")
throw RemoteError.httpError(httpResponse.statusCode)
}
}
}

View File

@@ -0,0 +1,46 @@
/* Check In Level Names */
"checkInResultT0" = "Great Misfortune";
"checkInResultT1" = "Misfortune";
"checkInResultT2" = "Moderate";
"checkInResultT3" = "Fortune";
"checkInResultT4" = "Great Fortune";
"checkInResultT5" = "Special";
/* Widget UI Strings */
"checkIn" = "Check In";
"tapToCheckIn" = "Tap to check in today";
"error" = "Error";
"openAppToRefresh" = "Open app to refresh";
"loading" = "Loading...";
"rewardPoints" = "%d";
"rewardExperience" = "%d EXP";
"footer" = "Solian Check In";
/* Notable Day Strings */
"notableDayToday" = "%@ is today!";
"notableDayIs" = "%@ is %@";
"notableDayUpcoming" = "Upcoming";
"today" = "Today";
/* Notification Widget Strings */
"notifications" = "Notifications";
"noNotifications" = "No notifications yet";
"tapToViewAll" = "Tap to view all notifications";
"justNow" = "Just now";
"minutesAgo" = "%d min ago";
"hoursAgo" = "%d hr ago";
"daysAgo" = "%d d ago";
"pleaseSignIn" = "Please open the app to sign in.";
"invalidServerConfig" = "Invalid server configuration.";
"invalidResponse" = "Server returned an invalid response.";
"httpError" = "Server error (%d).";
"decodingError" = "Failed to read server data.";
"unreadNotifications" = "Notification(s) unread";
"noNotifications" = "No notifications";
"noUnreadNotifications" = "All notifications are read";
"unread" = "unread";
/* Post Shuffle Widget Strings */
"noPostsAvailable" = "No posts available";
"tapToRefresh" = "Tap to refresh";
"loadingPost" = "Loading post...";

View File

@@ -0,0 +1,41 @@
/* Check In Level Names */
"checkInResultT0" = "Gran Desventura";
"checkInResultT1" = "Desventura";
"checkInResultT2" = "Moderado";
"checkInResultT3" = "Buena Fortuna";
"checkInResultT4" = "Gran Fortuna";
"checkInResultT5" = "Especial";
/* Widget UI Strings */
"checkIn" = "Registrar";
"tapToCheckIn" = "Toca para registrar hoy";
"error" = "Error";
"openAppToRefresh" = "Abre la aplicación para actualizar";
"loading" = "Cargando...";
"rewardPoints" = "%d";
"rewardExperience" = "%d EXP";
"footer" = "Registro Solian";
/* Notable Day Strings */
"notableDayToday" = "%@ es hoy!";
"notableDayIs" = "%@ es %@";
"today" = "Hoy";
/* Notification Widget Strings */
"notifications" = "Notificaciones";
"noNotifications" = "Aún no hay notificaciones";
"tapToViewAll" = "Toca para ver todas las notificaciones";
"justNow" = "Ahora mismo";
"minutesAgo" = "hace %d min";
"hoursAgo" = "hace %d hr";
"daysAgo" = "hace %d días";
"pleaseSignIn" = "Por favor, abre la aplicación para iniciar sesión.";
"invalidServerConfig" = "Configuración del servidor no válida.";
"invalidResponse" = "El servidor devolvió una respuesta no válida.";
"httpError" = "Error del servidor (%d).";
"decodingError" = "Error al leer los datos del servidor.";
/* Post Shuffle Widget Strings */
"noPostsAvailable" = "No hay publicaciones disponibles";
"tapToRefresh" = "Toca para actualizar";
"loadingPost" = "Cargando publicación...";

View File

@@ -0,0 +1,41 @@
/* Check In Level Names */
"checkInResultT0" = "大凶";
"checkInResultT1" = "凶";
"checkInResultT2" = "中平";
"checkInResultT3" = "吉";
"checkInResultT4" = "大吉";
"checkInResultT5" = "特殊";
/* Widget UI Strings */
"checkIn" = "チェックイン";
"tapToCheckIn" = "タップして今日チェックイン";
"error" = "エラー";
"openAppToRefresh" = "アプリを開いて更新";
"loading" = "読み込み中...";
"rewardPoints" = "%d";
"rewardExperience" = "%d 経験値";
"footer" = "Solian チェックイン";
/* Notable Day Strings */
"notableDayToday" = "%@は今日です!";
"notableDayIs" = "%@は%@です";
"today" = "今日";
/* Notification Widget Strings */
"notifications" = "通知";
"noNotifications" = "まだ通知はありません";
"tapToViewAll" = "タップしてすべての通知を表示";
"justNow" = "たった今";
"minutesAgo" = "%d分前";
"hoursAgo" = "%d時間前";
"daysAgo" = "%d日前";
"pleaseSignIn" = "アプリを開いてサインインしてください。";
"invalidServerConfig" = "サーバー設定が無効です。";
"invalidResponse" = "サーバーから無効な応答が返されました。";
"httpError" = "サーバーエラー (%d)。";
"decodingError" = "サーバーデータの読み込みに失敗しました。";
/* Post Shuffle Widget Strings */
"noPostsAvailable" = "利用可能な投稿がありません";
"tapToRefresh" = "タップして更新";
"loadingPost" = "投稿を読み込み中...";

View File

@@ -0,0 +1,41 @@
/* Check In Level Names */
"checkInResultT0" = "대흉";
"checkInResultT1" = "흉";
"checkInResultT2" = "중평";
"checkInResultT3" = "길";
"checkInResultT4" = "대길";
"checkInResultT5" = "특수";
/* Widget UI Strings */
"checkIn" = "체크인";
"tapToCheckIn" = "탭하여 오늘 체크인";
"error" = "오류";
"openAppToRefresh" = "앱을 열어 새로고침";
"loading" = "로딩 중...";
"rewardPoints" = "%d";
"rewardExperience" = "%d 경험치";
"footer" = "Solian 체크인";
/* Notable Day Strings */
"notableDayToday" = "%@ 오늘입니다!";
"notableDayIs" = "%@ 은/는 %@";
"today" = "오늘";
/* Notification Widget Strings */
"notifications" = "알림";
"noNotifications" = "아직 알림이 없습니다";
"tapToViewAll" = "탭하여 모든 알림 보기";
"justNow" = "방금";
"minutesAgo" = "%d분 전";
"hoursAgo" = "%d시간 전";
"daysAgo" = "%d일 전";
"pleaseSignIn" = "앱을 열어 로그인하세요.";
"invalidServerConfig" = "서버 구성이 올바르지 않습니다.";
"invalidResponse" = "서버에서 잘못된 응답을 받았습니다.";
"httpError" = "서버 오류 (%d).";
"decodingError" = "서버 데이터 읽기에 실패했습니다.";
/* Post Shuffle Widget Strings */
"noPostsAvailable" = "사용 가능한 게시물이 없습니다";
"tapToRefresh" = "탭하여 새로고침";
"loadingPost" = "게시물 로딩 중...";

View File

@@ -0,0 +1,46 @@
/* Check In Level Names */
"checkInResultT0" = "大凶";
"checkInResultT1" = "凶";
"checkInResultT2" = "中平";
"checkInResultT3" = "吉";
"checkInResultT4" = "大吉";
"checkInResultT5" = "特殊";
/* Widget UI Strings */
"checkIn" = "打卡";
"tapToCheckIn" = "点击今日打卡";
"error" = "错误";
"openAppToRefresh" = "打开应用以刷新";
"loading" = "加载中...";
"rewardPoints" = "%d";
"rewardExperience" = "%d 经验值";
"footer" = "Solian 签到";
/* Notable Day Strings */
"notableDayToday" = "%@是今天!";
"notableDayIs" = "%@ 是 %@";
"notableDayUpcoming" = "接下来";
"today" = "今天";
/* Notification Widget Strings */
"notifications" = "通知";
"noNotifications" = "还没有通知";
"tapToViewAll" = "点击查看所有通知";
"justNow" = "刚刚";
"minutesAgo" = "%d分钟前";
"hoursAgo" = "%d小时前";
"daysAgo" = "%d天前";
"pleaseSignIn" = "请打开应用登录。";
"invalidServerConfig" = "服务器配置无效。";
"invalidResponse" = "服务器返回了无效响应。";
"httpError" = "服务器错误 (%d)。";
"decodingError" = "读取服务器数据失败。";
"unreadNotifications" = "条通知未读";
"noNotifications" = "没有通知";
"noUnreadNotifications" = "没有未读通知";
"unread" = "未读";
/* Post Shuffle Widget Strings */
"noPostsAvailable" = "没有可用的帖子";
"tapToRefresh" = "点击刷新";
"loadingPost" = "加载帖子中...";

View File

@@ -0,0 +1,41 @@
/* Check In Level Names */
"checkInResultT0" = "大凶";
"checkInResultT1" = "凶";
"checkInResultT2" = "中平";
"checkInResultT3" = "吉";
"checkInResultT4" = "大吉";
"checkInResultT5" = "特殊";
/* Widget UI Strings */
"checkIn" = "打卡";
"tapToCheckIn" = "點擊今日打卡";
"error" = "錯誤";
"openAppToRefresh" = "打開應用以刷新";
"loading" = "載入中...";
"rewardPoints" = "%d";
"rewardExperience" = "%d 經驗值";
"footer" = "Solian 簽到";
/* Notable Day Strings */
"notableDayToday" = "%@是今天!";
"notableDayIs" = "%@ 是 %@";
"today" = "今天";
/* Notification Widget Strings */
"notifications" = "通知";
"noNotifications" = "還沒有通知";
"tapToViewAll" = "點擊查看所有通知";
"justNow" = "剛剛";
"minutesAgo" = "%d分鐘前";
"hoursAgo" = "%d小時前";
"daysAgo" = "%d天前";
"pleaseSignIn" = "請打開應用登錄。";
"invalidServerConfig" = "伺服器配置無效。";
"invalidResponse" = "伺服器返回了無效響應。";
"httpError" = "伺服器錯誤 (%d)。";
"decodingError" = "讀取伺服器數據失敗。";
/* Post Shuffle Widget Strings */
"noPostsAvailable" = "沒有可用的帖子";
"tapToRefresh" = "點擊刷新";
"loadingPost" = "載入帖子中...";

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.solsynth.solian</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -1,6 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/abuse_report.dart';
import 'package:island/pods/network.dart';
import 'package:island/accounts/accounts_models/abuse_report.dart';
import 'package:island/core/network.dart';
final abuseReportServiceProvider = Provider<AbuseReportService>((ref) {
return AbuseReportService(ref);

View File

@@ -0,0 +1,164 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/accounts/accounts_models/account.dart';
import 'package:island/core/network.dart';
import 'package:island/pagination/pagination.dart';
import 'package:island/core/services/time.dart';
import 'package:island/shared/widgets/pagination_list.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'credits.g.dart';
@riverpod
Future<double> socialCredits(Ref ref) async {
final client = ref.watch(apiClientProvider);
final response = await client.get('/pass/accounts/me/credits');
if (response.statusCode != 200) {
throw Exception('Failed to load social credits');
}
return response.data?.toDouble() ?? 0.0;
}
final socialCreditHistoryNotifierProvider = AsyncNotifierProvider.autoDispose(
SocialCreditHistoryNotifier.new,
);
class SocialCreditHistoryNotifier
extends AsyncNotifier<PaginationState<SnSocialCreditRecord>>
with AsyncPaginationController<SnSocialCreditRecord> {
static const int pageSize = 20;
@override
FutureOr<PaginationState<SnSocialCreditRecord>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<List<SnSocialCreditRecord>> fetch() async {
final client = ref.read(apiClientProvider);
final queryParams = {'offset': fetchedCount.toString(), 'take': pageSize};
final response = await client.get(
'/pass/accounts/me/credits/history',
queryParameters: queryParams,
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final records = response.data
.map((json) => SnSocialCreditRecord.fromJson(json))
.cast<SnSocialCreditRecord>()
.toList();
return records;
}
}
class SocialCreditsTab extends HookConsumerWidget {
const SocialCreditsTab({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final socialCredits = ref.watch(socialCreditsProvider);
return Column(
children: [
const Gap(8),
Card(
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
child: socialCredits
.when(
data: (credits) => Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
credits < 100
? 'socialCreditsLevelPoor'.tr()
: credits < 150
? 'socialCreditsLevelNormal'.tr()
: credits < 200
? 'socialCreditsLevelGood'.tr()
: 'socialCreditsLevelExcellent'.tr(),
).tr().bold().fontSize(20),
Text('${credits.toStringAsFixed(2)} pts').fontSize(14),
const Gap(8),
LinearProgressIndicator(value: credits / 200),
],
),
Positioned(
right: 0,
top: 0,
child: IconButton(
onPressed: () {},
icon: const Icon(Symbols.info),
tooltip: 'socialCreditsDescription'.tr(),
),
),
],
),
error: (_, _) => Text('Error loading credits'),
loading: () => const LinearProgressIndicator(),
)
.padding(horizontal: 20, vertical: 16),
),
Expanded(
child: PaginationList(
padding: EdgeInsets.zero,
provider: socialCreditHistoryNotifierProvider,
notifier: socialCreditHistoryNotifierProvider.notifier,
itemBuilder: (context, idx, record) {
final isExpired =
record.expiredAt != null &&
record.expiredAt!.isBefore(DateTime.now());
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(
record.reason,
style: isExpired
? TextStyle(
decoration: TextDecoration.lineThrough,
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.8),
)
: null,
),
subtitle: Row(
spacing: 4,
children: [
Text(record.createdAt.formatSystem()),
Text('to'),
if (record.expiredAt != null)
Text(record.expiredAt!.formatSystem()),
],
),
trailing: Text(
record.delta > 0 ? '+${record.delta}' : '${record.delta}',
style: TextStyle(
color: record.delta > 0 ? Colors.green : Colors.red,
),
),
);
},
),
),
],
);
}
}

View File

@@ -0,0 +1,43 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'credits.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(socialCredits)
final socialCreditsProvider = SocialCreditsProvider._();
final class SocialCreditsProvider
extends $FunctionalProvider<AsyncValue<double>, double, FutureOr<double>>
with $FutureModifier<double>, $FutureProvider<double> {
SocialCreditsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'socialCreditsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$socialCreditsHash();
@$internal
@override
$FutureProviderElement<double> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<double> create(Ref ref) {
return socialCredits(ref);
}
}
String _$socialCreditsHash() => r'a0284583e94bc97285c689ac2bc018536932da69';

View File

@@ -1,55 +1,64 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/account/credits.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/account/leveling_progress.dart';
import 'package:island/widgets/account/stellar_program_tab.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/accounts/accounts_models/account.dart';
import 'package:island/accounts/accounts_widgets/account/leveling_progress.dart';
import 'package:island/accounts/accounts_widgets/account/stellar_program_tab.dart';
import 'package:island/core/network.dart';
import 'package:island/pagination/pagination.dart';
import 'package:island/accounts/accounts_pod.dart';
import 'package:island/accounts/account/credits.dart';
import 'package:island/core/services/time.dart';
import 'package:island/shared/widgets/app_scaffold.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/shared/widgets/pagination_list.dart';
import 'package:styled_widget/styled_widget.dart';
part 'leveling.g.dart';
final levelingHistoryNotifierProvider =
AsyncNotifierProvider.autoDispose<
LevelingHistoryNotifier,
PaginationState<SnExperienceRecord>
>(LevelingHistoryNotifier.new);
@riverpod
class LevelingHistoryNotifier extends _$LevelingHistoryNotifier
with CursorPagingNotifierMixin<SnExperienceRecord> {
static const int _pageSize = 20;
class LevelingHistoryNotifier
extends AsyncNotifier<PaginationState<SnExperienceRecord>>
with AsyncPaginationController<SnExperienceRecord> {
static const int pageSize = 20;
@override
Future<CursorPagingData<SnExperienceRecord>> build() => fetch(cursor: null);
FutureOr<PaginationState<SnExperienceRecord>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<CursorPagingData<SnExperienceRecord>> fetch({
required String? cursor,
}) async {
Future<List<SnExperienceRecord>> fetch() async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final queryParams = {'offset': offset, 'take': _pageSize};
final queryParams = {'offset': fetchedCount.toString(), 'take': pageSize};
final response = await client.get(
'/pass/accounts/me/leveling',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final records =
data.map((json) => SnExperienceRecord.fromJson(json)).toList();
final hasMore = offset + records.length < total;
final nextCursor = hasMore ? (offset + records.length).toString() : null;
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
return CursorPagingData(
items: records,
hasMore: hasMore,
nextCursor: nextCursor,
);
final List<SnExperienceRecord> records = response.data
.map((json) => SnExperienceRecord.fromJson(json))
.cast<SnExperienceRecord>()
.toList();
return records;
}
}
@@ -170,8 +179,9 @@ class LevelingScreen extends HookConsumerWidget {
stopIndicatorRadius: 0,
trackGap: 0,
color: Theme.of(context).colorScheme.primary,
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerHigh,
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(32),
),
],
@@ -189,19 +199,12 @@ class LevelingScreen extends HookConsumerWidget {
),
),
const SliverGap(8),
PagingHelperSliverView(
PaginationList(
provider: levelingHistoryNotifierProvider,
futureRefreshable: levelingHistoryNotifierProvider.future,
notifierRefreshable: levelingHistoryNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final record = data.items[index];
return ListTile(
notifier: levelingHistoryNotifierProvider.notifier,
isRefreshable: false,
isSliver: true,
itemBuilder: (context, idx, record) => ListTile(
title: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -214,9 +217,7 @@ class LevelingScreen extends HookConsumerWidget {
record.createdAt.formatRelative(context),
).fontSize(13),
Text('·').fontSize(13).bold(),
Text(
record.createdAt.formatSystem(),
).fontSize(13),
Text(record.createdAt.formatSystem()).fontSize(13),
],
).opacity(0.8),
],
@@ -224,17 +225,13 @@ class LevelingScreen extends HookConsumerWidget {
subtitle: Row(
spacing: 8,
children: [
Text(
'${record.delta > 0 ? '+' : ''}${record.delta} EXP',
),
Text('${record.delta > 0 ? '+' : ''}${record.delta} EXP'),
if (record.bonusMultiplier != 1.0)
Text('x${record.bonusMultiplier}'),
],
),
minTileHeight: 56,
contentPadding: EdgeInsets.symmetric(horizontal: 4),
);
},
),
),
@@ -267,8 +264,7 @@ class LevelStairsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint =
Paint()
final paint = Paint()
..color = surfaceColor.withOpacity(0.2)
..strokeWidth = 1.5
..style = PaintingStyle.stroke;

View File

@@ -0,0 +1,496 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/accounts/accounts_widgets/account/account_devices.dart';
import 'package:island/auth/auth_models/auth.dart';
import 'package:island/accounts/accounts_models/account.dart';
import 'package:island/core/network.dart';
import 'package:island/accounts/accounts_pod.dart';
import 'package:island/accounts/account/me/settings_auth_factors.dart';
import 'package:island/accounts/account/me/settings_connections.dart';
import 'package:island/accounts/account/me/settings_contacts.dart';
import 'package:island/auth/captcha.dart';
import 'package:island/auth/login.dart';
import 'package:island/shared/widgets/alert.dart';
import 'package:island/shared/widgets/app_scaffold.dart';
import 'package:island/shared/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'account_settings.g.dart';
@riverpod
Future<List<SnAuthFactor>> authFactors(Ref ref) async {
final client = ref.read(apiClientProvider);
final res = await client.get('/pass/accounts/me/factors');
return res.data.map<SnAuthFactor>((e) => SnAuthFactor.fromJson(e)).toList();
}
@riverpod
Future<List<SnContactMethod>> contactMethods(Ref ref) async {
final client = ref.read(apiClientProvider);
final resp = await client.get('/pass/accounts/me/contacts');
return resp.data
.map<SnContactMethod>((e) => SnContactMethod.fromJson(e))
.toList();
}
@riverpod
Future<List<SnAccountConnection>> accountConnections(Ref ref) async {
final client = ref.read(apiClientProvider);
final resp = await client.get('/pass/accounts/me/connections');
return resp.data
.map<SnAccountConnection>((e) => SnAccountConnection.fromJson(e))
.toList();
}
class AccountSettingsScreen extends HookConsumerWidget {
const AccountSettingsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDesktop =
!kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
Future<void> requestAccountDeletion() async {
final confirm = await showConfirmAlert(
'accountDeletionHint'.tr(),
'accountDeletion'.tr(),
isDanger: true,
);
if (!confirm || !context.mounted) return;
try {
showLoadingModal(context);
final client = ref.read(apiClientProvider);
await client.delete('/pass/accounts/me');
if (context.mounted) {
showSnackBar('accountDeletionSent'.tr());
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> requestResetPassword() async {
final confirm = await showConfirmAlert(
'accountPasswordChangeDescription'.tr(),
'accountPasswordChange'.tr(),
);
if (!confirm || !context.mounted) return;
final captchaTk = await CaptchaScreen.show(context);
if (captchaTk == null) return;
try {
if (context.mounted) showLoadingModal(context);
final userInfo = ref.read(userInfoProvider);
final client = ref.read(apiClientProvider);
await client.post(
'/pass/accounts/recovery/password',
data: {'account': userInfo.value!.name, 'captcha_token': captchaTk},
);
if (context.mounted) {
showSnackBar('accountPasswordChangeSent'.tr());
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
final authFactors = ref.watch(authFactorsProvider);
// Group settings into categories for better organization
final securitySettings = [
ListTile(
minLeadingWidth: 48,
leading: const Icon(Symbols.devices),
title: Text('authSessions').tr(),
subtitle: Text('authSessionsDescription').tr().fontSize(12),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => const AccountSessionSheet(),
);
},
),
ExpansionTile(
leading: const Icon(
Symbols.link,
).alignment(Alignment.centerLeft).width(48),
title: Text('accountConnections').tr(),
subtitle: Text('accountConnectionsDescription').tr().fontSize(12),
tilePadding: const EdgeInsets.only(left: 24, right: 17),
children: [
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);
}
});
},
),
],
),
error: (err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(accountConnectionsProvider),
),
loading: () => const ResponseLoadingWidget(),
),
],
),
ExpansionTile(
leading: const Icon(
Symbols.security,
).alignment(Alignment.centerLeft).width(48),
title: Text('accountAuthFactor').tr(),
subtitle: Text('accountAuthFactorDescription').tr().fontSize(12),
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);
}
});
},
),
],
),
error: (err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(authFactorsProvider),
),
loading: () => ResponseLoadingWidget(),
),
],
),
ExpansionTile(
leading: const Icon(
Symbols.contact_mail,
).alignment(Alignment.centerLeft).width(48),
title: Text('accountContactMethod').tr(),
subtitle: Text('accountContactMethodDescription').tr().fontSize(12),
tilePadding: const EdgeInsets.only(left: 24, right: 17),
children: [
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);
}
});
},
),
],
),
error: (err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(contactMethodsProvider),
),
loading: () => const ResponseLoadingWidget(),
),
],
),
];
final dangerZoneSettings = [
ListTile(
minLeadingWidth: 48,
title: Text('accountDeletion').tr(),
subtitle: Text('accountDeletionDescription').tr().fontSize(12),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.delete_forever, color: Colors.red),
trailing: const Icon(Symbols.chevron_right),
onTap: requestAccountDeletion,
),
];
// Create a responsive layout based on screen width
Widget buildSettingsList() {
return Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'accountSecurityTitle',
children: securitySettings,
),
_SettingsSection(
title: 'accountDangerZoneTitle',
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,
),
body: Focus(
autofocus: true,
onKeyEvent: (node, event) {
// Add keyboard shortcuts for desktop
if (isDesktop &&
event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
Navigator.of(context).pop();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16),
child: buildSettingsList(),
),
),
);
}
}
// Helper widget for displaying settings sections with titles
class _SettingsSection extends StatelessWidget {
final String title;
final List<Widget> children;
const _SettingsSection({required this.title, required this.children});
@override
Widget build(BuildContext context) {
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),
],
),
);
}
}

View File

@@ -0,0 +1,134 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'account_settings.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(authFactors)
final authFactorsProvider = AuthFactorsProvider._();
final class AuthFactorsProvider
extends
$FunctionalProvider<
AsyncValue<List<SnAuthFactor>>,
List<SnAuthFactor>,
FutureOr<List<SnAuthFactor>>
>
with
$FutureModifier<List<SnAuthFactor>>,
$FutureProvider<List<SnAuthFactor>> {
AuthFactorsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'authFactorsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$authFactorsHash();
@$internal
@override
$FutureProviderElement<List<SnAuthFactor>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<SnAuthFactor>> create(Ref ref) {
return authFactors(ref);
}
}
String _$authFactorsHash() => r'ed87d7dbd421fef0a5620416727c3dc598c97ef5';
@ProviderFor(contactMethods)
final contactMethodsProvider = ContactMethodsProvider._();
final class ContactMethodsProvider
extends
$FunctionalProvider<
AsyncValue<List<SnContactMethod>>,
List<SnContactMethod>,
FutureOr<List<SnContactMethod>>
>
with
$FutureModifier<List<SnContactMethod>>,
$FutureProvider<List<SnContactMethod>> {
ContactMethodsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'contactMethodsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$contactMethodsHash();
@$internal
@override
$FutureProviderElement<List<SnContactMethod>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<SnContactMethod>> create(Ref ref) {
return contactMethods(ref);
}
}
String _$contactMethodsHash() => r'1d3d03e9ffbf36126236558ead22cb7d88bb9cb2';
@ProviderFor(accountConnections)
final accountConnectionsProvider = AccountConnectionsProvider._();
final class AccountConnectionsProvider
extends
$FunctionalProvider<
AsyncValue<List<SnAccountConnection>>,
List<SnAccountConnection>,
FutureOr<List<SnAccountConnection>>
>
with
$FutureModifier<List<SnAccountConnection>>,
$FutureProvider<List<SnAccountConnection>> {
AccountConnectionsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'accountConnectionsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$accountConnectionsHash();
@$internal
@override
$FutureProviderElement<List<SnAccountConnection>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<SnAccountConnection>> create(Ref ref) {
return accountConnections(ref);
}
}
String _$accountConnectionsHash() =>
r'33c10b98962ede6c428d4028c0d5f2f12ff0eb22';

View File

@@ -7,17 +7,17 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/services/timezone.dart';
import 'package:island/widgets/account/account_name.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/accounts/accounts_widgets/account/account_name.dart';
import 'package:island/core/services/image.dart';
import 'package:island/drive/drive_models/file.dart';
import 'package:island/accounts/accounts_models/account.dart';
import 'package:island/core/network.dart';
import 'package:island/accounts/accounts_pod.dart';
import 'package:island/drive/drive_service.dart';
import 'package:island/core/services/timezone.dart';
import 'package:island/shared/widgets/alert.dart';
import 'package:island/shared/widgets/app_scaffold.dart';
import 'package:island/drive/drive_widgets/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -74,13 +74,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
submitting.value = true;
try {
final cloudFile =
await FileUploader.createCloudFile(
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
fileData: UniversalFile(data: result, type: UniversalFileType.image),
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
@@ -188,8 +184,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
if (usernameColorType.value == 'gradient') ...{
if (usernameColorDirection.text.isNotEmpty)
'direction': usernameColorDirection.text,
'colors':
usernameColorColors.value.where((c) => c.isNotEmpty).toList(),
'colors': usernameColorColors.value
.where((c) => c.isNotEmpty)
.toList(),
},
};
@@ -206,16 +203,14 @@ class UpdateProfileScreen extends HookConsumerWidget {
'time_zone': timeZoneController.text,
'birthday': birthday.value?.toUtc().toIso8601String(),
'username_color': usernameColorData,
'links':
links.value
'links': links.value
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
.toList(),
},
);
final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser();
links.value =
links.value
links.value = links.value
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
.toList();
} catch (err) {
@@ -244,10 +239,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
GestureDetector(
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child:
user.value!.profile.background?.id != null
child: user.value!.profile.background != null
? CloudImageWidget(
fileId: user.value!.profile.background!.id,
file: user.value!.profile.background,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
@@ -261,7 +255,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: user.value!.profile.picture?.id,
file: user.value!.profile.picture,
radius: 40,
),
onTap: () {
@@ -291,14 +285,14 @@ class UpdateProfileScreen extends HookConsumerWidget {
),
controller: usernameController,
readOnly: true,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
TextFormField(
decoration: InputDecoration(labelText: 'nickname'.tr()),
controller: nicknameController,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
DropdownButtonFormField2<String>(
decoration: InputDecoration(
@@ -385,8 +379,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
labelText: 'firstName'.tr(),
),
controller: firstNameController,
onTapOutside:
(_) =>
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
@@ -396,8 +389,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
labelText: 'middleName'.tr(),
),
controller: middleNameController,
onTapOutside:
(_) =>
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
@@ -407,8 +399,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
labelText: 'lastName'.tr(),
),
controller: lastNameController,
onTapOutside:
(_) =>
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
@@ -423,8 +414,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
maxLines: null,
minLines: 3,
controller: bioController,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
Row(
spacing: 16,
@@ -445,7 +436,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
onSelected: (String selection) {
genderController.text = selection;
},
fieldViewBuilder: (
fieldViewBuilder:
(
context,
controller,
focusNode,
@@ -466,9 +458,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
onChanged: (value) {
genderController.text = value;
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
onTapOutside: (_) => FocusManager
.instance
.primaryFocus
?.unfocus(),
);
},
@@ -480,8 +472,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
labelText: 'pronouns'.tr(),
),
controller: pronounsController,
onTapOutside:
(_) =>
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
@@ -496,8 +487,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
labelText: 'location'.tr(),
),
controller: locationController,
onTapOutside:
(_) =>
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
@@ -507,8 +497,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
if (textEditingValue.text.isEmpty) {
return const Iterable<String>.empty();
}
final lowercaseQuery =
textEditingValue.text.toLowerCase();
final lowercaseQuery = textEditingValue.text
.toLowerCase();
return getAvailableTz().where((tz) {
return tz.toLowerCase().contains(lowercaseQuery);
});
@@ -516,14 +506,16 @@ class UpdateProfileScreen extends HookConsumerWidget {
onSelected: (String selection) {
timeZoneController.text = selection;
},
fieldViewBuilder: (
fieldViewBuilder:
(
context,
controller,
focusNode,
onFieldSubmitted,
) {
// Sync the controller with timeZoneController when the widget is built
if (controller.text != timeZoneController.text) {
if (controller.text !=
timeZoneController.text) {
controller.text = timeZoneController.text;
}
@@ -540,7 +532,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
onTap: () async {
try {
showLoadingModal(context);
final machineTz = await getMachineTz();
final machineTz =
await getMachineTz();
controller.text = machineTz;
timeZoneController.text = machineTz;
} finally {
@@ -569,11 +562,11 @@ class UpdateProfileScreen extends HookConsumerWidget {
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: options.length,
itemBuilder: (
BuildContext context,
int index,
) {
final option = options.elementAt(index);
itemBuilder:
(BuildContext context, int index) {
final option = options.elementAt(
index,
);
return ListTile(
title: Text(
option,
@@ -644,8 +637,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color:
Theme.of(
color: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
@@ -664,9 +656,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
type: usernameColorType.value,
value:
usernameColorType.value == 'plain' &&
usernameColorValue
.text
.isNotEmpty
usernameColorValue.text.isNotEmpty
? usernameColorValue.text
: null,
direction:
@@ -724,8 +714,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
? Symbols.check_circle
: Symbols.error,
size: 16,
color:
canUseColor
color: canUseColor
? Colors.green
: Colors.red,
),
@@ -736,8 +725,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
: 'upgradeRequired'.tr(),
style: TextStyle(
fontSize: 12,
color:
canUseColor
color: canUseColor
? Colors.green
: Colors.red,
),
@@ -792,7 +780,8 @@ class UpdateProfileScreen extends HookConsumerWidget {
onSelected: (String selection) {
usernameColorValue.text = selection;
},
fieldViewBuilder: (
fieldViewBuilder:
(
context,
controller,
focusNode,
@@ -814,9 +803,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
onChanged: (value) {
usernameColorValue.text = value;
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
onTapOutside: (_) => FocusManager
.instance
.primaryFocus
?.unfocus(),
);
},
@@ -862,8 +851,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
child: Text('gradientDirectionToTopLeft'.tr()),
),
],
value:
usernameColorDirection.text.isNotEmpty
value: usernameColorDirection.text.isNotEmpty
? usernameColorDirection.text
: 'to right',
onChanged: (value) {
@@ -911,20 +899,18 @@ class UpdateProfileScreen extends HookConsumerWidget {
onChanged: (value) {
usernameColorColors.value[i] = value;
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
onTapOutside: (_) => FocusManager
.instance
.primaryFocus
?.unfocus(),
),
),
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
usernameColorColors.value =
usernameColorColors.value
.whereIndexed(
(idx, _) => idx != i,
)
usernameColorColors
.value = usernameColorColors.value
.whereIndexed((idx, _) => idx != i)
.toList();
},
),
@@ -968,9 +954,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
name: value,
);
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
onTapOutside: (_) => FocusManager
.instance
.primaryFocus
?.unfocus(),
),
),
@@ -987,17 +973,16 @@ class UpdateProfileScreen extends HookConsumerWidget {
url: value,
);
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
onTapOutside: (_) => FocusManager
.instance
.primaryFocus
?.unfocus(),
),
),
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
links.value =
links.value
links.value = links.value
.whereIndexed((idx, _) => idx != i)
.toList();
},

View File

@@ -7,11 +7,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/auth.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/auth/login.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/auth/auth_models/auth.dart';
import 'package:island/core/network.dart';
import 'package:island/auth/login.dart';
import 'package:island/shared/widgets/alert.dart';
import 'package:island/drive/content/sheet_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -64,8 +64,7 @@ class AuthFactorSheet extends HookConsumerWidget {
if ([3].contains(factor.type)) {
final confirmed = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
builder: (context) => AlertDialog(
title: Text('authFactorEnable').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
@@ -233,8 +232,7 @@ class AuthFactorNewSheet extends HookConsumerWidget {
labelText: 'authFactor'.tr(),
border: const OutlineInputBorder(),
),
items:
kFactorTypes.entries.map((entry) {
items: kFactorTypes.entries.map((entry) {
return DropdownMenuItem<int>(
value: entry.key,
child: Row(
@@ -261,8 +259,8 @@ class AuthFactorNewSheet extends HookConsumerWidget {
hintText: 'authFactorSecretHint'.tr(),
border: const OutlineInputBorder(),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
)
else if ([4].contains(factorType.value))
OtpTextField(

View File

@@ -4,15 +4,15 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/auth.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/account/me/account_settings.dart';
import 'package:island/utils/text.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:island/auth/auth_models/auth.dart';
import 'package:island/core/config.dart';
import 'package:island/core/network.dart';
import 'package:island/accounts/account/me/account_settings.dart';
import 'package:island/core/text.dart';
import 'package:island/core/services/time.dart';
import 'package:island/shared/widgets/alert.dart';
import 'package:island/drive/content/sheet_scaffold.dart';
import 'package:island/shared/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -233,14 +233,11 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
labelText: 'accountConnectionProvider'.tr(),
border: const OutlineInputBorder(),
),
items:
providers.map((String provider) {
items: providers.map((String provider) {
return DropdownMenuItem<String>(
value: provider,
child: Row(
children: [
Text(getLocalizedProviderName(provider)).tr(),
],
children: [Text(getLocalizedProviderName(provider)).tr()],
),
);
}).toList(),
@@ -296,14 +293,10 @@ class AccountConnectionsSheet extends HookConsumerWidget {
),
],
child: connections.when(
data:
(data) => RefreshIndicator(
onRefresh:
() => Future.sync(
() => ref.invalidate(accountConnectionsProvider),
),
child:
data.isEmpty
data: (data) => RefreshIndicator(
onRefresh: () =>
Future.sync(() => ref.invalidate(accountConnectionsProvider)),
child: data.isEmpty
? Center(
child: Text(
'accountConnectionsEmpty'.tr(),
@@ -321,13 +314,8 @@ class AccountConnectionsSheet extends HookConsumerWidget {
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
child: const Icon(
Icons.delete,
color: Colors.white,
),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (direction) async {
final confirm = await showConfirmAlert(
@@ -355,14 +343,10 @@ class AccountConnectionsSheet extends HookConsumerWidget {
connection.provider,
color: Theme.of(context).colorScheme.onSurface,
),
title:
Text(
getLocalizedProviderName(
connection.provider,
),
title: Text(
getLocalizedProviderName(connection.provider),
).tr(),
subtitle:
connection.meta['email'] != null
subtitle: connection.meta['email'] != null
? Text(connection.meta['email'])
: Text(connection.providedIdentifier),
trailing: Text(
@@ -375,10 +359,8 @@ class AccountConnectionsSheet extends HookConsumerWidget {
final result = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
builder:
(context) => AccountConnectionSheet(
connection: connection,
),
builder: (context) =>
AccountConnectionSheet(connection: connection),
);
if (result == true) {
ref.invalidate(accountConnectionsProvider);
@@ -389,8 +371,7 @@ class AccountConnectionsSheet extends HookConsumerWidget {
},
),
),
error:
(err, _) => ResponseErrorWidget(
error: (err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(accountConnectionsProvider),
),

View File

@@ -3,10 +3,10 @@ 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/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/accounts/accounts_models/account.dart';
import 'package:island/core/network.dart';
import 'package:island/shared/widgets/alert.dart';
import 'package:island/drive/content/sheet_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -153,8 +153,7 @@ class ContactMethodSheet extends HookConsumerWidget {
child: Badge(
label: Text('contactMethodPrivate'.tr()),
textColor: Theme.of(context).colorScheme.onSurface,
backgroundColor:
Theme.of(
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
@@ -319,8 +318,7 @@ class ContactMethodNewSheet extends HookConsumerWidget {
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child:
Text(switch (contactType.value) {
child: Text(switch (contactType.value) {
0 => 'contactMethodEmailDescription',
1 => 'contactMethodPhoneDescription',
_ => 'contactMethodAddressDescription',

View File

@@ -7,33 +7,33 @@ 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/chat.dart';
import 'package:island/models/developer.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/relationship.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/event_calendar.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/color.dart';
import 'package:island/services/responsive.dart';
import 'package:island/utils/text.dart';
import 'package:island/services/time.dart';
import 'package:island/services/timezone/native.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/activity_presence.dart';
import 'package:island/widgets/account/badge.dart';
import 'package:island/widgets/account/fortune_graph.dart';
import 'package:island/widgets/account/leveling_progress.dart';
import 'package:island/widgets/account/status.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/markdown.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart';
import 'package:island/accounts/accounts_widgets/account/account_name.dart';
import 'package:island/accounts/accounts_widgets/account/activity_presence.dart';
import 'package:island/accounts/accounts_widgets/account/badge.dart';
import 'package:island/accounts/accounts_widgets/account/fortune_graph.dart';
import 'package:island/accounts/accounts_widgets/account/leveling_progress.dart';
import 'package:island/accounts/accounts_widgets/account/status.dart';
import 'package:island/chat/chat_models/chat.dart';
import 'package:island/developers/developers_models/developer.dart';
import 'package:island/posts/posts_models/publisher.dart';
import 'package:island/accounts/accounts_models/relationship.dart';
import 'package:island/accounts/accounts_models/account.dart';
import 'package:island/core/config.dart';
import 'package:island/accounts/event_calendar.dart';
import 'package:island/core/network.dart';
import 'package:island/accounts/accounts_pod.dart';
import 'package:island/core/services/color.dart';
import 'package:island/core/services/responsive.dart';
import 'package:island/core/text.dart';
import 'package:island/core/services/time.dart';
import 'package:island/core/services/timezone/native.dart';
import 'package:island/shared/widgets/alert.dart';
import 'package:island/shared/widgets/app_scaffold.dart';
import 'package:island/drive/drive_widgets/cloud_files.dart';
import 'package:island/drive/content/markdown.dart';
import 'package:island/reports/reports_widgets/safety/abuse_report_helper.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:island/services/color_extraction.dart';
import 'package:island/core/services/color_extraction.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:share_plus/share_plus.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -57,7 +57,7 @@ class _AccountBasicInfo extends StatelessWidget {
return Card(
child: Builder(
builder: (context) {
final hasBackground = data.profile.background?.id != null;
final hasBackground = data.profile.background != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -550,14 +550,12 @@ class _AccountAction extends StatelessWidget {
),
),
onPressed: relationshipAction,
label:
Text(
label: Text(
accountRelationship.value == null
? 'addFriendShort'
: 'added',
).tr(),
icon:
accountRelationship.value == null
icon: accountRelationship.value == null
? const Icon(Symbols.person_add)
: const Icon(Symbols.person_check),
),
@@ -579,14 +577,12 @@ class _AccountAction extends StatelessWidget {
),
),
onPressed: blockAction,
label:
Text(
label: Text(
accountRelationship.value == null
? 'blockUser'
: 'unblockUser',
).tr(),
icon:
accountRelationship.value == null
icon: accountRelationship.value == null
? const Icon(Symbols.block)
: const Icon(Symbols.person_cancel),
),
@@ -600,8 +596,7 @@ class _AccountAction extends StatelessWidget {
child: FilledButton.icon(
onPressed: directMessageAction,
icon: const Icon(Symbols.message),
label:
Text(
label: Text(
accountChat.value == null
? 'createDirectMessage'
: 'gotoDirectMessage',
@@ -664,7 +659,7 @@ Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
if (account.profile.background == null) return null;
final colors = await ColorExtractionService.getColorsFromImage(
CloudImageWidget.provider(
fileId: account.profile.background!.id,
file: account.profile.background!,
serverUrl: ref.watch(serverUrlProvider),
),
);
@@ -683,7 +678,7 @@ Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
final account = await ref.watch(accountProvider(uname).future);
final apiClient = ref.watch(apiClientProvider);
try {
final resp = await apiClient.get("/sphere/chat/direct/${account.id}");
final resp = await apiClient.get("/messager/chat/direct/${account.id}");
return SnChatRoom.fromJson(resp.data);
} catch (err) {
if (err is DioException && err.response?.statusCode == 404) {
@@ -812,7 +807,7 @@ class AccountProfileScreen extends HookConsumerWidget {
try {
final client = ref.watch(apiClientProvider);
final resp = await client.post(
'/sphere/chat/direct',
'/messager/chat/direct',
data: {'related_user_id': account.value!.id},
);
final chat = SnChatRoom.fromJson(resp.data);
@@ -838,8 +833,7 @@ class AccountProfileScreen extends HookConsumerWidget {
final accountPublishers = ref.watch(accountPublishersProvider(data.id));
return AppScaffold(
isNoBackground: false,
appBar:
isWideScreen(context)
appBar: isWideScreen(context)
? AppBar(
foregroundColor: appbarColor.value,
leading: PageBackButton(
@@ -857,8 +851,7 @@ class AccountProfileScreen extends HookConsumerWidget {
),
)
: null,
body:
isWideScreen(context)
body: isWideScreen(context)
? Row(
children: [
Flexible(
@@ -969,14 +962,12 @@ class AccountProfileScreen extends HookConsumerWidget {
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.profile.background?.id != null
child: data.profile.background != null
? CloudImageWidget(
file: data.profile.background,
)
: Container(
color:
Theme.of(
color: Theme.of(
context,
).appBarTheme.backgroundColor,
),
@@ -1084,13 +1075,11 @@ class AccountProfileScreen extends HookConsumerWidget {
),
);
},
error:
(error, stackTrace) => AppScaffold(
error: (error, stackTrace) => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Center(child: Text(error.toString())),
),
loading:
() => AppScaffold(
loading: () => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Center(child: CircularProgressIndicator()),
),

View File

@@ -0,0 +1,537 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'profile.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(account)
final accountProvider = AccountFamily._();
final class AccountProvider
extends
$FunctionalProvider<
AsyncValue<SnAccount>,
SnAccount,
FutureOr<SnAccount>
>
with $FutureModifier<SnAccount>, $FutureProvider<SnAccount> {
AccountProvider._({
required AccountFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'accountProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$accountHash();
@override
String toString() {
return r'accountProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<SnAccount> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<SnAccount> create(Ref ref) {
final argument = this.argument as String;
return account(ref, argument);
}
@override
bool operator ==(Object other) {
return other is AccountProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$accountHash() => r'5e2b7bd59151b4638a5561f495537c259f767123';
final class AccountFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<SnAccount>, String> {
AccountFamily._()
: super(
retry: null,
name: r'accountProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
AccountProvider call(String uname) =>
AccountProvider._(argument: uname, from: this);
@override
String toString() => r'accountProvider';
}
@ProviderFor(accountBadges)
final accountBadgesProvider = AccountBadgesFamily._();
final class AccountBadgesProvider
extends
$FunctionalProvider<
AsyncValue<List<SnAccountBadge>>,
List<SnAccountBadge>,
FutureOr<List<SnAccountBadge>>
>
with
$FutureModifier<List<SnAccountBadge>>,
$FutureProvider<List<SnAccountBadge>> {
AccountBadgesProvider._({
required AccountBadgesFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'accountBadgesProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$accountBadgesHash();
@override
String toString() {
return r'accountBadgesProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<List<SnAccountBadge>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<SnAccountBadge>> create(Ref ref) {
final argument = this.argument as String;
return accountBadges(ref, argument);
}
@override
bool operator ==(Object other) {
return other is AccountBadgesProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$accountBadgesHash() => r'68db63f49827020beecbdbf20529520d0cd14a7d';
final class AccountBadgesFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<List<SnAccountBadge>>, String> {
AccountBadgesFamily._()
: super(
retry: null,
name: r'accountBadgesProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
AccountBadgesProvider call(String uname) =>
AccountBadgesProvider._(argument: uname, from: this);
@override
String toString() => r'accountBadgesProvider';
}
@ProviderFor(accountAppbarForcegroundColor)
final accountAppbarForcegroundColorProvider =
AccountAppbarForcegroundColorFamily._();
final class AccountAppbarForcegroundColorProvider
extends $FunctionalProvider<AsyncValue<Color?>, Color?, FutureOr<Color?>>
with $FutureModifier<Color?>, $FutureProvider<Color?> {
AccountAppbarForcegroundColorProvider._({
required AccountAppbarForcegroundColorFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'accountAppbarForcegroundColorProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$accountAppbarForcegroundColorHash();
@override
String toString() {
return r'accountAppbarForcegroundColorProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<Color?> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<Color?> create(Ref ref) {
final argument = this.argument as String;
return accountAppbarForcegroundColor(ref, argument);
}
@override
bool operator ==(Object other) {
return other is AccountAppbarForcegroundColorProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$accountAppbarForcegroundColorHash() =>
r'59e0049a5158ea653f0afd724df9ff2312b90050';
final class AccountAppbarForcegroundColorFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<Color?>, String> {
AccountAppbarForcegroundColorFamily._()
: super(
retry: null,
name: r'accountAppbarForcegroundColorProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
AccountAppbarForcegroundColorProvider call(String uname) =>
AccountAppbarForcegroundColorProvider._(argument: uname, from: this);
@override
String toString() => r'accountAppbarForcegroundColorProvider';
}
@ProviderFor(accountDirectChat)
final accountDirectChatProvider = AccountDirectChatFamily._();
final class AccountDirectChatProvider
extends
$FunctionalProvider<
AsyncValue<SnChatRoom?>,
SnChatRoom?,
FutureOr<SnChatRoom?>
>
with $FutureModifier<SnChatRoom?>, $FutureProvider<SnChatRoom?> {
AccountDirectChatProvider._({
required AccountDirectChatFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'accountDirectChatProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$accountDirectChatHash();
@override
String toString() {
return r'accountDirectChatProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<SnChatRoom?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SnChatRoom?> create(Ref ref) {
final argument = this.argument as String;
return accountDirectChat(ref, argument);
}
@override
bool operator ==(Object other) {
return other is AccountDirectChatProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$accountDirectChatHash() => r'71bc9eed34a436a3743e8ef87f7aaae861fc5746';
final class AccountDirectChatFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<SnChatRoom?>, String> {
AccountDirectChatFamily._()
: super(
retry: null,
name: r'accountDirectChatProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
AccountDirectChatProvider call(String uname) =>
AccountDirectChatProvider._(argument: uname, from: this);
@override
String toString() => r'accountDirectChatProvider';
}
@ProviderFor(accountRelationship)
final accountRelationshipProvider = AccountRelationshipFamily._();
final class AccountRelationshipProvider
extends
$FunctionalProvider<
AsyncValue<SnRelationship?>,
SnRelationship?,
FutureOr<SnRelationship?>
>
with $FutureModifier<SnRelationship?>, $FutureProvider<SnRelationship?> {
AccountRelationshipProvider._({
required AccountRelationshipFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'accountRelationshipProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$accountRelationshipHash();
@override
String toString() {
return r'accountRelationshipProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<SnRelationship?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SnRelationship?> create(Ref ref) {
final argument = this.argument as String;
return accountRelationship(ref, argument);
}
@override
bool operator ==(Object other) {
return other is AccountRelationshipProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$accountRelationshipHash() =>
r'319f743261b113a1d3c6a397d48d13c858312669';
final class AccountRelationshipFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<SnRelationship?>, String> {
AccountRelationshipFamily._()
: super(
retry: null,
name: r'accountRelationshipProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
AccountRelationshipProvider call(String uname) =>
AccountRelationshipProvider._(argument: uname, from: this);
@override
String toString() => r'accountRelationshipProvider';
}
@ProviderFor(accountBotDeveloper)
final accountBotDeveloperProvider = AccountBotDeveloperFamily._();
final class AccountBotDeveloperProvider
extends
$FunctionalProvider<
AsyncValue<SnDeveloper?>,
SnDeveloper?,
FutureOr<SnDeveloper?>
>
with $FutureModifier<SnDeveloper?>, $FutureProvider<SnDeveloper?> {
AccountBotDeveloperProvider._({
required AccountBotDeveloperFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'accountBotDeveloperProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$accountBotDeveloperHash();
@override
String toString() {
return r'accountBotDeveloperProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<SnDeveloper?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SnDeveloper?> create(Ref ref) {
final argument = this.argument as String;
return accountBotDeveloper(ref, argument);
}
@override
bool operator ==(Object other) {
return other is AccountBotDeveloperProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$accountBotDeveloperHash() =>
r'673534770640a8cf1484ea0af0f4d0ef283ef157';
final class AccountBotDeveloperFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<SnDeveloper?>, String> {
AccountBotDeveloperFamily._()
: super(
retry: null,
name: r'accountBotDeveloperProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
AccountBotDeveloperProvider call(String uname) =>
AccountBotDeveloperProvider._(argument: uname, from: this);
@override
String toString() => r'accountBotDeveloperProvider';
}
@ProviderFor(accountPublishers)
final accountPublishersProvider = AccountPublishersFamily._();
final class AccountPublishersProvider
extends
$FunctionalProvider<
AsyncValue<List<SnPublisher>>,
List<SnPublisher>,
FutureOr<List<SnPublisher>>
>
with
$FutureModifier<List<SnPublisher>>,
$FutureProvider<List<SnPublisher>> {
AccountPublishersProvider._({
required AccountPublishersFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'accountPublishersProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$accountPublishersHash();
@override
String toString() {
return r'accountPublishersProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<List<SnPublisher>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<SnPublisher>> create(Ref ref) {
final argument = this.argument as String;
return accountPublishers(ref, argument);
}
@override
bool operator ==(Object other) {
return other is AccountPublishersProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$accountPublishersHash() => r'25f5695b4a5154163d77f1769876d826bf736609';
final class AccountPublishersFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<List<SnPublisher>>, String> {
AccountPublishersFamily._()
: super(
retry: null,
name: r'accountPublishersProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
AccountPublishersProvider call(String id) =>
AccountPublishersProvider._(argument: id, from: this);
@override
String toString() => r'accountPublishersProvider';
}

View File

@@ -1,25 +1,29 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/accounts/accounts_widgets/account/account_pfc.dart';
import 'package:island/accounts/accounts_widgets/account/account_picker.dart';
import 'package:island/pagination/pagination.dart';
import 'package:island/accounts/accounts_pod.dart';
import 'package:island/shared/widgets/alert.dart';
import 'package:island/shared/widgets/app_scaffold.dart';
import 'package:island/drive/drive_widgets/cloud_files.dart';
import 'package:island/shared/widgets/pagination_list.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/models/relationship.dart';
import 'package:island/pods/network.dart';
import 'package:island/accounts/accounts_models/relationship.dart';
import 'package:island/core/network.dart';
part 'relationship.g.dart';
@riverpod
Future<List<SnRelationship>> sentFriendRequest(Ref ref) async {
Future<List<SnRelationship>> friendRequest(Ref ref) async {
final client = ref.read(apiClientProvider);
final resp = await client.get('/pass/relationships/requests');
return resp.data
@@ -28,39 +32,44 @@ Future<List<SnRelationship>> sentFriendRequest(Ref ref) async {
.toList();
}
@riverpod
class RelationshipListNotifier extends _$RelationshipListNotifier
with CursorPagingNotifierMixin<SnRelationship> {
final relationshipListNotifierProvider = AsyncNotifierProvider.autoDispose(
RelationshipListNotifier.new,
);
class RelationshipListNotifier
extends AsyncNotifier<PaginationState<SnRelationship>>
with AsyncPaginationController<SnRelationship> {
@override
Future<CursorPagingData<SnRelationship>> build() => fetch(cursor: null);
FutureOr<PaginationState<SnRelationship>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<CursorPagingData<SnRelationship>> fetch({
required String? cursor,
}) async {
Future<List<SnRelationship>> fetch() async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final take = 20;
final response = await client.get(
'/pass/relationships',
queryParameters: {'offset': offset, 'take': take},
queryParameters: {'offset': fetchedCount.toString(), 'take': take},
);
final List<SnRelationship> items =
(response.data as List)
final List<SnRelationship> items = (response.data as List)
.map((e) => SnRelationship.fromJson(e as Map<String, dynamic>))
.cast<SnRelationship>()
.toList();
final total = int.tryParse(response.headers['x-total']?.first ?? '') ?? 0;
final hasMore = offset + items.length < total;
final nextCursor = hasMore ? (offset + items.length).toString() : null;
totalCount = int.tryParse(response.headers['x-total']?.first ?? '') ?? 0;
return CursorPagingData(
items: items,
hasMore: hasMore,
nextCursor: nextCursor,
);
return items;
}
}
@@ -74,6 +83,7 @@ class RelationshipListTile extends StatelessWidget {
final String? currentUserId;
final bool showRelatedAccount;
final Function(SnRelationship, int)? onUpdateStatus;
final Function(SnRelationship)? onDelete;
const RelationshipListTile({
super.key,
@@ -86,23 +96,25 @@ class RelationshipListTile extends StatelessWidget {
required this.currentUserId,
this.showRelatedAccount = false,
this.onUpdateStatus,
this.onDelete,
});
@override
Widget build(BuildContext context) {
final account =
showRelatedAccount ? relationship.related : relationship.account;
final account = showRelatedAccount
? relationship.related!
: relationship.account!;
final isPending =
relationship.status == 0 && relationship.relatedId == currentUserId;
final isWaiting =
relationship.status == 0 && relationship.accountId == currentUserId;
final isEstablished = relationship.status == 1 || relationship.status == 2;
final isEstablished = relationship.status != 0;
return ListTile(
contentPadding: const EdgeInsets.only(left: 16, right: 12),
leading: AccountPfcGestureDetector(
leading: AccountPfcRegion(
uname: account.name,
child: ProfilePictureWidget(fileId: account.profile.picture?.id),
child: ProfilePictureWidget(file: account.profile.picture),
),
title: Row(
spacing: 6,
@@ -145,8 +157,7 @@ class RelationshipListTile extends StatelessWidget {
],
),
subtitle: Text('@${account.name}'),
trailing:
showActions
trailing: showActions
? Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -172,31 +183,39 @@ class RelationshipListTile extends StatelessWidget {
PopupMenuButton(
padding: EdgeInsets.zero,
icon: const Icon(Symbols.more_vert),
itemBuilder:
(context) => [
itemBuilder: (context) => [
if (relationship.status >= 100) // If friend
PopupMenuItem(
child: ListTile(
leading: const Icon(Symbols.block),
title: Text('blockUser').tr(),
contentPadding: EdgeInsets.zero,
),
onTap:
() => onUpdateStatus?.call(
relationship,
-100,
onTap: () => onUpdateStatus?.call(relationship, -100),
child: Row(
children: [
const Icon(Symbols.block),
const Gap(12),
Text('blockUser').tr(),
],
),
)
else if (relationship.status <= -100) // If blocked
PopupMenuItem(
child: ListTile(
leading: const Icon(Symbols.person_add),
title: Text('unblockUser').tr(),
contentPadding: EdgeInsets.zero,
onTap: () => onUpdateStatus?.call(relationship, 100),
child: Row(
children: [
const Icon(Symbols.person_add),
const Gap(12),
Text('unblockUser').tr(),
],
),
),
if (onDelete != null)
PopupMenuItem(
onTap: () => onDelete?.call(relationship),
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(12),
Text('forgotRelationship').tr(),
],
),
onTap:
() =>
onUpdateStatus?.call(relationship, 100),
),
],
),
@@ -227,40 +246,11 @@ class RelationshipScreen extends HookConsumerWidget {
final client = ref.read(apiClientProvider);
await client.post('/pass/relationships/${result.id}/friends');
ref.invalidate(sentFriendRequestProvider);
ref.invalidate(friendRequestProvider);
}
final submitting = useState(false);
Future<void> handleFriendRequest(
SnRelationship relationship,
bool isAccept,
) async {
try {
submitting.value = true;
final client = ref.read(apiClientProvider);
await client.post(
'/pass/relationships/${relationship.accountId}/friends/${isAccept ? 'accept' : 'decline'}',
);
relationshipNotifier.forceRefresh();
if (!context.mounted) return;
if (isAccept) {
showSnackBar(
'friendRequestAccepted'.tr(args: ['@${relationship.account.name}']),
);
} else {
showSnackBar(
'friendRequestDeclined'.tr(args: ['@${relationship.account.name}']),
);
}
HapticFeedback.lightImpact();
} catch (err) {
showErrorAlert(err);
} finally {
submitting.value = false;
}
}
Future<void> updateRelationship(
SnRelationship relationship,
int newStatus,
@@ -270,11 +260,35 @@ class RelationshipScreen extends HookConsumerWidget {
'/pass/relationships/${relationship.accountId}',
data: {'status': newStatus},
);
relationshipNotifier.forceRefresh();
relationshipNotifier.refresh();
}
Future<void> deleteRelationship(SnRelationship relationship) async {
final confirmed = await showConfirmAlert(
'forgotRelationshipConfirm'.tr(
args: ['@${relationship.related!.name}'],
),
'forgotRelationship'.tr(),
isDanger: true,
);
if (!confirmed) return;
if (!context.mounted) return;
showLoadingModal(context);
try {
final client = ref.read(apiClientProvider);
await client.delete('/pass/relationships/${relationship.relatedId}');
relationshipNotifier.refresh();
showSnackBar('relationshipDeleted'.tr());
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
final user = ref.watch(userInfoProvider);
final requests = ref.watch(sentFriendRequestProvider);
final requests = ref.watch(friendRequestProvider);
return AppScaffold(
appBar: AppBar(title: Text('relationships').tr()),
@@ -290,9 +304,9 @@ class RelationshipScreen extends HookConsumerWidget {
if (requests.hasValue && requests.value!.isNotEmpty)
ListTile(
leading: const Icon(Symbols.send),
title: Text('friendSentRequest').tr(),
title: Text('friendRequests').tr(),
subtitle: Text(
'friendSentRequestHint'.plural(requests.value!.length),
'friendRequestsHint'.plural(requests.value!.length),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () {
@@ -305,34 +319,22 @@ class RelationshipScreen extends HookConsumerWidget {
),
const Divider(height: 1),
Expanded(
child: PagingHelperView(
provider: relationshipListNotifierProvider,
futureRefreshable: relationshipListNotifierProvider.future,
notifierRefreshable: relationshipListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
child: PaginationList(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final relationship = data.items[index];
provider: relationshipListNotifierProvider,
notifier: relationshipListNotifierProvider.notifier,
itemBuilder: (context, index, relationship) {
return RelationshipListTile(
relationship: relationship,
submitting: submitting.value,
onAccept: () => handleFriendRequest(relationship, true),
onDecline:
() => handleFriendRequest(relationship, false),
currentUserId: user.value?.id,
showRelatedAccount: false,
showRelatedAccount: true,
onUpdateStatus: updateRelationship,
onDelete: deleteRelationship,
);
},
),
),
),
],
),
);
@@ -344,19 +346,54 @@ class _SentFriendRequestsSheet extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final requests = ref.watch(sentFriendRequestProvider);
final requests = ref.watch(friendRequestProvider);
final user = ref.watch(userInfoProvider);
Future<void> cancelRequest(SnRelationship request) async {
try {
final client = ref.read(apiClientProvider);
await client.delete('/pass/relationships/${request.relatedId}/friends');
ref.invalidate(sentFriendRequestProvider);
ref.invalidate(friendRequestProvider);
} catch (err) {
showErrorAlert(err);
}
}
final submitting = useState(false);
Future<void> handleFriendRequest(
SnRelationship relationship,
bool isAccept,
) async {
try {
submitting.value = true;
final client = ref.read(apiClientProvider);
await client.post(
'/pass/relationships/${relationship.accountId}/friends/${isAccept ? 'accept' : 'decline'}',
);
ref.invalidate(friendRequestProvider);
if (!context.mounted) return;
if (isAccept) {
showSnackBar(
'friendRequestAccepted'.tr(
args: ['@${relationship.account!.name}'],
),
);
} else {
showSnackBar(
'friendRequestDeclined'.tr(
args: ['@${relationship.account!.name}'],
),
);
}
HapticFeedback.lightImpact();
} catch (err) {
showErrorAlert(err);
} finally {
submitting.value = false;
}
}
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
@@ -374,7 +411,7 @@ class _SentFriendRequestsSheet extends HookConsumerWidget {
child: Row(
children: [
Text(
'friendSentRequest'.tr(),
'friendRequests'.tr(),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
@@ -385,7 +422,7 @@ class _SentFriendRequestsSheet extends HookConsumerWidget {
icon: const Icon(Symbols.refresh),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
onPressed: () {
ref.invalidate(sentFriendRequestProvider);
ref.invalidate(friendRequestProvider);
},
),
IconButton(
@@ -399,12 +436,10 @@ class _SentFriendRequestsSheet extends HookConsumerWidget {
const Divider(height: 1),
Expanded(
child: requests.when(
data:
(items) =>
items.isEmpty
data: (items) => items.isEmpty
? Center(
child: Text(
'friendSentRequestEmpty'.tr(),
'friendRequestsEmpty'.tr(),
textAlign: TextAlign.center,
),
)
@@ -416,6 +451,8 @@ class _SentFriendRequestsSheet extends HookConsumerWidget {
return RelationshipListTile(
relationship: request,
onCancel: () => cancelRequest(request),
onAccept: () => handleFriendRequest(request, true),
onDecline: () => handleFriendRequest(request, false),
currentUserId: user.value?.id,
showRelatedAccount: true,
);

View File

@@ -0,0 +1,51 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'relationship.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(friendRequest)
final friendRequestProvider = FriendRequestProvider._();
final class FriendRequestProvider
extends
$FunctionalProvider<
AsyncValue<List<SnRelationship>>,
List<SnRelationship>,
FutureOr<List<SnRelationship>>
>
with
$FutureModifier<List<SnRelationship>>,
$FutureProvider<List<SnRelationship>> {
FriendRequestProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'friendRequestProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$friendRequestHash();
@$internal
@override
$FutureProviderElement<List<SnRelationship>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<SnRelationship>> create(Ref ref) {
return friendRequest(ref);
}
}
String _$friendRequestHash() => r'b066553f9bd0505a7a272cf10cb3ca312152acd6';

View File

@@ -12,16 +12,14 @@ _SnAbuseReport _$SnAbuseReportFromJson(Map<String, dynamic> json) =>
resourceIdentifier: json['resource_identifier'] as String,
type: (json['type'] as num).toInt(),
reason: json['reason'] as String,
resolvedAt:
json['resolved_at'] == null
resolvedAt: json['resolved_at'] == null
? null
: DateTime.parse(json['resolved_at'] as String),
resolution: json['resolution'] as String?,
accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);

View File

@@ -1,8 +1,8 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/activity.dart';
import 'package:island/models/auth.dart';
import 'package:island/models/file.dart';
import 'package:island/models/wallet.dart';
import 'package:island/core/models/activity.dart';
import 'package:island/auth/auth_models/auth.dart';
import 'package:island/drive/drive_models/file.dart';
import 'package:island/wallets/wallet_models/wallet.dart';
part 'account.freezed.dart';
part 'account.g.dart';
@@ -216,20 +216,20 @@ sealed class SnAuthDevice with _$SnAuthDevice {
}
@freezed
sealed class SnAuthDeviceWithChallenge with _$SnAuthDeviceWithChallenge {
const factory SnAuthDeviceWithChallenge({
sealed class SnAuthDeviceWithSession with _$SnAuthDeviceWithSession {
const factory SnAuthDeviceWithSession({
required String id,
required String deviceId,
required String deviceName,
required String? deviceLabel,
required String accountId,
required int platform,
required List<SnAuthChallenge> challenges,
required List<SnAuthSession> sessions,
@Default(false) bool isCurrent,
}) = _SnAuthDeviceWithChallengee;
}) = _SnAuthDeviceWithSessione;
factory SnAuthDeviceWithChallenge.fromJson(Map<String, dynamic> json) =>
_$SnAuthDeviceWithChallengeFromJson(json);
factory SnAuthDeviceWithSession.fromJson(Map<String, dynamic> json) =>
_$SnAuthDeviceWithSessionFromJson(json);
}
@freezed

View File

@@ -3068,51 +3068,51 @@ as bool,
}
SnAuthDeviceWithChallenge _$SnAuthDeviceWithChallengeFromJson(
SnAuthDeviceWithSession _$SnAuthDeviceWithSessionFromJson(
Map<String, dynamic> json
) {
return _SnAuthDeviceWithChallengee.fromJson(
return _SnAuthDeviceWithSessione.fromJson(
json
);
}
/// @nodoc
mixin _$SnAuthDeviceWithChallenge {
mixin _$SnAuthDeviceWithSession {
String get id; String get deviceId; String get deviceName; String? get deviceLabel; String get accountId; int get platform; List<SnAuthChallenge> get challenges; bool get isCurrent;
/// Create a copy of SnAuthDeviceWithChallenge
String get id; String get deviceId; String get deviceName; String? get deviceLabel; String get accountId; int get platform; List<SnAuthSession> get sessions; bool get isCurrent;
/// Create a copy of SnAuthDeviceWithSession
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAuthDeviceWithChallengeCopyWith<SnAuthDeviceWithChallenge> get copyWith => _$SnAuthDeviceWithChallengeCopyWithImpl<SnAuthDeviceWithChallenge>(this as SnAuthDeviceWithChallenge, _$identity);
$SnAuthDeviceWithSessionCopyWith<SnAuthDeviceWithSession> get copyWith => _$SnAuthDeviceWithSessionCopyWithImpl<SnAuthDeviceWithSession>(this as SnAuthDeviceWithSession, _$identity);
/// Serializes this SnAuthDeviceWithChallenge to a JSON map.
/// Serializes this SnAuthDeviceWithSession to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthDeviceWithChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other.challenges, challenges)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthDeviceWithSession&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other.sessions, sessions)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,const DeepCollectionEquality().hash(challenges),isCurrent);
int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,const DeepCollectionEquality().hash(sessions),isCurrent);
@override
String toString() {
return 'SnAuthDeviceWithChallenge(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, challenges: $challenges, isCurrent: $isCurrent)';
return 'SnAuthDeviceWithSession(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, sessions: $sessions, isCurrent: $isCurrent)';
}
}
/// @nodoc
abstract mixin class $SnAuthDeviceWithChallengeCopyWith<$Res> {
factory $SnAuthDeviceWithChallengeCopyWith(SnAuthDeviceWithChallenge value, $Res Function(SnAuthDeviceWithChallenge) _then) = _$SnAuthDeviceWithChallengeCopyWithImpl;
abstract mixin class $SnAuthDeviceWithSessionCopyWith<$Res> {
factory $SnAuthDeviceWithSessionCopyWith(SnAuthDeviceWithSession value, $Res Function(SnAuthDeviceWithSession) _then) = _$SnAuthDeviceWithSessionCopyWithImpl;
@useResult
$Res call({
String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent
String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthSession> sessions, bool isCurrent
});
@@ -3120,16 +3120,16 @@ $Res call({
}
/// @nodoc
class _$SnAuthDeviceWithChallengeCopyWithImpl<$Res>
implements $SnAuthDeviceWithChallengeCopyWith<$Res> {
_$SnAuthDeviceWithChallengeCopyWithImpl(this._self, this._then);
class _$SnAuthDeviceWithSessionCopyWithImpl<$Res>
implements $SnAuthDeviceWithSessionCopyWith<$Res> {
_$SnAuthDeviceWithSessionCopyWithImpl(this._self, this._then);
final SnAuthDeviceWithChallenge _self;
final $Res Function(SnAuthDeviceWithChallenge) _then;
final SnAuthDeviceWithSession _self;
final $Res Function(SnAuthDeviceWithSession) _then;
/// Create a copy of SnAuthDeviceWithChallenge
/// Create a copy of SnAuthDeviceWithSession
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? challenges = null,Object? isCurrent = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? sessions = null,Object? isCurrent = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
@@ -3137,8 +3137,8 @@ as String,deviceName: null == deviceName ? _self.deviceName : deviceName // igno
as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,challenges: null == challenges ? _self.challenges : challenges // ignore: cast_nullable_to_non_nullable
as List<SnAuthChallenge>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
as int,sessions: null == sessions ? _self.sessions : sessions // ignore: cast_nullable_to_non_nullable
as List<SnAuthSession>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
));
}
@@ -3146,8 +3146,8 @@ as bool,
}
/// Adds pattern-matching-related methods to [SnAuthDeviceWithChallenge].
extension SnAuthDeviceWithChallengePatterns on SnAuthDeviceWithChallenge {
/// Adds pattern-matching-related methods to [SnAuthDeviceWithSession].
extension SnAuthDeviceWithSessionPatterns on SnAuthDeviceWithSession {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
@@ -3160,10 +3160,10 @@ extension SnAuthDeviceWithChallengePatterns on SnAuthDeviceWithChallenge {
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnAuthDeviceWithChallengee value)? $default,{required TResult orElse(),}){
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnAuthDeviceWithSessione value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnAuthDeviceWithChallengee() when $default != null:
case _SnAuthDeviceWithSessione() when $default != null:
return $default(_that);case _:
return orElse();
@@ -3182,10 +3182,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnAuthDeviceWithChallengee value) $default,){
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnAuthDeviceWithSessione value) $default,){
final _that = this;
switch (_that) {
case _SnAuthDeviceWithChallengee():
case _SnAuthDeviceWithSessione():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
@@ -3200,10 +3200,10 @@ return $default(_that);}
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnAuthDeviceWithChallengee value)? $default,){
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnAuthDeviceWithSessione value)? $default,){
final _that = this;
switch (_that) {
case _SnAuthDeviceWithChallengee() when $default != null:
case _SnAuthDeviceWithSessione() when $default != null:
return $default(_that);case _:
return null;
@@ -3221,10 +3221,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthSession> sessions, bool isCurrent)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnAuthDeviceWithChallengee() when $default != null:
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.challenges,_that.isCurrent);case _:
case _SnAuthDeviceWithSessione() when $default != null:
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.sessions,_that.isCurrent);case _:
return orElse();
}
@@ -3242,10 +3242,10 @@ return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthSession> sessions, bool isCurrent) $default,) {final _that = this;
switch (_that) {
case _SnAuthDeviceWithChallengee():
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.challenges,_that.isCurrent);}
case _SnAuthDeviceWithSessione():
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.sessions,_that.isCurrent);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -3259,10 +3259,10 @@ return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthSession> sessions, bool isCurrent)? $default,) {final _that = this;
switch (_that) {
case _SnAuthDeviceWithChallengee() when $default != null:
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.challenges,_that.isCurrent);case _:
case _SnAuthDeviceWithSessione() when $default != null:
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.sessions,_that.isCurrent);case _:
return null;
}
@@ -3273,9 +3273,9 @@ return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that
/// @nodoc
@JsonSerializable()
class _SnAuthDeviceWithChallengee implements SnAuthDeviceWithChallenge {
const _SnAuthDeviceWithChallengee({required this.id, required this.deviceId, required this.deviceName, required this.deviceLabel, required this.accountId, required this.platform, required final List<SnAuthChallenge> challenges, this.isCurrent = false}): _challenges = challenges;
factory _SnAuthDeviceWithChallengee.fromJson(Map<String, dynamic> json) => _$SnAuthDeviceWithChallengeeFromJson(json);
class _SnAuthDeviceWithSessione implements SnAuthDeviceWithSession {
const _SnAuthDeviceWithSessione({required this.id, required this.deviceId, required this.deviceName, required this.deviceLabel, required this.accountId, required this.platform, required final List<SnAuthSession> sessions, this.isCurrent = false}): _sessions = sessions;
factory _SnAuthDeviceWithSessione.fromJson(Map<String, dynamic> json) => _$SnAuthDeviceWithSessioneFromJson(json);
@override final String id;
@override final String deviceId;
@@ -3283,49 +3283,49 @@ class _SnAuthDeviceWithChallengee implements SnAuthDeviceWithChallenge {
@override final String? deviceLabel;
@override final String accountId;
@override final int platform;
final List<SnAuthChallenge> _challenges;
@override List<SnAuthChallenge> get challenges {
if (_challenges is EqualUnmodifiableListView) return _challenges;
final List<SnAuthSession> _sessions;
@override List<SnAuthSession> get sessions {
if (_sessions is EqualUnmodifiableListView) return _sessions;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_challenges);
return EqualUnmodifiableListView(_sessions);
}
@override@JsonKey() final bool isCurrent;
/// Create a copy of SnAuthDeviceWithChallenge
/// Create a copy of SnAuthDeviceWithSession
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAuthDeviceWithChallengeeCopyWith<_SnAuthDeviceWithChallengee> get copyWith => __$SnAuthDeviceWithChallengeeCopyWithImpl<_SnAuthDeviceWithChallengee>(this, _$identity);
_$SnAuthDeviceWithSessioneCopyWith<_SnAuthDeviceWithSessione> get copyWith => __$SnAuthDeviceWithSessioneCopyWithImpl<_SnAuthDeviceWithSessione>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAuthDeviceWithChallengeeToJson(this, );
return _$SnAuthDeviceWithSessioneToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthDeviceWithChallengee&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other._challenges, _challenges)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthDeviceWithSessione&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other._sessions, _sessions)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,const DeepCollectionEquality().hash(_challenges),isCurrent);
int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,const DeepCollectionEquality().hash(_sessions),isCurrent);
@override
String toString() {
return 'SnAuthDeviceWithChallenge(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, challenges: $challenges, isCurrent: $isCurrent)';
return 'SnAuthDeviceWithSession(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, sessions: $sessions, isCurrent: $isCurrent)';
}
}
/// @nodoc
abstract mixin class _$SnAuthDeviceWithChallengeeCopyWith<$Res> implements $SnAuthDeviceWithChallengeCopyWith<$Res> {
factory _$SnAuthDeviceWithChallengeeCopyWith(_SnAuthDeviceWithChallengee value, $Res Function(_SnAuthDeviceWithChallengee) _then) = __$SnAuthDeviceWithChallengeeCopyWithImpl;
abstract mixin class _$SnAuthDeviceWithSessioneCopyWith<$Res> implements $SnAuthDeviceWithSessionCopyWith<$Res> {
factory _$SnAuthDeviceWithSessioneCopyWith(_SnAuthDeviceWithSessione value, $Res Function(_SnAuthDeviceWithSessione) _then) = __$SnAuthDeviceWithSessioneCopyWithImpl;
@override @useResult
$Res call({
String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent
String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthSession> sessions, bool isCurrent
});
@@ -3333,25 +3333,25 @@ $Res call({
}
/// @nodoc
class __$SnAuthDeviceWithChallengeeCopyWithImpl<$Res>
implements _$SnAuthDeviceWithChallengeeCopyWith<$Res> {
__$SnAuthDeviceWithChallengeeCopyWithImpl(this._self, this._then);
class __$SnAuthDeviceWithSessioneCopyWithImpl<$Res>
implements _$SnAuthDeviceWithSessioneCopyWith<$Res> {
__$SnAuthDeviceWithSessioneCopyWithImpl(this._self, this._then);
final _SnAuthDeviceWithChallengee _self;
final $Res Function(_SnAuthDeviceWithChallengee) _then;
final _SnAuthDeviceWithSessione _self;
final $Res Function(_SnAuthDeviceWithSessione) _then;
/// Create a copy of SnAuthDeviceWithChallenge
/// Create a copy of SnAuthDeviceWithSession
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? challenges = null,Object? isCurrent = null,}) {
return _then(_SnAuthDeviceWithChallengee(
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? sessions = null,Object? isCurrent = null,}) {
return _then(_SnAuthDeviceWithSessione(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable
as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
as int,challenges: null == challenges ? _self._challenges : challenges // ignore: cast_nullable_to_non_nullable
as List<SnAuthChallenge>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
as int,sessions: null == sessions ? _self._sessions : sessions // ignore: cast_nullable_to_non_nullable
as List<SnAuthSession>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
));
}

View File

@@ -15,8 +15,7 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
isSuperuser: json['is_superuser'] as bool,
automatedId: json['automated_id'] as String?,
profile: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
perkSubscription:
json['perk_subscription'] == null
perkSubscription: json['perk_subscription'] == null
? null
: SnWalletSubscriptionRef.fromJson(
json['perk_subscription'] as Map<String, dynamic>,
@@ -31,14 +30,12 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
?.map((e) => SnContactMethod.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
activatedAt:
json['activated_at'] == null
activatedAt: json['activated_at'] == null
? null
: DateTime.parse(json['activated_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
@@ -73,8 +70,9 @@ _UsernameColor _$UsernameColorFromJson(Map<String, dynamic> json) =>
type: json['type'] as String? ?? 'plain',
value: json['value'] as String?,
direction: json['direction'] as String?,
colors:
(json['colors'] as List<dynamic>?)?.map((e) => e as String).toList(),
colors: (json['colors'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
);
Map<String, dynamic> _$UsernameColorToJson(_UsernameColor instance) =>
@@ -85,8 +83,9 @@ Map<String, dynamic> _$UsernameColorToJson(_UsernameColor instance) =>
'colors': instance.colors,
};
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
_SnAccountProfile(
_SnAccountProfile _$SnAccountProfileFromJson(
Map<String, dynamic> json,
) => _SnAccountProfile(
id: json['id'] as String,
firstName: json['first_name'] as String? ?? '',
middleName: json['middle_name'] as String? ?? '',
@@ -96,58 +95,43 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
pronouns: json['pronouns'] as String? ?? '',
location: json['location'] as String? ?? '',
timeZone: json['time_zone'] as String? ?? '',
birthday:
json['birthday'] == null
birthday: json['birthday'] == null
? null
: DateTime.parse(json['birthday'] as String),
links:
json['links'] == null
links: json['links'] == null
? const []
: const ProfileLinkConverter().fromJson(json['links']),
lastSeenAt:
json['last_seen_at'] == null
lastSeenAt: json['last_seen_at'] == null
? null
: DateTime.parse(json['last_seen_at'] as String),
activeBadge:
json['active_badge'] == null
activeBadge: json['active_badge'] == null
? null
: SnAccountBadge.fromJson(
json['active_badge'] as Map<String, dynamic>,
),
: SnAccountBadge.fromJson(json['active_badge'] as Map<String, dynamic>),
experience: (json['experience'] as num).toInt(),
level: (json['level'] as num).toInt(),
socialCredits: (json['social_credits'] as num?)?.toDouble() ?? 100,
socialCreditsLevel: (json['social_credits_level'] as num?)?.toInt() ?? 0,
levelingProgress: (json['leveling_progress'] as num).toDouble(),
picture:
json['picture'] == null
picture: json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background:
json['background'] == null
background: json['background'] == null
? null
: SnCloudFile.fromJson(
json['background'] as Map<String, dynamic>,
),
verification:
json['verification'] == null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
verification: json['verification'] == null
? null
: SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>,
),
usernameColor:
json['username_color'] == null
usernameColor: json['username_color'] == null
? null
: UsernameColor.fromJson(
json['username_color'] as Map<String, dynamic>,
),
: UsernameColor.fromJson(json['username_color'] 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
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
);
Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
<String, dynamic>{
@@ -188,15 +172,13 @@ _SnAccountStatus _$SnAccountStatusFromJson(Map<String, dynamic> json) =>
isCustomized: json['is_customized'] as bool,
label: json['label'] as String? ?? "",
meta: json['meta'] as Map<String, dynamic>?,
clearedAt:
json['cleared_at'] == null
clearedAt: json['cleared_at'] == null
? null
: DateTime.parse(json['cleared_at'] as String),
accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
@@ -225,19 +207,16 @@ _SnAccountBadge _$SnAccountBadgeFromJson(Map<String, dynamic> json) =>
label: json['label'] as String?,
caption: json['caption'] as String?,
meta: json['meta'] as Map<String, dynamic>,
expiredAt:
json['expired_at'] == null
expiredAt: json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
activatedAt:
json['activated_at'] == null
activatedAt: json['activated_at'] == null
? null
: DateTime.parse(json['activated_at'] as String),
deletedAt:
json['deleted_at'] == null
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
@@ -261,8 +240,7 @@ _SnContactMethod _$SnContactMethodFromJson(Map<String, dynamic> json) =>
_SnContactMethod(
id: json['id'] as String,
type: (json['type'] as num).toInt(),
verifiedAt:
json['verified_at'] == null
verifiedAt: json['verified_at'] == null
? null
: DateTime.parse(json['verified_at'] as String),
isPrimary: json['is_primary'] as bool,
@@ -271,8 +249,7 @@ _SnContactMethod _$SnContactMethodFromJson(Map<String, dynamic> json) =>
accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
@@ -295,8 +272,7 @@ _SnNotification _$SnNotificationFromJson(Map<String, dynamic> json) =>
_SnNotification(
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
id: json['id'] as String,
@@ -306,8 +282,7 @@ _SnNotification _$SnNotificationFromJson(Map<String, dynamic> json) =>
content: json['content'] as String,
meta: json['meta'] as Map<String, dynamic>? ?? const {},
priority: (json['priority'] as num).toInt(),
viewedAt:
json['viewed_at'] == null
viewedAt: json['viewed_at'] == null
? null
: DateTime.parse(json['viewed_at'] as String),
accountId: json['account_id'] as String,
@@ -367,24 +342,23 @@ Map<String, dynamic> _$SnAuthDeviceToJson(_SnAuthDevice instance) =>
'is_current': instance.isCurrent,
};
_SnAuthDeviceWithChallengee _$SnAuthDeviceWithChallengeeFromJson(
_SnAuthDeviceWithSessione _$SnAuthDeviceWithSessioneFromJson(
Map<String, dynamic> json,
) => _SnAuthDeviceWithChallengee(
) => _SnAuthDeviceWithSessione(
id: json['id'] as String,
deviceId: json['device_id'] as String,
deviceName: json['device_name'] as String,
deviceLabel: json['device_label'] as String?,
accountId: json['account_id'] as String,
platform: (json['platform'] as num).toInt(),
challenges:
(json['challenges'] as List<dynamic>)
.map((e) => SnAuthChallenge.fromJson(e as Map<String, dynamic>))
sessions: (json['sessions'] as List<dynamic>)
.map((e) => SnAuthSession.fromJson(e as Map<String, dynamic>))
.toList(),
isCurrent: json['is_current'] as bool? ?? false,
);
Map<String, dynamic> _$SnAuthDeviceWithChallengeeToJson(
_SnAuthDeviceWithChallengee instance,
Map<String, dynamic> _$SnAuthDeviceWithSessioneToJson(
_SnAuthDeviceWithSessione instance,
) => <String, dynamic>{
'id': instance.id,
'device_id': instance.deviceId,
@@ -392,7 +366,7 @@ Map<String, dynamic> _$SnAuthDeviceWithChallengeeToJson(
'device_label': instance.deviceLabel,
'account_id': instance.accountId,
'platform': instance.platform,
'challenges': instance.challenges.map((e) => e.toJson()).toList(),
'sessions': instance.sessions.map((e) => e.toJson()).toList(),
'is_current': instance.isCurrent,
};
@@ -405,8 +379,7 @@ _SnExperienceRecord _$SnExperienceRecordFromJson(Map<String, dynamic> json) =>
bonusMultiplier: (json['bonus_multiplier'] as num?)?.toDouble() ?? 1.0,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
@@ -430,14 +403,12 @@ _SnSocialCreditRecord _$SnSocialCreditRecordFromJson(
delta: (json['delta'] as num).toDouble(),
reasonType: json['reason_type'] as String,
reason: json['reason'] as String,
expiredAt:
json['expired_at'] == null
expiredAt: json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
@@ -460,8 +431,7 @@ _SnFriendOverviewItem _$SnFriendOverviewItemFromJson(
) => _SnFriendOverviewItem(
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
status: SnAccountStatus.fromJson(json['status'] as Map<String, dynamic>),
activities:
(json['activities'] as List<dynamic>)
activities: (json['activities'] as List<dynamic>)
.map((e) => SnPresenceActivity.fromJson(e as Map<String, dynamic>))
.toList(),
);

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

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

@@ -1,6 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/account.dart';
import 'package:island/accounts/accounts_models/account.dart';
part 'relationship.freezed.dart';
part 'relationship.g.dart';
@@ -12,9 +12,10 @@ sealed class SnRelationship with _$SnRelationship {
required DateTime? updatedAt,
required DateTime? deletedAt,
required String accountId,
required SnAccount account,
// Usually the account was not included in the response
required SnAccount? account,
required String relatedId,
required SnAccount related,
required SnAccount? related,
required DateTime? expiredAt,
required int status,
}) = _SnRelationship;

View File

@@ -15,7 +15,8 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnRelationship {
DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; String get accountId; SnAccount get account; String get relatedId; SnAccount get related; DateTime? get expiredAt; int get status;
DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; String get accountId;// Usually the account was not included in the response
SnAccount? get account; String get relatedId; SnAccount? get related; DateTime? get expiredAt; int get status;
/// Create a copy of SnRelationship
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -48,11 +49,11 @@ abstract mixin class $SnRelationshipCopyWith<$Res> {
factory $SnRelationshipCopyWith(SnRelationship value, $Res Function(SnRelationship) _then) = _$SnRelationshipCopyWithImpl;
@useResult
$Res call({
DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String accountId, SnAccount account, String relatedId, SnAccount related, DateTime? expiredAt, int status
DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String accountId, SnAccount? account, String relatedId, SnAccount? related, DateTime? expiredAt, int status
});
$SnAccountCopyWith<$Res> get account;$SnAccountCopyWith<$Res> get related;
$SnAccountCopyWith<$Res>? get account;$SnAccountCopyWith<$Res>? get related;
}
/// @nodoc
@@ -65,16 +66,16 @@ class _$SnRelationshipCopyWithImpl<$Res>
/// Create a copy of SnRelationship
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? accountId = null,Object? account = null,Object? relatedId = null,Object? related = null,Object? expiredAt = freezed,Object? status = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? accountId = null,Object? account = freezed,Object? relatedId = null,Object? related = freezed,Object? expiredAt = freezed,Object? status = null,}) {
return _then(_self.copyWith(
createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount,relatedId: null == relatedId ? _self.relatedId : relatedId // ignore: cast_nullable_to_non_nullable
as String,related: null == related ? _self.related : related // ignore: cast_nullable_to_non_nullable
as SnAccount,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,relatedId: null == relatedId ? _self.relatedId : relatedId // ignore: cast_nullable_to_non_nullable
as String,related: freezed == related ? _self.related : related // ignore: cast_nullable_to_non_nullable
as SnAccount?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as int,
));
@@ -83,18 +84,24 @@ as int,
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res> get account {
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account, (value) {
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}/// Create a copy of SnRelationship
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res> get related {
$SnAccountCopyWith<$Res>? get related {
if (_self.related == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.related, (value) {
return $SnAccountCopyWith<$Res>(_self.related!, (value) {
return _then(_self.copyWith(related: value));
});
}
@@ -176,7 +183,7 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String accountId, SnAccount account, String relatedId, SnAccount related, DateTime? expiredAt, int status)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String accountId, SnAccount? account, String relatedId, SnAccount? related, DateTime? expiredAt, int status)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnRelationship() when $default != null:
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.accountId,_that.account,_that.relatedId,_that.related,_that.expiredAt,_that.status);case _:
@@ -197,7 +204,7 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.accountId,
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String accountId, SnAccount account, String relatedId, SnAccount related, DateTime? expiredAt, int status) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String accountId, SnAccount? account, String relatedId, SnAccount? related, DateTime? expiredAt, int status) $default,) {final _that = this;
switch (_that) {
case _SnRelationship():
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.accountId,_that.account,_that.relatedId,_that.related,_that.expiredAt,_that.status);}
@@ -214,7 +221,7 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.accountId,
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String accountId, SnAccount account, String relatedId, SnAccount related, DateTime? expiredAt, int status)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String accountId, SnAccount? account, String relatedId, SnAccount? related, DateTime? expiredAt, int status)? $default,) {final _that = this;
switch (_that) {
case _SnRelationship() when $default != null:
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.accountId,_that.account,_that.relatedId,_that.related,_that.expiredAt,_that.status);case _:
@@ -236,9 +243,10 @@ class _SnRelationship implements SnRelationship {
@override final DateTime? updatedAt;
@override final DateTime? deletedAt;
@override final String accountId;
@override final SnAccount account;
// Usually the account was not included in the response
@override final SnAccount? account;
@override final String relatedId;
@override final SnAccount related;
@override final SnAccount? related;
@override final DateTime? expiredAt;
@override final int status;
@@ -275,11 +283,11 @@ abstract mixin class _$SnRelationshipCopyWith<$Res> implements $SnRelationshipCo
factory _$SnRelationshipCopyWith(_SnRelationship value, $Res Function(_SnRelationship) _then) = __$SnRelationshipCopyWithImpl;
@override @useResult
$Res call({
DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String accountId, SnAccount account, String relatedId, SnAccount related, DateTime? expiredAt, int status
DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String accountId, SnAccount? account, String relatedId, SnAccount? related, DateTime? expiredAt, int status
});
@override $SnAccountCopyWith<$Res> get account;@override $SnAccountCopyWith<$Res> get related;
@override $SnAccountCopyWith<$Res>? get account;@override $SnAccountCopyWith<$Res>? get related;
}
/// @nodoc
@@ -292,16 +300,16 @@ class __$SnRelationshipCopyWithImpl<$Res>
/// Create a copy of SnRelationship
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? accountId = null,Object? account = null,Object? relatedId = null,Object? related = null,Object? expiredAt = freezed,Object? status = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? accountId = null,Object? account = freezed,Object? relatedId = null,Object? related = freezed,Object? expiredAt = freezed,Object? status = null,}) {
return _then(_SnRelationship(
createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount,relatedId: null == relatedId ? _self.relatedId : relatedId // ignore: cast_nullable_to_non_nullable
as String,related: null == related ? _self.related : related // ignore: cast_nullable_to_non_nullable
as SnAccount,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,relatedId: null == relatedId ? _self.relatedId : relatedId // ignore: cast_nullable_to_non_nullable
as String,related: freezed == related ? _self.related : related // ignore: cast_nullable_to_non_nullable
as SnAccount?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as int,
));
@@ -311,18 +319,24 @@ as int,
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res> get account {
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account, (value) {
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}/// Create a copy of SnRelationship
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res> get related {
$SnAccountCopyWith<$Res>? get related {
if (_self.related == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.related, (value) {
return $SnAccountCopyWith<$Res>(_self.related!, (value) {
return _then(_self.copyWith(related: value));
});
}

View File

@@ -8,24 +8,24 @@ part of 'relationship.dart';
_SnRelationship _$SnRelationshipFromJson(Map<String, dynamic> json) =>
_SnRelationship(
createdAt:
json['created_at'] == null
createdAt: json['created_at'] == null
? null
: DateTime.parse(json['created_at'] as String),
updatedAt:
json['updated_at'] == null
updatedAt: json['updated_at'] == null
? null
: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
accountId: json['account_id'] as String,
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
account: json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
relatedId: json['related_id'] as String,
related: SnAccount.fromJson(json['related'] as Map<String, dynamic>),
expiredAt:
json['expired_at'] == null
related: json['related'] == null
? null
: SnAccount.fromJson(json['related'] as Map<String, dynamic>),
expiredAt: json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
status: (json['status'] as num).toInt(),
@@ -37,9 +37,9 @@ Map<String, dynamic> _$SnRelationshipToJson(_SnRelationship instance) =>
'updated_at': instance.updatedAt?.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'account_id': instance.accountId,
'account': instance.account.toJson(),
'account': instance.account?.toJson(),
'related_id': instance.relatedId,
'related': instance.related.toJson(),
'related': instance.related?.toJson(),
'expired_at': instance.expiredAt?.toIso8601String(),
'status': instance.status,
};

View File

@@ -0,0 +1,96 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:island/shared/widgets/alert.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/accounts/accounts_models/account.dart';
import 'package:island/core/config.dart';
import 'package:island/core/network.dart';
import 'package:island/talker.dart';
import 'package:island/core/services/analytics_service.dart';
class UserInfoNotifier extends AsyncNotifier<SnAccount?> {
@override
Future<SnAccount?> build() async {
final token = ref.watch(tokenProvider);
if (token == null) {
talker.info('[UserInfo] No token found, not going to fetch...');
return null;
}
return _fetchUser();
}
Future<SnAccount?> _fetchUser() async {
try {
final client = ref.read(apiClientProvider);
final response = await client.get('/pass/accounts/me');
final user = SnAccount.fromJson(response.data);
AnalyticsService().setUserId(user.id);
return user;
} catch (error, stackTrace) {
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...",
error,
stackTrace,
);
return null;
}
}
Future<void> fetchUser() async {
ref.invalidateSelf();
await future;
}
Future<void> logOut() async {
state = const AsyncValue.data(null);
final prefs = ref.read(sharedPreferencesProvider);
await prefs.remove(kTokenPairStoreKey);
ref.invalidate(tokenProvider);
AnalyticsService().setUserId(null);
AnalyticsService().logLogout();
}
}
final userInfoProvider = AsyncNotifierProvider<UserInfoNotifier, SnAccount?>(
UserInfoNotifier.new,
);

View File

@@ -3,20 +3,20 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/message.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.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/activity_presence.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/account/leveling_progress.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/debug_sheet.dart';
import 'package:island/accounts/accounts_widgets/account/account_name.dart';
import 'package:island/accounts/accounts_widgets/account/activity_presence.dart';
import 'package:island/accounts/accounts_widgets/account/leveling_progress.dart';
import 'package:island/accounts/accounts_widgets/account/status.dart';
import 'package:island/core/websocket.dart';
import 'package:island/core/database.dart';
import 'package:island/core/network.dart';
import 'package:island/accounts/accounts_pod.dart';
import 'package:island/core/services/responsive.dart';
import 'package:island/shared/widgets/alert.dart';
import 'package:island/shared/widgets/app_scaffold.dart';
import 'package:island/drive/drive_widgets/cloud_files.dart';
import 'package:island/core/debug_sheet.dart';
import 'package:island/notifications/notification.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -57,9 +57,7 @@ class AccountScreen extends HookConsumerWidget {
}
final user = ref.watch(userInfoProvider);
final notificationUnreadCount = ref.watch(
notificationUnreadCountNotifierProvider,
);
final notificationUnreadCount = ref.watch(notificationUnreadCountProvider);
if (user.value == null || user.value == null) {
return _UnauthorizedAccountScreen();
@@ -76,7 +74,7 @@ class AccountScreen extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (user.value?.profile.background?.id != null)
if (user.value?.profile.background != null)
Stack(
clipBehavior: Clip.none,
children: [
@@ -114,7 +112,7 @@ class AccountScreen extends HookConsumerWidget {
Builder(
builder: (context) {
final hasBackground =
user.value?.profile.background?.id != null;
user.value?.profile.background != null;
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: hasBackground ? 0 : 16,
@@ -333,8 +331,7 @@ class AccountScreen extends HookConsumerWidget {
if (availableWidth > totalMin) {
return Row(
spacing: 8,
children:
children
children: children
.map((child) => Expanded(child: child))
.toList(),
).padding(horizontal: 12).height(48);
@@ -343,8 +340,7 @@ class AccountScreen extends HookConsumerWidget {
scrollDirection: Axis.horizontal,
child: Row(
spacing: 8,
children:
children
children: children
.map(
(child) =>
SizedBox(width: minWidth, child: child),
@@ -356,21 +352,14 @@ class AccountScreen extends HookConsumerWidget {
},
),
),
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: () {
Builder(
builder: (context) {
final menuItems = [
{
'icon': Symbols.notifications,
'title': 'notifications',
'badgeCount': notificationUnreadCount.value ?? 0,
'onTap': () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
@@ -378,83 +367,107 @@ class AccountScreen extends HookConsumerWidget {
builder: (context) => const NotificationSheet(),
);
},
),
},
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: () {
{
'icon': Symbols.files,
'title': 'files',
'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: () {
},
if (!isWideScreen(context))
{
'icon': Symbols.groups_3,
'title': 'realms',
'onTap': () {
context.goNamed('realmList');
},
},
{
'icon': Symbols.wallet,
'title': 'wallet',
'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: () {
},
{
'icon': Symbols.people,
'title': 'relationships',
'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: () {
},
{
'icon': Symbols.sticker_rounded,
'title': 'stickers',
'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: () {
},
{
'icon': Symbols.rss_feed,
'title': 'webFeeds',
'onTap': () {
context.pushNamed('webFeedMarketplace');
},
},
{
'icon': Symbols.gavel,
'title': 'abuseReport',
'onTap': () {
context.pushNamed('reportList');
},
},
{
'icon': Symbols.fitness_center,
'title': 'fitnessActivity',
'onTap': () {
context.pushNamed('fitnessActivity');
},
},
];
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,
),
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'),
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(),
);
},
),
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,
@@ -465,11 +478,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);
@@ -497,8 +510,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: const Text('account').tr()),
body:
ConstrainedBox(
body: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,

View File

@@ -0,0 +1,367 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/accounts/accounts_models/account.dart';
import 'package:island/core/network.dart';
import 'package:island/core/services/responsive.dart';
import 'package:island/core/services/time.dart';
import 'package:island/core/services/udid.dart';
import 'package:island/shared/widgets/alert.dart';
import 'package:island/drive/content/sheet_scaffold.dart';
import 'package:island/shared/widgets/response.dart';
import 'package:island/shared/widgets/info_row.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/shared/widgets/extended_refresh_indicator.dart';
part 'account_devices.g.dart';
@riverpod
Future<List<SnAuthDeviceWithSession>> authDevices(Ref ref) async {
final resp = await ref
.watch(apiClientProvider)
.get('/pass/accounts/me/devices');
final currentId = await getUdid();
final data = resp.data.map<SnAuthDeviceWithSession>((e) {
final ele = SnAuthDeviceWithSession.fromJson(e);
return ele.copyWith(isCurrent: ele.deviceId == currentId);
}).toList();
return data;
}
class _DeviceListTile extends StatelessWidget {
final SnAuthDeviceWithSession device;
final Function(String) updateDeviceLabel;
final Function(String) logoutDevice;
final Function(String) logoutSession;
const _DeviceListTile({
required this.device,
required this.updateDeviceLabel,
required this.logoutDevice,
required this.logoutSession,
});
@override
Widget build(BuildContext context) {
return ExpansionTile(
title: Row(
spacing: 8,
children: [
Flexible(child: Text(device.deviceLabel ?? device.deviceName)),
if (device.isCurrent)
Row(
children: [
Badge(
backgroundColor: Theme.of(context).colorScheme.primary,
label: Text(
'authDeviceCurrent'.tr(),
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (device.sessions.isNotEmpty)
Text(
'lastActiveAt'.tr(
args: [device.sessions.first.createdAt.formatSystem()],
),
),
],
),
leading: Icon(switch (device.platform) {
0 => Icons.device_unknown, // Unidentified
1 => Icons.web, // Web
2 => Icons.phone_iphone, // iOS
3 => Icons.phone_android, // Android
4 => Icons.laptop_mac, // macOS
5 => Icons.window, // Windows
6 => Icons.computer, // Linux
_ => Icons.device_unknown, // fallback
}).padding(top: 4),
trailing: isWideScreen(context)
? Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.edit),
tooltip: 'authDeviceEditLabel'.tr(),
onPressed: () => updateDeviceLabel(device.deviceId),
),
if (!device.isCurrent)
IconButton(
icon: Icon(Icons.logout),
tooltip: 'authDeviceLogout'.tr(),
onPressed: () => logoutDevice(device.deviceId),
),
],
)
: null,
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text('authDeviceChallenges'.tr()),
),
...device.sessions
.map(
(session) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
InfoRow(
label: 'createdAt'.tr(
args: [session.createdAt.toLocal().formatSystem()],
),
icon: Symbols.join,
),
InfoRow(
label: 'lastActiveAt'.tr(
args: [
session.lastGrantedAt.toLocal().formatSystem(),
],
),
icon: Symbols.refresh_rounded,
),
InfoRow(
label:
'${'location'.tr()} ${session.location?.city ?? 'unknown'.tr()}',
icon: Symbols.pin_drop,
),
InfoRow(
label:
'${'ipAddress'.tr()} ${session.ipAddress ?? 'unknown'.tr()}',
icon: Symbols.dns,
),
],
),
),
IconButton(
icon: Icon(Icons.logout),
tooltip: 'authSessionLogout'.tr(),
onPressed: () => logoutSession(session.id),
),
const Gap(4),
],
).padding(horizontal: 20, vertical: 8),
)
.expand((element) => [element, const Divider(height: 1)])
.toList()
..removeLast(),
],
);
}
}
class AccountSessionSheet extends HookConsumerWidget {
const AccountSessionSheet({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final authDevices = ref.watch(authDevicesProvider);
void logoutDevice(String sessionId) async {
final confirm = await showConfirmAlert(
'authDeviceLogoutHint'.tr(),
'authDeviceLogout'.tr(),
isDanger: true,
);
if (!confirm || !context.mounted) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete('/pass/accounts/me/devices/$sessionId');
ref.invalidate(authDevicesProvider);
} catch (err) {
showErrorAlert(err);
}
}
void logoutSession(String sessionId) async {
final confirm = await showConfirmAlert(
'authSessionLogoutHint'.tr(),
'authSessionLogout'.tr(),
isDanger: true,
);
if (!confirm || !context.mounted) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete('/pass/accounts/me/sessions/$sessionId');
ref.invalidate(authDevicesProvider);
} catch (err) {
showErrorAlert(err);
}
}
void updateDeviceLabel(String sessionId) async {
final controller = TextEditingController();
final label = await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: Text('authDeviceLabelTitle'.tr()),
content: TextField(
controller: controller,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
hintText: 'authDeviceLabelHint'.tr(),
),
autofocus: true,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('cancel'.tr()),
),
TextButton(
onPressed: () => Navigator.pop(context, controller.text),
child: Text('confirm'.tr()),
),
],
),
);
if (label == null || label.isEmpty || !context.mounted) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.patch(
'/pass/accounts/me/devices/$sessionId/label',
data: jsonEncode(label),
);
ref.invalidate(authDevicesProvider);
} catch (err) {
showErrorAlert(err);
}
}
final wideScreen = isWideScreen(context);
return SheetScaffold(
titleText: 'authSessions'.tr(),
child: Column(
children: [
if (!wideScreen)
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
const Icon(Symbols.info, size: 16).padding(top: 2),
Flexible(
child: Text(
'authDeviceHint'.tr(),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
),
Expanded(
child: authDevices.when(
data: (data) => ExtendedRefreshIndicator(
onRefresh: () =>
Future.sync(() => ref.invalidate(authDevicesProvider)),
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.length,
itemBuilder: (context, index) {
final device = data[index];
if (wideScreen) {
return _DeviceListTile(
device: device,
updateDeviceLabel: updateDeviceLabel,
logoutDevice: logoutDevice,
logoutSession: logoutSession,
);
} else {
return Dismissible(
key: Key('device-${device.id}'),
direction: device.isCurrent
? DismissDirection.startToEnd
: DismissDirection.horizontal,
background: Container(
color: Colors.blue,
alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(Icons.edit, color: Colors.white),
),
secondaryBackground: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(Icons.logout, color: Colors.white),
),
confirmDismiss: (direction) async {
if (direction == DismissDirection.startToEnd) {
updateDeviceLabel(device.deviceId);
return false;
} else {
final confirm = await showConfirmAlert(
'authDeviceLogoutHint'.tr(),
'authDeviceLogout'.tr(),
isDanger: true,
);
if (confirm && context.mounted) {
try {
showLoadingModal(context);
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete(
'/pass/accounts/me/devices/${device.deviceId}',
);
ref.invalidate(authDevicesProvider);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) {
hideLoadingModal(context);
}
}
}
return confirm;
}
},
child: _DeviceListTile(
device: device,
updateDeviceLabel: updateDeviceLabel,
logoutDevice: logoutDevice,
logoutSession: logoutSession,
),
);
}
},
),
),
error: (err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(authDevicesProvider),
),
loading: () => ResponseLoadingWidget(),
),
),
],
),
);
}
}

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