Compare commits

..

329 Commits

Author SHA1 Message Date
LittleSheep
98749f42c0 ⬆️ Upgrade deps 2024-08-17 19:18:51 +08:00
LittleSheep
f0e6bd64f4 ♻️ Refactor video player 2024-08-17 19:02:57 +08:00
LittleSheep
3bea3a114a Post alias 2024-08-17 18:44:20 +08:00
LittleSheep
454f711656 ⬆️ Upgrade deps 2024-08-16 23:27:38 +08:00
LittleSheep
82e4c923e7 📈 Simple log user share 2024-08-16 23:08:05 +08:00
LittleSheep
5b4d8282ae Re-google (firebase) 2024-08-16 22:59:34 +08:00
LittleSheep
cf767a1d94 💄 Optimized post editor 2024-08-16 21:06:50 +08:00
LittleSheep
af93a8386a ⬆️ Upgrade deps 2024-08-16 01:05:21 +08:00
LittleSheep
29ca263130 🚀 Launch 1.2.1+13 2024-08-16 01:03:55 +08:00
LittleSheep
7332f68d9c Live preview of post editor 2024-08-16 00:52:36 +08:00
LittleSheep
e9e6f3313e 👽 Use capital to deal with mfa 2024-08-13 10:54:42 +08:00
LittleSheep
85764c37c2 🚨 Fix livekit android complie issue
Following issue:
https://github.com/livekit/client-sdk-flutter/issues/569
2024-08-12 09:06:30 +08:00
LittleSheep
ef1f29f905 🐛 Fix edit post won't rollback thumbnail 2024-08-11 02:07:09 +08:00
LittleSheep
22026efa7d Thumbnail 2024-08-11 01:57:58 +08:00
LittleSheep
4a3e6a9e15 🚀 Launch 1.2.1+12 2024-08-11 00:50:25 +08:00
LittleSheep
00092ba7b6 Some useful options 2024-08-11 00:36:27 +08:00
LittleSheep
b5da8ece4a Use capital share link 2024-08-10 18:24:47 +08:00
LittleSheep
dfe9165bc9 🐛 Bug fixes on upload attachment 2024-08-10 01:17:31 +08:00
LittleSheep
3d45b54236 ⬆️ Upgrade flutter & deps 2024-08-10 01:16:40 +08:00
LittleSheep
7f63fe7f0e 💄 Better sidebar navigation 2024-08-10 00:51:54 +08:00
LittleSheep
bc5dbab9c5 Dismissible refresh notification 2024-08-10 00:49:21 +08:00
LittleSheep
9910fc7a92 Channel content auto refresh after long time background activity 2024-08-10 00:43:55 +08:00
LittleSheep
2356eac118 Better side navigation bar 2024-08-09 22:59:24 +08:00
LittleSheep
0135b8d838 Better screenshare 2024-08-09 22:40:05 +08:00
LittleSheep
8ec33ccbf4 🚨 Fix CarouselController import issue 2024-08-07 19:21:01 +08:00
LittleSheep
d267316a35 💄 Better emotes 2024-08-07 19:11:52 +08:00
LittleSheep
138da60e55 🚸 Prevent user from sending empty message 2024-08-07 19:02:49 +08:00
LittleSheep
4562c2f991 🐛 Fix able send space message 2024-08-07 18:31:26 +08:00
LittleSheep
8009f4ca9b 💄 Better sidebar navigation 2024-08-07 18:24:16 +08:00
LittleSheep
54dee9702b 🐛 Fix attachments max width 2024-08-07 14:34:41 +08:00
LittleSheep
94385564bd 🐛 Fix dupe attachment notification 2024-08-07 14:27:23 +08:00
LittleSheep
0b2309816f 🐛 Fix desktop panic when download things 2024-08-07 13:50:50 +08:00
LittleSheep
8283272a3b 🗑️ Fix mis-import 2024-08-07 01:49:03 +08:00
LittleSheep
eb02a47e9a 💄 Fixes and improvements 2024-08-07 01:47:53 +08:00
LittleSheep
7c0c1ec94f 💄 Optimize styles 2024-08-07 01:20:23 +08:00
LittleSheep
272044a77e 💄 Optimize logo in signup & signin popup 2024-08-07 01:06:57 +08:00
LittleSheep
39c22b1cf6 Sticker has pack id 2024-08-07 00:56:06 +08:00
LittleSheep
98c3bb912d Stickers auto resize 2024-08-07 00:52:34 +08:00
LittleSheep
035b92d9b8 Rollback sized container 2024-08-07 00:12:44 +08:00
LittleSheep
0bfc0bd61b 🌐 Update en translation 2024-08-07 00:08:29 +08:00
LittleSheep
de00a20eee 💄 Better call ui 2024-08-06 23:23:02 +08:00
LittleSheep
73982f48d6 🐛 Bug fixes 2024-08-06 20:00:13 +08:00
LittleSheep
1d36b30361 Video won't load until click 2024-08-06 19:39:07 +08:00
LittleSheep
dea743a307 Username hint 2024-08-06 18:34:46 +08:00
LittleSheep
c48bd3e758 Stickers hint 2024-08-06 18:18:40 +08:00
LittleSheep
56bbf73b5e Better sticker & able embed attachment into markdown 2024-08-06 16:24:47 +08:00
LittleSheep
4f6c5aa053 🐛 Bug fixes 2024-08-04 21:12:35 +08:00
LittleSheep
d8e79fb4f9 🚀 Launch 1.2.1+5 2024-08-04 20:49:11 +08:00
LittleSheep
06e0fa465b Article has special badge 2024-08-04 20:48:51 +08:00
LittleSheep
895a257f50 Better overflow effect 2024-08-04 20:43:25 +08:00
LittleSheep
d9804ba00b 🚸 Enhanced share feature 2024-08-04 18:32:16 +08:00
LittleSheep
62ff1c2f1c 🚀 Launch 1.2.1+4 2024-08-04 18:14:28 +08:00
LittleSheep
a157596a2e Optimize and fixes 2024-08-04 18:13:59 +08:00
LittleSheep
12102bf527 Limit content and read more in posts 2024-08-04 17:39:22 +08:00
LittleSheep
c00a018380 🐛 Fix draft box 2024-08-04 17:15:56 +08:00
LittleSheep
53b3cac4ca Show hint when dismissible error 2024-08-04 16:26:05 +08:00
LittleSheep
19eabfaba1 🚀 Launch 1.2.1+2 2024-08-04 13:27:14 +08:00
LittleSheep
ec2eadad6d 🐛 Fix bootstrapper icon issue 2024-08-04 12:59:13 +08:00
LittleSheep
54e176e75d 🐛 Fix post editor cannot reply either repost 2024-08-04 12:55:05 +08:00
LittleSheep
0a7ccaeefa 🐛 Fix attachment editor title overflow 2024-08-04 12:23:39 +08:00
LittleSheep
a5f093e185 🐛 Fix unauthorized wont load stickers 2024-08-04 11:10:25 +08:00
LittleSheep
a4f68dd175 🚀 Launch 1.2.1+1 2024-08-04 01:54:35 +08:00
LittleSheep
8067c35c70 Follow the manifest to load emotes 2024-08-04 01:53:52 +08:00
LittleSheep
ebe381053e Load emojis 2024-08-04 01:37:54 +08:00
LittleSheep
03f2470dae Basic sticker management 2024-08-04 01:03:09 +08:00
LittleSheep
ea434815cf Create sticker
 Single file mode attachment editor and more options
2024-08-03 21:29:48 +08:00
LittleSheep
bbea4b4359 🍱 Update app icons 2024-08-03 17:44:36 +08:00
LittleSheep
e0b485cc81 🐛 Fix mis-style 2024-08-03 14:00:52 +08:00
LittleSheep
87bb37ac01 ⚗️ Markdown embed content 2024-08-03 12:29:13 +08:00
LittleSheep
989b5babd9 Auto update checking 2024-08-03 01:14:42 +08:00
LittleSheep
9ea364640d 🚀 Launch 1.2.0+8 2024-08-02 23:24:36 +08:00
LittleSheep
a9f55a489d ⬆️ Clean and upgrade packages 2024-08-02 23:22:50 +08:00
LittleSheep
4616f3a3e2 Friend request indicator 2024-08-02 23:15:28 +08:00
LittleSheep
425bae9d13 💄 Better friend page loading indicator 2024-08-02 22:54:56 +08:00
LittleSheep
07771e8979 Improve the speed of fetching attachments meta via batch api 2024-08-02 22:46:48 +08:00
LittleSheep
0ad4854443 💄 Grid view in call 2024-08-02 21:12:37 +08:00
LittleSheep
4238ea6fdc Call grid layout 2024-08-02 18:49:28 +08:00
LittleSheep
7d45c06302 💄 Optimized signal indicator 2024-08-02 18:29:01 +08:00
LittleSheep
7e8993fbd2 💫 Auto hide or show call controls 2024-08-02 18:09:07 +08:00
LittleSheep
c88fcc84da Show call participants 2024-08-02 17:14:23 +08:00
LittleSheep
11fb79623e Attachment can link exists things
 Optimize upload progress
2024-08-02 15:49:32 +08:00
LittleSheep
98cc313a91 💫 Optimize chat event list animation 2024-08-02 14:14:09 +08:00
LittleSheep
bc3401a897 🐛 Fix post item color mismatch 2024-08-02 05:10:10 +08:00
LittleSheep
5b6a5d9046 🐛 Fix post popup color mismatch 2024-08-02 05:04:31 +08:00
LittleSheep
6cbd78e836 💫 Optimize post editor transition 2024-08-02 04:59:35 +08:00
LittleSheep
aefcbad02f 💫 Better animated post list 2024-08-02 04:42:38 +08:00
LittleSheep
70617be687 💫 Animated chat 2024-08-02 04:24:12 +08:00
LittleSheep
cccb3d5c16 🐛 Fix post won't refresh after post 2024-08-02 01:00:31 +08:00
LittleSheep
a0a3a8d182 DM message last preview 2024-08-02 00:54:19 +08:00
LittleSheep
c6b2ef8459 💄 Better about 2024-08-02 00:41:12 +08:00
LittleSheep
34a2fe3988 Move about page link from account to settings 2024-08-02 00:29:51 +08:00
LittleSheep
0a5604d0ff Crop image in personalize 2024-08-02 00:12:16 +08:00
LittleSheep
5e754ad233 💫 About page icon will rotate 2024-08-01 23:51:03 +08:00
LittleSheep
5b9c92e4d3 Crop image 2024-08-01 23:44:07 +08:00
LittleSheep
b2a6ca7244 Improve attachments queue performance 2024-08-01 23:10:19 +08:00
LittleSheep
27c60fc8cb Block user action when attachments isn't ready 2024-08-01 22:36:00 +08:00
LittleSheep
8b3c45ab29 Queued upload 2024-08-01 22:13:08 +08:00
LittleSheep
adb415700a 💄 Optimized attachment edit action 2024-08-01 17:19:55 +08:00
LittleSheep
1e4b44a78b 💄 Better attachment editor previewing 2024-08-01 16:45:18 +08:00
LittleSheep
9765b200b9 🐛 Fix content previewing will show attachments 2024-08-01 16:28:48 +08:00
LittleSheep
47d03ce1e5 🐛 Bug fixes 2024-08-01 16:09:09 +08:00
LittleSheep
c41a71388d Post with publish at and until 2024-08-01 15:49:42 +08:00
LittleSheep
7655dfdf37 Post publish zone 2024-08-01 15:21:43 +08:00
LittleSheep
190bb34958 Markdown toolbar 2024-08-01 14:46:01 +08:00
LittleSheep
d02ed68afa Mention user in chat 2024-08-01 14:01:12 +08:00
LittleSheep
2bc4513bb6 🐛 Fix post tag input issue 2024-08-01 11:49:28 +08:00
LittleSheep
f10393f6d0 Download attachment 2024-08-01 02:10:57 +08:00
LittleSheep
ecef8dab0c Fix post list ui jank 2024-08-01 01:21:27 +08:00
LittleSheep
52e58fce3d Make theme switcher easier to use 2024-07-31 22:48:22 +08:00
LittleSheep
31d50bfb1f 🐛 Fix web url issue 2024-07-31 21:01:32 +08:00
LittleSheep
ca8ad12d93 🍱 Update font 2024-07-31 20:45:36 +08:00
LittleSheep
f799900450 🐛 Fix crash on ratio 1 in attachment 2024-07-31 20:45:16 +08:00
LittleSheep
dfdf7b23c8 🐛 Fix theme switching 2024-07-31 13:29:26 +08:00
LittleSheep
771b2029b0 🍱 Add fonts 2024-07-31 13:29:17 +08:00
LittleSheep
cc9c99f375 Global theme color 2024-07-31 02:44:49 +08:00
LittleSheep
b70d3795d1 Better tags input 2024-07-31 02:00:03 +08:00
LittleSheep
a16ff1b9a1 🍱 Update app icon 2024-07-30 21:24:30 +08:00
LittleSheep
19751617cb Able to edit visibility 2024-07-30 20:49:01 +08:00
LittleSheep
bb77b74356 Able to post article 2024-07-30 16:53:13 +08:00
LittleSheep
fc77c8693f Post editor able to edit article 2024-07-30 16:44:04 +08:00
LittleSheep
58bb549217 Post content local cache 2024-07-30 16:29:30 +08:00
LittleSheep
6590062dcb Post overview w/ content length limit indicator 2024-07-30 14:49:26 +08:00
LittleSheep
6ace977bf6 💄 Better fullscreen attachment viewer 2024-07-30 12:22:57 +08:00
LittleSheep
387f0d14ac ⬆️ Upgrade packages 2024-07-30 12:21:39 +08:00
LittleSheep
18bb0d3db2 🍱 Update app icon for v1.2.0 2024-07-30 11:50:26 +08:00
LittleSheep
8ab3ca5633 ⬆️ Support latest Paperclip 2024-07-29 18:06:38 +08:00
LittleSheep
3db6850d89 🐛 Fix attachment displaying according the latest server 2024-07-29 17:56:36 +08:00
LittleSheep
3ca98fa58c 🐛 Fix share link issue 2024-07-27 20:37:04 +08:00
LittleSheep
425c79d6fc 🚀 Launch the last version of 1.1.0 2024-07-27 20:34:02 +08:00
LittleSheep
7e98edfbc9 🐛 Fix web issue 2024-07-27 20:27:29 +08:00
LittleSheep
056b98db07 🐛 Fix web 404 issue 2024-07-27 19:58:44 +08:00
LittleSheep
7bfbd37b76 🐛 Fix attachment fullscreen in dark mode 2024-07-27 19:52:22 +08:00
LittleSheep
7800a70ef2 Deep link 2024-07-27 19:20:53 +08:00
LittleSheep
74b6ccd5c7 🐛 Fix share 2024-07-27 14:32:31 +08:00
LittleSheep
6ca4aad1c4 💄 Better bootstrapping 2024-07-27 14:16:49 +08:00
LittleSheep
102df2ef1c Share 2024-07-27 02:11:59 +08:00
LittleSheep
f08c9903b4 Bootstrapper 2024-07-27 01:39:20 +08:00
LittleSheep
0d279842cf 💄 Better full screen attachment display 2024-07-27 00:20:11 +08:00
LittleSheep
33d69908a6 Social credit points & quick send friend request 2024-07-26 22:37:08 +08:00
LittleSheep
4552dfd3f3 Pinned post & Total vote counts 2024-07-26 18:23:51 +08:00
LittleSheep
ae87e9ad31 💄 Optimized album page 2024-07-26 17:35:54 +08:00
LittleSheep
277ba69513 Account profile page 2024-07-26 16:53:05 +08:00
LittleSheep
6e3d0f9787 💄 Better attachments in posts 2024-07-26 14:21:00 +08:00
LittleSheep
0237409d27 🐛 Fix search with tag won't work 2024-07-26 01:31:45 +08:00
LittleSheep
a5b6ace79b 💄 Better attachments list styles 2024-07-26 01:16:32 +08:00
LittleSheep
42c3e5ff0a Shuffle mode swiper 2024-07-25 16:08:46 +08:00
LittleSheep
7dc198f0a7 ♻️ Post list controller layer 2024-07-25 14:42:50 +08:00
LittleSheep
fa3ba0e188 Shuffle mode 2024-07-25 02:00:29 +08:00
LittleSheep
02c28533db 💄 Optimized post create popup 2024-07-25 01:43:50 +08:00
LittleSheep
6d92a16a62 ♻️ Refactored auth system 2024-07-25 01:18:47 +08:00
LittleSheep
ef58430060 🐛 Fix error when body haven't attachment in post 2024-07-24 16:28:29 +08:00
LittleSheep
8366bda846 ♻️ Refactored friend module 2024-07-24 01:17:41 +08:00
LittleSheep
39c8597428 🐛 Fix notification list render issue 2024-07-23 22:09:20 +08:00
LittleSheep
e91b4b0947 ⬆️ Support latest version of server 2024-07-23 18:09:41 +08:00
LittleSheep
3545a0737d 🐛 Fix macos ITMS-90894 2024-07-23 11:19:27 +08:00
LittleSheep
58b3d75896 🐛 Fix NSE 2024-07-23 11:18:06 +08:00
LittleSheep
f69339292b ⚗️ Add NSE into macos platform 2024-07-22 00:04:12 +08:00
LittleSheep
62edab0131 Bug fixes in notification and support iOS Communication Notification! 2024-07-21 23:43:18 +08:00
LittleSheep
dbd05dbb79 🔨 Fix iOS building 2024-07-20 19:29:23 +08:00
LittleSheep
dac7440477 🍺 Add experimental iOS notification service extensions 2024-07-20 16:12:26 +08:00
LittleSheep
0573ee456e ♻️ Improved image analyzer in attachments 2024-07-19 23:56:59 +08:00
LittleSheep
5a7432e330 ⬆️ Support new notification APIs 2024-07-19 23:38:25 +08:00
LittleSheep
6811d8e9b1 Optimization and show stack trace in error dialog 2024-07-17 11:38:25 +08:00
LittleSheep
e068c72b69 🔨 Build linux workflow 2024-07-17 11:20:18 +08:00
LittleSheep
ca72a44a86 🔨 Make windows artificial smaller [skip ci] 2024-07-16 21:07:57 +08:00
LittleSheep
47d3fc90a3 🔨 Fix gh actions workflow android missing java 2024-07-16 20:48:52 +08:00
LittleSheep
e38d8339f1 🔨 Update github action workflow 2024-07-16 20:44:13 +08:00
LittleSheep
8c04b81b7c 💚 Fix action workflow 2024-07-16 20:34:47 +08:00
LittleSheep
9f3485a2a8 🔨 Add github action 2024-07-16 20:33:10 +08:00
LittleSheep
da265da61d ⬆️ Upgrade to support latest version of server 2024-07-16 19:46:53 +08:00
LittleSheep
286dd8193d 🐛 Fix crashes on android 2024-07-13 21:03:56 +08:00
LittleSheep
6311322c4a Suspended account tip 2024-07-13 19:09:04 +08:00
LittleSheep
a68a78597e 💄 Optimized for navigation drawer 2024-07-13 18:54:08 +08:00
LittleSheep
201c38800b 🐛 Fix drawer unreasonable round corner 2024-07-12 23:43:41 +08:00
LittleSheep
df7348e117 Add max height to attachments 2024-07-12 22:50:52 +08:00
LittleSheep
156e6f1075 Adaptive app bar leading 2024-07-12 22:37:58 +08:00
LittleSheep
a2db9a7ae4 App bar leading icon for drawer 2024-07-12 22:31:45 +08:00
LittleSheep
1a26880719 ♻️ Chat listening on sidebar 2024-07-12 21:59:16 +08:00
LittleSheep
aa43eaa0eb ♻️ Refactored navigation 2024-07-12 16:19:54 +08:00
LittleSheep
48b76ed574 Account status on sidebar 2024-07-12 13:15:46 +08:00
LittleSheep
3b1b6ec8d6 Drawer navigation 2024-07-12 11:39:44 +08:00
LittleSheep
a6d8e2e311 💄 Better ui 2024-07-12 00:44:57 +08:00
LittleSheep
8dbf6ff4f3 Articles 2024-07-10 10:50:10 +08:00
LittleSheep
505290b2ae Basic article rendering (overview) 2024-07-10 00:44:10 +08:00
LittleSheep
065cda27e9 Articles writing 2024-07-09 23:06:55 +08:00
LittleSheep
fa600d6c69 Draft box 2024-07-09 22:39:44 +08:00
LittleSheep
a0fe3f918e Post draft 2024-07-09 21:23:38 +08:00
LittleSheep
10ed44d2e2 💄 Optimize tags 2024-07-08 19:56:03 +08:00
LittleSheep
b241956ce7 🐛 Fix focus track still exists after that track disappeared 2024-07-07 14:45:26 +08:00
LittleSheep
d4cd120431 🐛 Make system bar appear in call screen 2024-07-07 14:41:32 +08:00
LittleSheep
60d7df4496 Search with tag & category 2024-07-07 14:22:53 +08:00
LittleSheep
f7cc4420b3 Attachment preview 2024-07-07 13:38:43 +08:00
LittleSheep
5864041e57 Replace pasteboard and drop zone deps 2024-07-07 13:16:08 +08:00
LittleSheep
f231fc9ec0 Display post's tag 2024-07-07 12:33:54 +08:00
LittleSheep
75c753ef63 Optimized refresh credentials 2024-07-07 11:56:25 +08:00
LittleSheep
22ee817676 Support new feed API
 Able to add tag onto post
2024-07-07 11:46:48 +08:00
LittleSheep
f8bed6946e 💄 Better posting page 2024-07-07 03:02:10 +08:00
LittleSheep
b2a2d38c3d 💄 Better macos window 2024-07-07 02:45:13 +08:00
LittleSheep
343b84e3e1 Better post list 2024-07-06 21:14:19 +08:00
LittleSheep
66ddfea68d 💄 Better bottom navigation 2024-07-06 20:55:53 +08:00
LittleSheep
a304b26c96 ♻️ Optimized video lib 2024-07-06 19:07:46 +08:00
LittleSheep
7d087af4cd Optimized attachments 2024-07-06 18:35:43 +08:00
LittleSheep
90daff5b97 Optimized channel list 2024-07-06 18:17:54 +08:00
LittleSheep
cc59814b55 Optimized websocket 2024-07-06 17:39:19 +08:00
LittleSheep
b808c76ea3 Optimized chat messages 2024-07-06 17:12:57 +08:00
LittleSheep
20a82da2fa Basic optimization of repainting 2024-07-05 23:37:54 +08:00
LittleSheep
867b024285 🐛 Bug fixes in sign up 2024-07-02 23:26:17 +08:00
LittleSheep
f1abdad54d Password reset 2024-06-30 18:03:02 +08:00
LittleSheep
9d54b04f77 💄 Better paste & drag 'n drop handling 2024-06-30 17:43:36 +08:00
LittleSheep
8c7de68e7a 💄 Optimize window on windows 2024-06-29 23:49:18 +08:00
LittleSheep
31513b0e84 🐛 Fix attachment meta won't load 2024-06-29 22:35:56 +08:00
LittleSheep
65333ccef6 💄 Optimize platform specfic code 2024-06-29 22:29:21 +08:00
LittleSheep
49f999871a Paste to upload 2024-06-29 21:03:15 +08:00
LittleSheep
e336d2372a Drag to upload 2024-06-29 20:25:29 +08:00
LittleSheep
85bba21285 💄 Sending message indicator 2024-06-29 18:40:26 +08:00
LittleSheep
df2f04d2b2 🐛 Bug fixes 2024-06-29 18:29:30 +08:00
LittleSheep
fdeb52bf38 💄 Better post rendering 2024-06-29 18:19:52 +08:00
LittleSheep
fffad00f00 💄 Better multi-factor authenticate callback experience
 Support custom app protocol solink://
2024-06-29 18:09:56 +08:00
LittleSheep
6b0f644353 🐛 Fix bugs and optimize your auth experience 2024-06-29 17:35:18 +08:00
LittleSheep
7b45d95fd6 🐛 Fix call last duration issue 2024-06-28 19:36:48 +08:00
LittleSheep
424be16ab0 🐛 Fix null value in event body 2024-06-28 19:33:59 +08:00
LittleSheep
29a975235c 🍱 Update android manifest 2024-06-28 19:33:49 +08:00
LittleSheep
d93a00066a 🐛 Fix translation key issue 2024-06-28 16:57:55 +08:00
LittleSheep
a0fdf915cf 🌐 Fix translation mistakes 2024-06-28 16:26:33 +08:00
LittleSheep
a7035581e9 🐛 Fix events applying issue 2024-06-28 14:56:57 +08:00
LittleSheep
793ad156e3 🐛 Fix web messaging module 2024-06-28 04:54:03 +08:00
LittleSheep
693725d5ae 🚀 Change dev flag to launch new changes 2024-06-28 04:38:34 +08:00
cb99fa3444 🔀 Merge pull request '⬆️ 升级支持服务器的 Event Based Messages' (#2) from experimental/event-based-messages into master
Reviewed-on: #2
2024-06-27 20:37:32 +00:00
LittleSheep
459469998b Call records 2024-06-28 04:34:35 +08:00
LittleSheep
ec14d1e3b3 💄 Restyle quote 2024-06-28 02:55:05 +08:00
LittleSheep
44833bb87f Quote messages 2024-06-28 02:49:28 +08:00
LittleSheep
78f9ad941b Basic event based rendering 2024-06-28 00:59:11 +08:00
LittleSheep
bbc9ea69f7 ♻️ Basic things to move to events system 2024-06-28 00:05:43 +08:00
LittleSheep
e84bca8948 Almost everywhere click avatar can open popup profile 2024-06-27 15:57:02 +08:00
LittleSheep
f239fbbed6 🐛 Fix bottom navigation everywhere 2024-06-27 15:50:13 +08:00
LittleSheep
b913c6a432 Large screen support 2024-06-27 14:56:09 +08:00
LittleSheep
977cc2e524 💄 Chat large screen support 2024-06-27 14:31:15 +08:00
LittleSheep
43242de659 💄 Optimize styles 2024-06-27 12:33:43 +08:00
LittleSheep
6260e63b09 Custom status 2024-06-27 11:44:27 +08:00
LittleSheep
6caad19365 🎨 Change naming's way 2024-06-27 11:05:15 +08:00
LittleSheep
4f5762c5a9 User last seen 2024-06-27 11:04:06 +08:00
LittleSheep
42acffff3e Preset statuses 2024-06-27 01:33:03 +08:00
LittleSheep
834eed652d 🐛 Fix post item mis-styled 2024-06-27 00:34:22 +08:00
LittleSheep
3d8315d09b 🐛 Fix post item missing avatar in replies 2024-06-27 00:33:49 +08:00
LittleSheep
d91680ada7 Status basis 2024-06-27 00:31:03 +08:00
LittleSheep
7c35323279 🐛 Fix message history display latency 2024-06-24 22:25:17 +08:00
LittleSheep
7034ff80db iOS clear all push notification when app become active 2024-06-24 13:00:01 +08:00
LittleSheep
a0a002974c 💄 Better attachment display 2024-06-23 20:18:55 +08:00
LittleSheep
580d9c7151 🐛 Fix late not initialized 2024-06-23 19:41:50 +08:00
6d755fc1b7 🔀 Merge pull request '♻️ 使用 SQLITE 来存储本地消息记录' (#1) from features/local-message-history into master
Reviewed-on: #1
2024-06-23 11:13:41 +00:00
LittleSheep
8036930084 🐛 Bug fixes on duplicate message 2024-06-23 19:13:07 +08:00
LittleSheep
d0cd75d653 Now can load more messages via click the tile 2024-06-23 19:02:41 +08:00
LittleSheep
aa8eec1a5a ♻️ Use controller instead of state to manage history 2024-06-23 18:51:49 +08:00
LittleSheep
2038d33a31 Load more messages 2024-06-23 18:03:46 +08:00
LittleSheep
52e5dd6860 ♻️ Moved the chat page to use local db 2024-06-23 13:27:21 +08:00
LittleSheep
34706531ad Local message history db 2024-06-23 12:29:07 +08:00
LittleSheep
c8d23e7632 🐛 Fix display time isn't local 2024-06-23 11:29:34 +08:00
LittleSheep
e221016c8d Better chat screen 2024-06-23 02:25:45 +08:00
LittleSheep
bb67edd227 Better post display
🐛 Fix realm with post
2024-06-23 01:52:05 +08:00
LittleSheep
4144bb307e 💄 MacOS specialized window 2024-06-22 23:59:11 +08:00
LittleSheep
ee1922b1b5 💄 Beautifier window on macOS and desktop platform 2024-06-22 23:20:31 +08:00
LittleSheep
09e9a30eef 🎨 Improve code structure 2024-06-22 22:39:32 +08:00
LittleSheep
27e5b4ca6f ⬆️ Upgrade web project 2024-06-09 23:30:01 +08:00
LittleSheep
88c99b7467 🐛 Bug fixes and optimization 2024-06-09 23:00:11 +08:00
LittleSheep
0f24ac03f7 Notify level in channel 2024-06-09 00:09:01 +08:00
LittleSheep
6acbd1ee9e 🐛 Bug fixes and optimization 2024-06-08 21:47:51 +08:00
LittleSheep
e88a0ddb22 About page 2024-06-08 13:28:49 +08:00
LittleSheep
78c0323908 📝 Update README 2024-06-08 13:08:57 +08:00
LittleSheep
879bd4e4db Seamless push notification activation 2024-06-07 19:50:50 +08:00
LittleSheep
3fad6e6b51 Use APNs on iOS/macOS 2024-06-07 00:17:45 +08:00
LittleSheep
0d179f6544 Firebase push notification 2024-06-07 00:00:28 +08:00
LittleSheep
0b8daad945 Add firebase 2024-06-06 23:28:19 +08:00
LittleSheep
4b2ac8894d 🐛 Fix no safe area 2024-06-06 21:07:11 +08:00
LittleSheep
ce15944018 ♻️ Better http client management, no more expired token 2024-06-06 20:49:18 +08:00
LittleSheep
d1a8793550 Connection state notifier 2024-06-06 20:23:50 +08:00
LittleSheep
df7dd85a0c Optimized chat 2024-06-05 23:55:21 +08:00
LittleSheep
ca1a8a04cb 🐛 Fix replies 2024-06-04 23:29:05 +08:00
LittleSheep
f0f33f7bb3 💄 Optimize styles 2024-06-04 00:00:47 +08:00
LittleSheep
628b448e81 New badge 2024-06-03 23:41:09 +08:00
LittleSheep
6007bdff77 Badges 2024-06-03 23:36:46 +08:00
LittleSheep
6090367ed6 🐛 Fix messages opacity and couldn't reply 2024-06-02 23:38:34 +08:00
LittleSheep
19e243e277 🐛 Fix listen stream that already listened 2024-06-02 23:20:34 +08:00
LittleSheep
9287e3fc90 🐛 Bug fixes 2024-06-02 23:17:15 +08:00
LittleSheep
a8edd26ba2 Large screen sidebar 2024-06-02 22:45:54 +08:00
LittleSheep
eb82f35a34 🐛 Fix call lagging issue 2024-06-02 15:43:42 +08:00
LittleSheep
16844294e1 🐛 Change manifest application id 2024-06-02 15:15:39 +08:00
LittleSheep
85e5e5f144 🐛 Fix background mode usability 2024-06-02 15:13:18 +08:00
LittleSheep
bece579f9d Appbar in call 2024-06-02 15:08:11 +08:00
LittleSheep
8271852867 🎨 Format code 2024-06-02 14:45:43 +08:00
LittleSheep
456bac67f2 🐛 Fix scroll auto scroll back 2024-06-02 14:45:19 +08:00
LittleSheep
665615ae70 Popup userinfo 2024-06-02 14:42:07 +08:00
LittleSheep
0d1aa7ef08 🚀 The real launch of v1.1 2024-06-02 00:46:06 +08:00
LittleSheep
ad94f6999c ⬆️ Add sentry 2024-06-02 00:42:36 +08:00
LittleSheep
f6510bf4c2 🐛 Bug fixes 2024-06-02 00:39:50 +08:00
LittleSheep
19c3f07212 🐛 Fix permissions 2024-06-02 00:28:34 +08:00
LittleSheep
f41189d7e8 💄 Fix attachment loading bar 2024-06-02 00:25:12 +08:00
LittleSheep
bd4560fda3 🚚 Identifier changes 2024-06-01 22:00:32 +08:00
LittleSheep
50d6d0c9eb 🚀 Launch v1.1! 2024-06-01 21:50:37 +08:00
LittleSheep
b8aae5a4c0 💄 Changes for desktop windows 2024-06-01 21:50:24 +08:00
LittleSheep
99a51de9f6 🐛 Auto reconnect 2024-06-01 21:41:40 +08:00
LittleSheep
e96b49e3cd Improved attachments 2024-06-01 21:39:28 +08:00
LittleSheep
a651350104 🍱 Update icon 2024-06-01 20:54:43 +08:00
LittleSheep
5c625fc15a Full functional call 2024-06-01 20:18:25 +08:00
LittleSheep
508cba8ed3 Call button 2024-06-01 01:25:45 +08:00
LittleSheep
9a2e0756b8 Full functional message chat 2024-05-30 23:14:29 +08:00
LittleSheep
2716690c41 Better channel list 2024-05-30 22:02:54 +08:00
LittleSheep
30b05e440c Realm detail 2024-05-30 00:05:39 +08:00
LittleSheep
cd08e65840 Realm channels 2024-05-29 23:22:24 +08:00
LittleSheep
6bb29dfbc0 Realm posts 2024-05-29 22:42:11 +08:00
LittleSheep
5f06fc4f9d 💄 Better scrolling 2024-05-29 20:13:53 +08:00
LittleSheep
d4cbabeb31 Better DM 2024-05-29 00:14:41 +08:00
LittleSheep
c50a49f37d Realms creation 2024-05-28 22:13:23 +08:00
LittleSheep
99f3211151 Login hint 2024-05-28 20:13:36 +08:00
LittleSheep
9aceabd83c Video player! 2024-05-27 23:07:01 +08:00
LittleSheep
6e09414036 Channel member management 2024-05-27 21:21:10 +08:00
LittleSheep
ff9e1896b4 Channel detail 2024-05-26 23:13:43 +08:00
LittleSheep
c3bf0a19b8 Chat attachments 2024-05-26 21:03:25 +08:00
LittleSheep
9cb2b9122e Chat messaging 2024-05-26 13:39:21 +08:00
LittleSheep
5b45718ebd Chat basis 2024-05-26 01:21:08 +08:00
LittleSheep
657f36c1f8 Channel organize 2024-05-26 00:11:00 +08:00
LittleSheep
9eae49128e Replies 2024-05-25 17:21:27 +08:00
LittleSheep
daee3e8074 Post detail 2024-05-25 13:19:16 +08:00
LittleSheep
f376603482 Notifications 2024-05-25 13:00:40 +08:00
LittleSheep
806ae602d5 Completed friend list 2024-05-25 00:40:05 +08:00
LittleSheep
15ed75b04e 🐛 Bug fixes on bad switching account UX 2024-05-23 23:54:05 +08:00
LittleSheep
3e640768c8 Better account page 2024-05-23 22:11:42 +08:00
LittleSheep
d98d167f4c Friend list 2024-05-23 21:12:47 +08:00
LittleSheep
05f88fe3f3 💄 Optimize design 2024-05-23 20:00:26 +08:00
LittleSheep
a291e8af66 Personalize 2024-05-23 00:11:54 +08:00
260 changed files with 23132 additions and 2896 deletions

41
.github/workflows/nightly.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: release-nightly
on:
push:
branches: [master]
jobs:
build-web:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
- run: flutter build web --release --base-href=/
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-web
path: build/web
build-exe:
runs-on: windows-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
- run: flutter build windows
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-windows
path: build/windows/x64/runner/Release

View File

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d"
revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3"
channel: "stable"
project_type: app
@@ -13,26 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: android
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: ios
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: linux
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: macos
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
- platform: web
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: windows
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
# User provided section

View File

@@ -1,16 +1,3 @@
# solian
# Solian
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
The Solar Network application for all platform. Including Desktop, Mobile and the Web.

View File

@@ -7,6 +7,9 @@
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
analyzer:
errors:
use_build_context_synchronously: ignore
include: package:flutter_lints/flutter.yaml
linter:
@@ -21,8 +24,8 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
avoid_print: true # Uncomment to disable the `avoid_print` rule
prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -1,67 +1,76 @@
plugins {
id "com.android.application"
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localPropertiesFile.withReader("UTF-8") { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
if (flutterVersionCode == null) {
flutterVersionCode = '1'
flutterVersionCode = "1"
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
def flutterVersionName = localProperties.getProperty("flutter.versionName")
if (flutterVersionName == null) {
flutterVersionName = '1.0'
flutterVersionName = "1.0"
}
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
namespace "com.example.solian"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
namespace = "dev.solsynth.solian"
compileSdk = flutter.compileSdkVersion
ndkVersion = "26.1.10909125"
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.solian"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
applicationId = "dev.solsynth.solian"
minSdkVersion 23
multiDexEnabled true
targetSdk = flutter.targetSdkVersion
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
// signingConfig = signingConfigs.debug
signingConfig = signingConfigs.release
}
}
}
flutter {
source '../..'
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
}
dependencies {}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "961776991058",
"project_id": "solian-0x001",
"storage_bucket": "solian-0x001.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:961776991058:android:a8d3f7995b0b8e86f4188b",
"android_client_info": {
"package_name": "dev.solsynth.solian"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDvFNudXYs29uDtcCv6pFR8h5tXBs90FYk"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@@ -1,37 +1,89 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="solian"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:label="Solian"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:supportsRtl="true">
<receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"/>
<receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="solsynth.dev" />
<data android:host="sn.solsynth.dev" />
<data android:scheme="https" />
<data android:scheme="https" />
<data android:scheme="solink" />
</intent-filter>
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
android:name="flutterEmbedding"
android:value="2"/>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->

View File

@@ -1,4 +1,4 @@
package com.example.solian
package dev.solsynth.solian
import io.flutter.embedding.android.FlutterActivity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -1,16 +1,29 @@
allprojects {
ext.kotlin_version = "2.0.0"
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
// TO FIX LIVEKIT ISSUE BY THIS
// https://github.com/livekit/client-sdk-flutter/issues/569#issuecomment-2275686786
afterEvaluate { project ->
if (project.plugins.hasPlugin("com.android.application") ||
project.plugins.hasPlugin("com.android.library")) {
project.android {
compileSdkVersion 34
buildToolsVersion "34.0.0"
}
}
}
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {

View File

@@ -1,3 +1,6 @@
org.gradle.jvmargs=-Xmx4G
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip

View File

@@ -5,10 +5,9 @@ pluginManagement {
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
}()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
@@ -19,8 +18,10 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
id "com.android.application" version '8.4.0' apply false
id "com.google.gms.google-services" version "4.3.15" apply false
id "com.google.firebase.crashlytics" version "2.8.1" apply false
id "org.jetbrains.kotlin.android" version '2.0.0' apply false
}
include ":app"

BIN
assets/fonts/Comfortaa-Bold.ttf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/NotoSansHK-Bold.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/NotoSansJP-Bold.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/NotoSansSC-Bold.ttf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

BIN
assets/icon-w-shadow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 360 KiB

4
devtools_options.yaml Normal file
View File

@@ -0,0 +1,4 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
- provider: true

1
firebase.json Normal file
View File

@@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:android:a8d3f7995b0b8e86f4188b","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:ios:727229d368cc47e1f4188b","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:ios:727229d368cc47e1f4188b","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"solian-0x001","configurations":{"android":"1:961776991058:android:a8d3f7995b0b8e86f4188b","ios":"1:961776991058:ios:727229d368cc47e1f4188b","macos":"1:961776991058:ios:727229d368cc47e1f4188b","web":"1:961776991058:web:b91d12f2892a5609f4188b","windows":"1:961776991058:web:dcd731c8c5ce1281f4188b"}}}}}}

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -1,4 +1,9 @@
PODS:
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
@@ -33,62 +38,368 @@ PODS:
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/Analytics (10.29.0):
- Firebase/Core
- Firebase/Core (10.29.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 10.29.0)
- Firebase/CoreOnly (10.29.0):
- FirebaseCore (= 10.29.0)
- Firebase/Crashlytics (10.29.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 10.29.0)
- Firebase/Messaging (10.29.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 10.29.0)
- firebase_analytics (11.2.1):
- Firebase/Analytics (= 10.29.0)
- firebase_core
- Flutter
- firebase_core (3.3.0):
- Firebase/CoreOnly (= 10.29.0)
- Flutter
- firebase_crashlytics (4.0.4):
- Firebase/Crashlytics (= 10.29.0)
- firebase_core
- Flutter
- firebase_messaging (15.0.4):
- Firebase/Messaging (= 10.29.0)
- firebase_core
- Flutter
- FirebaseAnalytics (10.29.0):
- FirebaseAnalytics/AdIdSupport (= 10.29.0)
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 7.11)
- GoogleUtilities/MethodSwizzler (~> 7.11)
- GoogleUtilities/Network (~> 7.11)
- "GoogleUtilities/NSData+zlib (~> 7.11)"
- nanopb (< 2.30911.0, >= 2.30908.0)
- FirebaseAnalytics/AdIdSupport (10.29.0):
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleAppMeasurement (= 10.29.0)
- GoogleUtilities/AppDelegateSwizzler (~> 7.11)
- GoogleUtilities/MethodSwizzler (~> 7.11)
- GoogleUtilities/Network (~> 7.11)
- "GoogleUtilities/NSData+zlib (~> 7.11)"
- nanopb (< 2.30911.0, >= 2.30908.0)
- FirebaseCore (10.29.0):
- FirebaseCoreInternal (~> 10.0)
- GoogleUtilities/Environment (~> 7.12)
- GoogleUtilities/Logger (~> 7.12)
- FirebaseCoreExtension (10.29.0):
- FirebaseCore (~> 10.0)
- FirebaseCoreInternal (10.29.0):
- "GoogleUtilities/NSData+zlib (~> 7.8)"
- FirebaseCrashlytics (10.29.0):
- FirebaseCore (~> 10.5)
- FirebaseInstallations (~> 10.0)
- FirebaseRemoteConfigInterop (~> 10.23)
- FirebaseSessions (~> 10.5)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/Environment (~> 7.8)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesObjC (~> 2.1)
- FirebaseInstallations (10.29.0):
- FirebaseCore (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8)
- PromisesObjC (~> 2.1)
- FirebaseMessaging (10.29.0):
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleDataTransport (~> 9.3)
- GoogleUtilities/AppDelegateSwizzler (~> 7.8)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/Reachability (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8)
- nanopb (< 2.30911.0, >= 2.30908.0)
- FirebaseRemoteConfigInterop (10.29.0)
- FirebaseSessions (10.29.0):
- FirebaseCore (~> 10.5)
- FirebaseCoreExtension (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/Environment (~> 7.13)
- GoogleUtilities/UserDefaults (~> 7.13)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesSwift (~> 2.1)
- Flutter (1.0.0)
- flutter_keyboard_visibility (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- flutter_webrtc (0.11.3):
- Flutter
- WebRTC-SDK (= 125.6422.04)
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleAppMeasurement (10.29.0):
- GoogleAppMeasurement/AdIdSupport (= 10.29.0)
- GoogleUtilities/AppDelegateSwizzler (~> 7.11)
- GoogleUtilities/MethodSwizzler (~> 7.11)
- GoogleUtilities/Network (~> 7.11)
- "GoogleUtilities/NSData+zlib (~> 7.11)"
- nanopb (< 2.30911.0, >= 2.30908.0)
- GoogleAppMeasurement/AdIdSupport (10.29.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 10.29.0)
- GoogleUtilities/AppDelegateSwizzler (~> 7.11)
- GoogleUtilities/MethodSwizzler (~> 7.11)
- GoogleUtilities/Network (~> 7.11)
- "GoogleUtilities/NSData+zlib (~> 7.11)"
- nanopb (< 2.30911.0, >= 2.30908.0)
- GoogleAppMeasurement/WithoutAdIdSupport (10.29.0):
- GoogleUtilities/AppDelegateSwizzler (~> 7.11)
- GoogleUtilities/MethodSwizzler (~> 7.11)
- GoogleUtilities/Network (~> 7.11)
- "GoogleUtilities/NSData+zlib (~> 7.11)"
- nanopb (< 2.30911.0, >= 2.30908.0)
- GoogleDataTransport (9.4.1):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/AppDelegateSwizzler (7.13.3):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (7.13.3):
- GoogleUtilities/Privacy
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.13.3):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/MethodSwizzler (7.13.3):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/Network (7.13.3):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (7.13.3)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (7.13.3)
- GoogleUtilities/Reachability (7.13.3):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (7.13.3):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- image_cropper (0.0.4):
- Flutter
- TOCropViewController (~> 2.7.4)
- image_picker_ios (0.0.1):
- Flutter
- livekit_client (2.2.4):
- Flutter
- WebRTC-SDK (= 125.6422.04)
- nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0)
- nanopb/decode (2.30910.0)
- nanopb/encode (2.30910.0)
- package_info_plus (0.4.5):
- Flutter
- pasteboard (0.0.1):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- SDWebImage (5.19.2):
- SDWebImage/Core (= 5.19.2)
- SDWebImage/Core (5.19.2)
- permission_handler_apple (9.3.0):
- Flutter
- pointer_interceptor_ios (0.0.1):
- Flutter
- PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- protocol_handler_ios (0.0.1):
- Flutter
- SDWebImage (5.19.6):
- SDWebImage/Core (= 5.19.6)
- SDWebImage/Core (5.19.6)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite (0.0.3):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- TOCropViewController (2.7.4)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- wakelock_plus (0.0.1):
- Flutter
- WebRTC-SDK (125.6422.04)
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- livekit_client (from `.symlinks/plugins/livekit_client/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`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- Firebase
- FirebaseAnalytics
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations
- FirebaseMessaging
- FirebaseRemoteConfigInterop
- FirebaseSessions
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
- nanopb
- PromisesObjC
- PromisesSwift
- SDWebImage
- SwiftyGif
- TOCropViewController
- WebRTC-SDK
EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_analytics:
:path: ".symlinks/plugins/firebase_analytics/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_crashlytics:
:path: ".symlinks/plugins/firebase_crashlytics/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios"
gal:
:path: ".symlinks/plugins/gal/darwin"
image_cropper:
:path: ".symlinks/plugins/image_cropper/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
livekit_client:
:path: ".symlinks/plugins/livekit_client/ios"
package_info_plus:
: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"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d
firebase_analytics: 04491d1ee74c8e7c2330c96afc54188a969b06ee
firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb
firebase_crashlytics: e3d3e0c99bad5aaab5908385133dea8ec344693f
firebase_messaging: c862b3d2b973ecc769194dc8de09bd22c77ae757
FirebaseAnalytics: 23717de130b779aa506e757edb9713d24b6ffeda
FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16
FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f
FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934
FirebaseCrashlytics: 34647b41e18de773717fdd348a22206f2f9bc774
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366
FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d
FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleAppMeasurement: f9de05ee17401e3355f68e8fc8b5064d429f5918
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
livekit_client: d079c5f040d4bf2b80440ff0ae997725a183e4bc
nanopb: 438bc412db1928dac798aa6fd75726007be04262
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
pointer_interceptor_ios: 508241697ff0947f853c061945a8b822463947c1
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
SDWebImage: a79252b60f4678812d94316c91da69ec83089c9f
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
PODFILE CHECKSUM: a57f30d18f102dd3ce366b1d62a55ecbef2158e5
COCOAPODS: 1.15.2

View File

@@ -12,10 +12,13 @@
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
697629BCCB242B335F9740F6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 644CB23863BAC87225224BEB /* Pods_Runner.framework */; };
730D648E2C4AC4D0005A1975 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 730D648D2C4AC4D0005A1975 /* NotificationService.swift */; };
730D64922C4AC4D0005A1975 /* SolianNotifyExt.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 730D648B2C4AC4D0005A1975 /* SolianNotifyExt.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
DA05013449E99A927762ECFB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7E5383C11873DEAF66E16385 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -26,9 +29,27 @@
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
730D64902C4AC4D0005A1975 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 730D648A2C4AC4D0005A1975;
remoteInfo = SolianNotifyExt;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
730D64932C4AC4D0005A1975 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
730D64922C4AC4D0005A1975 /* SolianNotifyExt.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -49,10 +70,15 @@
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
644CB23863BAC87225224BEB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
730D648B2C4AC4D0005A1975 /* SolianNotifyExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianNotifyExt.appex; sourceTree = BUILT_PRODUCTS_DIR; };
730D648D2C4AC4D0005A1975 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
730D648F2C4AC4D0005A1975 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
73EB49922C11F3D300A080A2 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7507B5B1756DA08A398095AC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7E5383C11873DEAF66E16385 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
875B1905BB09FD3E418F83BE /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8C092932B0B7297947BE9263 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
925B960477E1606F0EF59C87 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
@@ -76,6 +102,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
730D64882C4AC4D0005A1975 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -104,6 +137,15 @@
name = Frameworks;
sourceTree = "<group>";
};
730D648C2C4AC4D0005A1975 /* SolianNotifyExt */ = {
isa = PBXGroup;
children = (
730D648D2C4AC4D0005A1975 /* NotificationService.swift */,
730D648F2C4AC4D0005A1975 /* Info.plist */,
);
path = SolianNotifyExt;
sourceTree = "<group>";
};
7BA6BD8939A7BE19A2C7086C /* Pods */ = {
isa = PBXGroup;
children = (
@@ -114,7 +156,6 @@
DD2E0D3CBC50FE4BCEF3770A /* Pods-RunnerTests.release.xcconfig */,
02A460D36C4C66B1E6179D1B /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
@@ -134,10 +175,12 @@
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
730D648C2C4AC4D0005A1975 /* SolianNotifyExt */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
7BA6BD8939A7BE19A2C7086C /* Pods */,
47971B15D8567924545E35C5 /* Frameworks */,
7E5383C11873DEAF66E16385 /* GoogleService-Info.plist */,
);
sourceTree = "<group>";
};
@@ -146,6 +189,7 @@
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
730D648B2C4AC4D0005A1975 /* SolianNotifyExt.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -153,6 +197,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
73EB49922C11F3D300A080A2 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -187,6 +232,23 @@
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
730D648A2C4AC4D0005A1975 /* SolianNotifyExt */ = {
isa = PBXNativeTarget;
buildConfigurationList = 730D64972C4AC4D0005A1975 /* Build configuration list for PBXNativeTarget "SolianNotifyExt" */;
buildPhases = (
730D64872C4AC4D0005A1975 /* Sources */,
730D64882C4AC4D0005A1975 /* Frameworks */,
730D64892C4AC4D0005A1975 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = SolianNotifyExt;
productName = SolianNotifyExt;
productReference = 730D648B2C4AC4D0005A1975 /* SolianNotifyExt.appex */;
productType = "com.apple.product-type.app-extension";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
@@ -196,13 +258,17 @@
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
730D64932C4AC4D0005A1975 /* Embed Foundation Extensions */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */,
0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */,
1A9FD6BE5DEE99CDA7399504 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */,
);
buildRules = (
);
dependencies = (
730D64912C4AC4D0005A1975 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
@@ -216,6 +282,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1540;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@@ -223,6 +290,9 @@
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
730D648A2C4AC4D0005A1975 = {
CreatedOnToolsVersion = 15.4;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
@@ -244,6 +314,7 @@
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
730D648A2C4AC4D0005A1975 /* SolianNotifyExt */,
);
};
/* End PBXProject section */
@@ -256,6 +327,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
730D64892C4AC4D0005A1975 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -264,12 +342,48 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
DA05013449E99A927762ECFB /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
1A9FD6BE5DEE99CDA7399504 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\"";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nPATH=${PATH}:$FLUTTER_ROOT/bin:$HOME/.pub-cache/bin\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=$PODS_ROOT/FirebaseCrashlytics/upload-symbols --platform=ios --apple-project-path=${SRCROOT} --env-platform-name=${PLATFORM_NAME} --env-configuration=${CONFIGURATION} --env-project-dir=${PROJECT_DIR} --env-built-products-dir=${BUILT_PRODUCTS_DIR} --env-dwarf-dsym-folder-path=${DWARF_DSYM_FOLDER_PATH} --env-dwarf-dsym-file-name=${DWARF_DSYM_FILE_NAME} --env-infoplist-path=${INFOPLIST_PATH} --default-config=default\n";
};
259653AE41D478F4C6BAE9B2 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -373,6 +487,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
730D64872C4AC4D0005A1975 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
730D648E2C4AC4D0005A1975 /* NotificationService.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -390,6 +512,11 @@
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
730D64912C4AC4D0005A1975 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 730D648A2C4AC4D0005A1975 /* SolianNotifyExt */;
targetProxy = 730D64902C4AC4D0005A1975 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -468,17 +595,21 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.solian;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -493,9 +624,10 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.solian.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -511,9 +643,10 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.solian.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -527,20 +660,135 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.solian.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
730D64942C4AC4D0005A1975 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
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;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
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 = SolianNotifyExt/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianNotifyExt;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.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.SolianNotifyExt;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
730D64952C4AC4D0005A1975 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
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;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
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 = SolianNotifyExt/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianNotifyExt;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.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.SolianNotifyExt;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
730D64962C4AC4D0005A1975 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
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;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
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 = SolianNotifyExt/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianNotifyExt;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.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.SolianNotifyExt;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -597,7 +845,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -651,17 +899,21 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.solian;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -674,17 +926,21 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.solian;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -705,6 +961,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
730D64972C4AC4D0005A1975 /* Build configuration list for PBXNativeTarget "SolianNotifyExt" */ = {
isa = XCConfigurationList;
buildConfigurations = (
730D64942C4AC4D0005A1975 /* Debug */,
730D64952C4AC4D0005A1975 /* Release */,
730D64962C4AC4D0005A1975 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@@ -1,13 +1,17 @@
import UIKit
import Flutter
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func applicationDidBecomeActive(_ application: UIApplication) {
application.applicationIconBadgeNumber = 0;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
@@ -14,13 +16,14 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-16" y="-40"/>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,30 @@
<?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>API_KEY</key>
<string>AIzaSyCzQIyiYKoYHTpGXhN-IjgMML8z797WVD8</string>
<key>GCM_SENDER_ID</key>
<string>961776991058</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>dev.solsynth.solian</string>
<key>PROJECT_ID</key>
<string>solian-0x001</string>
<key>STORAGE_BUCKET</key>
<string>solian-0x001.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:961776991058:ios:727229d368cc47e1f4188b</string>
</dict>
</plist>

View File

@@ -2,6 +2,23 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>solink</string>
</array>
</dict>
</array>
<key>FirebaseMessagingAutoInitEnabled</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -22,24 +39,41 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Allow you take photo/video for your message or post</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow you record audio for your message or post</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow you add photo to your message or post</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>remote-notification</string>
<string>voip</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow you add photo to your message or post</string>
<key>NSCameraUsageDescription</key>
<string>Allow you take photo/video for your message or post</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow you record audio for your message or post</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
@@ -47,9 +81,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,17 @@
<?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>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:solsynth.dev</string>
<string>applinks:solsynth.dev</string>
<string>webcredentials:sn.solsynth.dev</string>
<string>applinks:sn.solsynth.dev</string>
</array>
<key>com.apple.developer.usernotifications.communication</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,18 @@
<?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>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
<string>INSendMessageIntent</string>
</array>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,81 @@
//
// NotificationService.swift
// SolianNotifyExt
//
// Created by LittleSheep on 2024/7/19.
//
import UserNotifications
import Intents
enum ParseNotificationPayloadError: Error {
case noMetadata(String)
case noAvatarUrl(String)
}
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
do {
switch bestAttemptContent.categoryIdentifier {
case "messaging.message", "messaging.callStart":
guard let metadata = bestAttemptContent.userInfo["metadata"] as? [AnyHashable : Any] else {
throw ParseNotificationPayloadError.noMetadata("The notification has no metadata.")
}
guard let avatarUrl = bestAttemptContent.userInfo["avatar"] as? String else {
throw ParseNotificationPayloadError.noMetadata("The notification has no avatar url.")
}
let handle = INPersonHandle(value: String(metadata["user_id"] as! Int), type: .unknown)
let avatar = INImage(
url: URL(string: avatarUrl)!
)!
let sender = INPerson(personHandle: handle,
nameComponents: nil,
displayName: bestAttemptContent.title,
image: avatar,
contactIdentifier: nil,
customIdentifier: nil)
let intent = INSendMessageIntent(recipients: nil,
outgoingMessageType: .outgoingMessageText,
content: bestAttemptContent.body,
speakableGroupName: nil,
conversationIdentifier: String(metadata["channel_id"] as! Int),
serviceName: nil,
sender: sender,
attachments: nil)
let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .incoming
interaction.donate(completion: nil)
let updatedContent = try request.content.updating(from: intent)
contentHandler(updatedContent)
break
default:
contentHandler(bestAttemptContent)
break
}
} catch {
contentHandler(bestAttemptContent)
}
}
}
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}

265
lib/bootstrapper.dart Normal file
View File

@@ -0,0 +1,265 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:solian/exts.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/sized_container.dart';
class BootstrapperShell extends StatefulWidget {
final Widget child;
const BootstrapperShell({super.key, required this.child});
@override
State<BootstrapperShell> createState() => _BootstrapperShellState();
}
class _BootstrapperShellState extends State<BootstrapperShell> {
bool _isBusy = true;
bool _isErrored = false;
bool _isDismissable = true;
String? _subtitle;
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
int _periodCursor = 0;
late final List<({String label, Future<void> Function() action})> _periods = [
(
label: 'bsLoadingTheme',
action: () async {
await context.read<ThemeSwitcher>().restoreTheme();
},
),
(
label: 'bsCheckForUpdate',
action: () async {
if (PlatformInfo.isWeb) return;
try {
final info = await PackageInfo.fromPlatform();
final localVersionString = '${info.version}+${info.buildNumber}';
final resp = await GetConnect().get(
'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?limit=1',
);
if (resp.body[0]['name'] != localVersionString) {
setState(() {
_isErrored = true;
_subtitle = PlatformInfo.isIOS || PlatformInfo.isMacOS
? 'bsCheckForUpdateDescApple'.tr
: 'bsCheckForUpdateDescCommon'.tr;
});
}
} catch (e) {
setState(() {
_isErrored = true;
_subtitle = 'bsCheckForUpdateFailed'.tr;
});
}
},
),
(
label: 'bsCheckingServer',
action: () async {
final client = ServiceFinder.configureClient('dealer');
final resp = await client.get('/.well-known');
if (resp.statusCode != null && resp.statusCode != 200) {
setState(() {
_isErrored = true;
_subtitle = 'bsCheckingServerDown'.tr;
_isDismissable = false;
});
throw Exception('unable connect to server');
} else if (resp.statusCode == null) {
setState(() {
_isErrored = true;
_subtitle = 'bsCheckingServerFail'.tr;
_isDismissable = false;
});
throw Exception('unable connect to server');
}
},
),
(
label: 'bsAuthorizing',
action: () async {
final AuthProvider auth = Get.find();
await auth.refreshAuthorizeStatus();
if (auth.isAuthorized.isTrue) {
await auth.refreshUserProfile();
}
},
),
(
label: 'bsEstablishingConn',
action: () async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isTrue) {
await Get.find<WebSocketProvider>().connect();
}
},
),
(
label: 'bsPreparingData',
action: () async {
final AuthProvider auth = Get.find();
await Future.wait([
Get.find<StickerProvider>().refreshAvailableStickers(),
if (auth.isAuthorized.isTrue)
Get.find<ChannelProvider>().refreshAvailableChannel(),
if (auth.isAuthorized.isTrue)
Get.find<RelationshipProvider>().refreshRelativeList(),
if (auth.isAuthorized.isTrue)
Get.find<RealmProvider>().refreshAvailableRealms(),
]);
},
),
(
label: 'bsRegisteringPushNotify',
action: () async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isTrue) {
try {
Get.find<WebSocketProvider>().registerPushNotifications();
} catch (err) {
context.showSnackbar(
'pushNotifyRegisterFailed'.trParams({'reason': err.toString()}),
);
}
}
},
),
];
Future<void> _runPeriods() async {
try {
for (var idx = 0; idx < _periods.length; idx++) {
await _periods[idx].action();
if (_isErrored && !_isDismissable) break;
if (_periodCursor < _periods.length - 1) {
setState(() => _periodCursor++);
}
}
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_runPeriods();
}
@override
Widget build(BuildContext context) {
if (_isBusy || _isErrored) {
return GestureDetector(
child: Material(
color: Theme.of(context).colorScheme.surface,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
SizedBox(
height: 280,
child: Align(
alignment: Alignment.bottomCenter,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child:
Image.asset('assets/logo.png', width: 80, height: 80),
),
),
),
Column(
children: [
if (_isErrored && !_isDismissable && !_isBusy)
const Icon(Icons.cancel, size: 24),
if (_isErrored && _isDismissable && !_isBusy)
const Icon(Icons.warning, size: 24),
if ((_isErrored && _isDismissable && _isBusy) || _isBusy)
const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 3),
),
const SizedBox(height: 12),
CenteredContainer(
maxWidth: 280,
child: Column(
children: [
if (_subtitle == null)
Text(
'${_periods[_periodCursor].label.tr} (${_periodCursor + 1}/${_periods.length})',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: _unFocusColor,
),
),
if (_subtitle != null)
Text(
_subtitle!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: _unFocusColor,
),
).paddingOnly(bottom: 4),
if (!_isBusy && _isErrored && _isDismissable)
Text(
'bsDismissibleErrorHint'.tr,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: _unFocusColor,
),
).paddingOnly(bottom: 5),
Text(
'2024 © Solsynth LLC',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11,
color: _unFocusColor,
),
),
],
),
),
],
),
],
),
),
onTap: () {
if (_isBusy) return;
if (_isDismissable) {
setState(() {
_isBusy = false;
_isErrored = false;
});
} else {
setState(() {
_isBusy = true;
_isErrored = false;
_periodCursor = 0;
});
_runPeriods();
}
},
);
}
return widget.child;
}
}

View File

@@ -0,0 +1,177 @@
import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/message/adaptor.dart';
import 'package:solian/providers/message/events.dart';
class ChatEventController {
late final MessageHistoryDb database;
final RxList<LocalEvent> currentEvents = RxList.empty(growable: true);
final RxInt totalEvents = 0.obs;
final RxBool isLoading = false.obs;
Channel? channel;
String? scope;
Future<void> initialize() async {
if (!PlatformInfo.isWeb) {
database = await createHistoryDb();
}
currentEvents.clear();
}
Future<LocalEvent?> getEvent(int id) async {
if (channel == null || scope == null) return null;
if (PlatformInfo.isWeb) {
final remoteRecord = await getRemoteEvent(id, channel!, scope!);
if (remoteRecord == null) return null;
return LocalEvent(
remoteRecord.id,
remoteRecord,
remoteRecord.channelId,
remoteRecord.createdAt,
);
} else {
return await database.getEvent(id, channel!, scope: scope!);
}
}
Future<void> getEvents(Channel channel, String scope) async {
this.channel = channel;
this.scope = scope;
syncLocal(channel);
isLoading.value = true;
if (PlatformInfo.isWeb) {
final result = await getRemoteEvents(
channel,
scope,
remainDepth: 3,
offset: 0,
);
totalEvents.value = result?.$2 ?? 0;
if (result != null) {
for (final x in result.$1.reversed) {
final entry = LocalEvent(x.id, x, x.channelId, x.createdAt);
insertEvent(entry);
applyEvent(entry);
}
}
} else {
final result = await database.syncRemoteEvents(
channel,
scope: scope,
);
totalEvents.value = result?.$2 ?? 0;
await syncLocal(channel);
}
isLoading.value = false;
}
Future<void> loadEvents(Channel channel, String scope) async {
isLoading.value = true;
if (PlatformInfo.isWeb) {
final result = await getRemoteEvents(
channel,
scope,
remainDepth: 3,
offset: currentEvents.length,
);
if (result != null) {
totalEvents.value = result.$2;
for (final x in result.$1.reversed) {
final entry = LocalEvent(x.id, x, x.channelId, x.createdAt);
currentEvents.add(entry);
applyEvent(entry);
}
}
} else {
final result = await database.syncRemoteEvents(
channel,
depth: 3,
scope: scope,
offset: currentEvents.length,
);
totalEvents.value = result?.$2 ?? 0;
await syncLocal(channel);
}
isLoading.value = false;
}
Future<bool> syncLocal(Channel channel) async {
if (PlatformInfo.isWeb) return false;
final data = await database.localEvents.findAllByChannel(channel.id);
currentEvents.replaceRange(0, currentEvents.length, data);
for (final x in data.reversed) {
applyEvent(x);
}
return true;
}
receiveEvent(Event remote) async {
LocalEvent entry;
if (PlatformInfo.isWeb) {
entry = LocalEvent(
remote.id,
remote,
remote.channelId,
remote.createdAt,
);
} else {
entry = await database.receiveEvent(remote);
}
insertEvent(entry);
applyEvent(entry);
}
insertEvent(LocalEvent entry) {
if (entry.channelId != channel?.id) return;
final idx = currentEvents.indexWhere((x) => x.data.uuid == entry.data.uuid);
if (idx != -1) {
currentEvents[idx] = entry;
} else {
currentEvents.insert(0, entry);
}
}
applyEvent(LocalEvent entry) {
if (entry.channelId != channel?.id) return;
switch (entry.data.type) {
case 'messages.edit':
final body = EventMessageBody.fromJson(entry.data.body);
if (body.relatedEvent != null) {
final idx =
currentEvents.indexWhere((x) => x.data.id == body.relatedEvent);
if (idx != -1) {
currentEvents[idx].data.body = entry.data.body;
currentEvents[idx].data.updatedAt = entry.data.updatedAt;
}
}
case 'messages.delete':
final body = EventMessageBody.fromJson(entry.data.body);
if (body.relatedEvent != null) {
currentEvents.removeWhere((x) => x.id == body.relatedEvent);
}
}
}
addPendingEvent(Event info) async {
currentEvents.insert(
0,
LocalEvent(
info.id,
info,
info.channelId,
DateTime.now(),
),
);
}
}

View File

@@ -0,0 +1,363 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/post.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart';
import 'package:solian/widgets/posts/editor/post_editor_categories_tags.dart';
import 'package:solian/widgets/posts/editor/post_editor_date.dart';
import 'package:solian/widgets/posts/editor/post_editor_overview.dart';
import 'package:solian/widgets/posts/editor/post_editor_publish_zone.dart';
import 'package:solian/widgets/posts/editor/post_editor_thumbnail.dart';
import 'package:solian/widgets/posts/editor/post_editor_visibility.dart';
import 'package:shared_preferences/shared_preferences.dart';
class PostEditorController extends GetxController {
late final SharedPreferences _prefs;
final aliasController = TextEditingController();
final titleController = TextEditingController();
final descriptionController = TextEditingController();
final contentController = TextEditingController();
RxInt mode = 0.obs;
RxInt contentLength = 0.obs;
Rx<Post?> editTo = Rx(null);
Rx<Post?> replyTo = Rx(null);
Rx<Post?> repostTo = Rx(null);
Rx<Realm?> realmZone = Rx(null);
Rx<DateTime?> publishedAt = Rx(null);
Rx<DateTime?> publishedUntil = Rx(null);
RxList<int> attachments = RxList<int>.empty(growable: true);
RxList<String> tags = RxList<String>.empty(growable: true);
Rx<int?> thumbnail = Rx(null);
RxList<int> visibleUsers = RxList.empty(growable: true);
RxList<int> invisibleUsers = RxList.empty(growable: true);
RxInt visibility = 0.obs;
RxBool isDraft = false.obs;
RxBool isRestoreFromLocal = false.obs;
Rx<DateTime?> lastSaveTime = Rx(null);
Timer? _saveTimer;
PostEditorController() {
SharedPreferences.getInstance().then((inst) {
_prefs = inst;
_saveTimer = Timer.periodic(
const Duration(seconds: 3),
(Timer t) {
if (isNotEmpty) {
localSave();
lastSaveTime.value = DateTime.now();
lastSaveTime.refresh();
} else if (_prefs.containsKey('post_editor_local_save')) {
localClear();
lastSaveTime.value = null;
}
},
);
});
contentController.addListener(() {
contentLength.value = contentController.text.length;
});
}
Future<void> editOverview(BuildContext context) {
return showDialog(
context: context,
builder: (context) => PostEditorOverviewDialog(
controller: this,
),
);
}
Future<void> editVisibility(BuildContext context) {
return showDialog(
context: context,
builder: (context) => PostEditorVisibilityDialog(
controller: this,
),
);
}
Future<void> editCategoriesAndTags(BuildContext context) {
return showDialog(
context: context,
builder: (context) => PostEditorCategoriesDialog(
controller: this,
),
);
}
Future<void> editPublishZone(BuildContext context) {
return showDialog(
context: context,
builder: (context) => PostEditorPublishZoneDialog(
controller: this,
),
);
}
Future<void> editPublishDate(BuildContext context) {
return showDialog(
context: context,
builder: (context) => PostEditorDateDialog(
controller: this,
),
);
}
Future<void> editAttachment(BuildContext context) {
return showModalBottomSheet(
context: context,
builder: (context) => AttachmentEditorPopup(
usage: 'i.attachment',
initialAttachments: attachments,
onAdd: (int value) {
attachments.add(value);
},
onRemove: (int value) {
attachments.remove(value);
},
),
);
}
Future<void> editThumbnail(BuildContext context) {
return showDialog(
context: context,
builder: (context) => PostEditorThumbnailDialog(
controller: this,
),
);
}
void toggleDraftMode() {
isDraft.value = !isDraft.value;
}
void localSave() {
_prefs.setString(
'post_editor_local_save',
jsonEncode({
...payload,
'reply_to': replyTo.value?.toJson(),
'repost_to': repostTo.value?.toJson(),
'edit_to': editTo.value?.toJson(),
'realm': realmZone.value?.toJson(),
'type': type,
}),
);
}
void localRead() {
SharedPreferences.getInstance().then((inst) {
if (inst.containsKey('post_editor_local_save')) {
isRestoreFromLocal.value = true;
payload = jsonDecode(inst.getString('post_editor_local_save')!);
}
});
}
void localClear() {
_prefs.remove('post_editor_local_save');
}
void currentClear() {
titleController.clear();
descriptionController.clear();
contentController.clear();
attachments.clear();
tags.clear();
visibleUsers.clear();
invisibleUsers.clear();
visibility.value = 0;
thumbnail.value = null;
publishedAt.value = null;
publishedUntil.value = null;
isDraft.value = false;
isRestoreFromLocal.value = false;
lastSaveTime.value = null;
contentLength.value = 0;
editTo.value = null;
replyTo.value = null;
repostTo.value = null;
realmZone.value = null;
}
set editTarget(Post? value) {
if (value == null) {
editTo.value = null;
return;
}
type = value.type;
editTo.value = value;
realmZone.value = value.realm;
isDraft.value = value.isDraft ?? false;
aliasController.text = value.alias ?? '';
titleController.text = value.body['title'] ?? '';
descriptionController.text = value.body['description'] ?? '';
contentController.text = value.body['content'] ?? '';
publishedAt.value = value.publishedAt;
publishedUntil.value = value.publishedUntil;
tags.value = List.from(
value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty(),
growable: true,
);
tags.refresh();
attachments.value = List.from(
value.body['attachments'] ?? List.empty(),
growable: true,
);
attachments.refresh();
thumbnail.value = value.body['thumbnail'];
contentLength.value = contentController.text.length;
}
String get typeEndpoint {
switch (mode.value) {
case 0:
return 'stories';
case 1:
return 'articles';
default:
return 'stories';
}
}
String get type {
switch (mode.value) {
case 0:
return 'story';
case 1:
return 'article';
default:
return 'story';
}
}
set type(String value) {
switch (value) {
case 'story':
mode.value = 0;
case 'article':
mode.value = 1;
}
}
String? get title {
if (titleController.text.isEmpty) return null;
return titleController.text;
}
String? get description {
if (descriptionController.text.isEmpty) return null;
return descriptionController.text;
}
Map<String, dynamic> get payload {
return {
'alias': aliasController.text,
'title': title,
'description': description,
'content': contentController.text,
'thumbnail': thumbnail.value,
'tags': tags.map((x) => {'alias': x}).toList(),
'attachments': attachments,
'visible_users': visibleUsers,
'invisible_users': invisibleUsers,
'visibility': visibility.value,
'published_at': publishedAt.value?.toUtc().toIso8601String() ??
DateTime.now().toUtc().toIso8601String(),
'published_until': publishedUntil.value?.toUtc().toIso8601String(),
'is_draft': isDraft.value,
if (replyTo.value != null) 'reply_to': replyTo.value!.id,
if (repostTo.value != null) 'repost_to': repostTo.value!.id,
if (realmZone.value != null) 'realm': realmZone.value!.alias,
};
}
set payload(Map<String, dynamic> value) {
type = value['type'];
tags.value = List.from(
value['tags'].map((x) => x['alias']).toList(),
growable: true,
);
aliasController.text = value['alias'] ?? '';
titleController.text = value['title'] ?? '';
descriptionController.text = value['description'] ?? '';
contentController.text = value['content'] ?? '';
attachments.value = List.from(
value['attachments'] ?? List.empty(),
growable: true,
);
attachments.refresh();
thumbnail.value = value['thumbnail'];
visibility.value = value['visibility'];
isDraft.value = value['is_draft'];
if (value['visible_users'] != null) {
visibleUsers.value = List.from(
value['visible_users'],
growable: true,
);
}
if (value['invisible_users'] != null) {
invisibleUsers.value = List.from(
value['invisible_users'],
growable: true,
);
}
if (value['published_at'] != null) {
publishedAt.value = DateTime.parse(value['published_at']).toLocal();
}
if (value['published_until'] != null) {
publishedAt.value = DateTime.parse(value['published_until']).toLocal();
}
if (value['reply_to'] != null) {
replyTo.value = Post.fromJson(value['reply_to']);
}
if (value['repost_to'] != null) {
repostTo.value = Post.fromJson(value['repost_to']);
}
if (value['edit_to'] != null) {
editTo.value = Post.fromJson(value['edit_to']);
}
if (value['realm'] != null) {
realmZone.value = Realm.fromJson(value['realm']);
}
}
bool get isEmpty {
if (contentController.text.isEmpty) return true;
return false;
}
bool get isNotEmpty {
return [
aliasController.text.isNotEmpty,
titleController.text.isNotEmpty,
descriptionController.text.isNotEmpty,
contentController.text.isNotEmpty,
attachments.isNotEmpty,
tags.isNotEmpty,
thumbnail.value != null,
].any((x) => x);
}
@override
void dispose() {
_saveTimer?.cancel();
titleController.dispose();
descriptionController.dispose();
contentController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,138 @@
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart';
class PostListController extends GetxController {
String? author;
/// The polling source modifier.
/// - `0`: default recommendations
/// - `1`: shuffle mode
RxInt mode = 0.obs;
/// The paging controller for infinite loading.
/// Only available when mode is `0`.
PagingController<int, Post> pagingController =
PagingController(firstPageKey: 0);
PostListController({this.author}) {
_initPagingController();
}
/// Initialize a compatibility layer to paging controller
void _initPagingController() {
pagingController.addPageRequestListener(_onPagingControllerRequest);
}
Future<void> _onPagingControllerRequest(int pageKey) async {
try {
final result = await loadMore();
if (result != null && hasMore.value) {
pagingController.appendPage(result, nextPageKey.value);
} else if (result != null) {
pagingController.appendLastPage(result);
}
} catch (e) {
pagingController.error = e;
}
}
void _resetPagingController() {
pagingController.removePageRequestListener(_onPagingControllerRequest);
pagingController.nextPageKey = nextPageKey.value;
pagingController.itemList?.clear();
}
RxBool isBusy = false.obs;
RxBool isPreparing = false.obs;
RxInt focusCursor = 0.obs;
Post get focusPost => postList[focusCursor.value];
RxInt postTotal = 0.obs;
RxList<Post> postList = RxList.empty(growable: true);
RxInt nextPageKey = 0.obs;
RxBool hasMore = true.obs;
Future<void> reloadAllOver() async {
isPreparing.value = true;
focusCursor.value = 0;
nextPageKey.value = 0;
postList.clear();
hasMore.value = true;
_resetPagingController();
final result = await loadMore();
if (result != null && hasMore.value) {
pagingController.appendPage(result, nextPageKey.value);
} else if (result != null) {
pagingController.appendLastPage(result);
}
_initPagingController();
isPreparing.value = false;
}
Future<List<Post>?> loadMore() async {
final result = await _loadPosts(nextPageKey.value);
if (result != null && result.length >= 10) {
postList.addAll(result);
nextPageKey.value += result.length;
hasMore.value = true;
} else if (result != null) {
postList.addAll(result);
nextPageKey.value += result.length;
hasMore.value = false;
}
final idx = <dynamic>{};
postList.retainWhere((x) => idx.add(x.id));
return result;
}
Future<List<Post>?> _loadPosts(int pageKey) async {
isBusy.value = true;
final PostProvider provider = Get.find();
Response resp;
try {
if (author != null) {
resp = await provider.listPost(
pageKey,
author: author,
);
} else {
resp = await provider.listRecommendations(
pageKey,
channel: mode.value == 0 ? null : 'shuffle',
);
}
} catch (e) {
rethrow;
} finally {
isBusy.value = false;
}
final result = PaginationResult.fromJson(resp.body);
final out = result.data?.map((e) => Post.fromJson(e)).toList();
postTotal.value = result.count;
return out;
}
@override
void dispose() {
pagingController.dispose();
super.dispose();
}
}

View File

@@ -2,27 +2,61 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
extension SolianExtenions on BuildContext {
void showSnackbar(String content) {
void showSnackbar(String content, {SnackBarAction? action}) {
ScaffoldMessenger.of(this).showSnackBar(SnackBar(
content: Text(content),
action: action,
));
}
void clearSnackbar() {
ScaffoldMessenger.of(this).clearSnackBars();
}
Future<void> showModalDialog(String title, desc) {
return showDialog<void>(
useRootNavigator: true,
context: this,
builder: (ctx) => AlertDialog(
title: Text(title),
content: Text(desc),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text('okay'.tr),
)
],
),
);
}
Future<void> showInfoDialog(String title, body) {
return showDialog<void>(
useRootNavigator: true,
context: this,
builder: (ctx) => AlertDialog(
title: Text(title),
content: Text(body),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text('okay'.tr),
)
],
),
);
}
Future<void> showErrorDialog(dynamic exception) {
String formatMessage(dynamic exception) {
final message = exception.toString();
if (message.trim().isEmpty) return '';
return message
.split(' ')
.map((element) => '${element[0].toUpperCase()}${element.substring(1).toLowerCase()}')
.join(' ');
}
var stack = StackTrace.current;
var stackTrace = '$stack';
return showDialog<void>(
useRootNavigator: true,
context: this,
builder: (ctx) => AlertDialog(
title: Text('errorHappened'.tr),
content: Text(formatMessage(exception)),
content: Text('${exception.toString().capitalize!}\n\nStack Trace: $stackTrace'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),

89
lib/firebase_options.dart Normal file
View File

@@ -0,0 +1,89 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
return windows;
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyBKfIQpTouj5rXnlzkEieSlbAzepm4mgJE',
appId: '1:961776991058:web:b91d12f2892a5609f4188b',
messagingSenderId: '961776991058',
projectId: 'solian-0x001',
authDomain: 'solian-0x001.firebaseapp.com',
storageBucket: 'solian-0x001.appspot.com',
measurementId: 'G-XY3HHKG0PE',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyDvFNudXYs29uDtcCv6pFR8h5tXBs90FYk',
appId: '1:961776991058:android:a8d3f7995b0b8e86f4188b',
messagingSenderId: '961776991058',
projectId: 'solian-0x001',
storageBucket: 'solian-0x001.appspot.com',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyCzQIyiYKoYHTpGXhN-IjgMML8z797WVD8',
appId: '1:961776991058:ios:727229d368cc47e1f4188b',
messagingSenderId: '961776991058',
projectId: 'solian-0x001',
storageBucket: 'solian-0x001.appspot.com',
iosBundleId: 'dev.solsynth.solian',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyCzQIyiYKoYHTpGXhN-IjgMML8z797WVD8',
appId: '1:961776991058:ios:727229d368cc47e1f4188b',
messagingSenderId: '961776991058',
projectId: 'solian-0x001',
storageBucket: 'solian-0x001.appspot.com',
iosBundleId: 'dev.solsynth.solian',
);
static const FirebaseOptions windows = FirebaseOptions(
apiKey: 'AIzaSyBKfIQpTouj5rXnlzkEieSlbAzepm4mgJE',
appId: '1:961776991058:web:dcd731c8c5ce1281f4188b',
messagingSenderId: '961776991058',
projectId: 'solian-0x001',
authDomain: 'solian-0x001.firebaseapp.com',
storageBucket: 'solian-0x001.appspot.com',
measurementId: 'G-EF9BZMKBC3',
);
}

View File

@@ -1,40 +1,130 @@
import 'dart:ui';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:get/get.dart';
import 'package:go_router/go_router.dart';
import 'package:protocol_handler/protocol_handler.dart';
import 'package:provider/provider.dart';
import 'package:solian/bootstrapper.dart';
import 'package:solian/firebase_options.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/providers/account_status.dart';
import 'package:solian/router.dart';
import 'package:solian/shells/system_shell.dart';
import 'package:solian/theme.dart';
import 'package:solian/translations.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
void main() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Future.wait([
_initializeFirebase(),
_initializePlatformComponents(),
]);
GoRouter.optionURLReflectsImperativeAPIs = true;
usePathUrlStrategy();
runApp(const SolianApp());
}
Future<void> _initializeFirebase() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FlutterError.onError = (errorDetails) {
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
};
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
}
Future<void> _initializePlatformComponents() async {
if (!PlatformInfo.isWeb) {
await protocolHandler.register('solink');
}
if (PlatformInfo.isDesktop) {
await Window.initialize();
if (PlatformInfo.isMacOS) {
await Future.wait([
Window.hideTitle(),
Window.makeTitlebarTransparent(),
Window.enableFullSizeContentView(),
]);
}
}
}
final themeSwitcher = ThemeSwitcher(
lightThemeData: SolianTheme.build(Brightness.light),
darkThemeData: SolianTheme.build(Brightness.dark),
);
class SolianApp extends StatelessWidget {
const SolianApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp.router(
title: 'Solian',
theme: SolianTheme.build(Brightness.light),
darkTheme: SolianTheme.build(Brightness.dark),
themeMode: ThemeMode.system,
routerDelegate: AppRouter.instance.routerDelegate,
routeInformationParser: AppRouter.instance.routeInformationParser,
routeInformationProvider: AppRouter.instance.routeInformationProvider,
translations: SolianMessages(),
locale: Get.deviceLocale,
fallbackLocale: const Locale('en', 'US'),
onInit: () {
Get.lazyPut(() => AuthProvider());
Get.lazyPut(() => AttachmentProvider());
},
builder: (context, child) {
return ScaffoldMessenger(
child: child ?? Container(),
return ChangeNotifierProvider.value(
value: themeSwitcher,
child: Builder(builder: (context) {
final theme = Provider.of<ThemeSwitcher>(context);
return GetMaterialApp.router(
title: 'Solian',
theme: theme.lightThemeData,
darkTheme: theme.darkThemeData,
themeMode: ThemeMode.system,
routerDelegate: AppRouter.instance.routerDelegate,
routeInformationParser: AppRouter.instance.routeInformationParser,
routeInformationProvider: AppRouter.instance.routeInformationProvider,
backButtonDispatcher: AppRouter.instance.backButtonDispatcher,
translations: SolianMessages(),
locale: Get.deviceLocale,
fallbackLocale: const Locale('en', 'US'),
onInit: () => _initializeProviders(context),
builder: (context, child) {
return SystemShell(
child: ScaffoldMessenger(
child: BootstrapperShell(
child: child ?? const SizedBox(),
),
),
);
},
);
},
}),
);
}
void _initializeProviders(BuildContext context) async {
Get.lazyPut(() => AuthProvider());
Get.lazyPut(() => RelationshipProvider());
Get.lazyPut(() => PostProvider());
Get.lazyPut(() => StickerProvider());
Get.lazyPut(() => AttachmentProvider());
Get.lazyPut(() => WebSocketProvider());
Get.lazyPut(() => StatusProvider());
Get.lazyPut(() => ChannelProvider());
Get.lazyPut(() => RealmProvider());
Get.lazyPut(() => ChatCallProvider());
Get.lazyPut(() => AttachmentUploaderController());
}
}

View File

@@ -3,11 +3,14 @@ class Account {
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
DateTime? confirmedAt;
DateTime? suspendedAt;
String name;
String nick;
dynamic avatar;
dynamic banner;
String description;
List<AccountBadge>? badges;
String? emailAddress;
int? externalId;
@@ -15,13 +18,16 @@ class Account {
required this.id,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
required this.deletedAt,
required this.confirmedAt,
required this.suspendedAt,
required this.name,
required this.nick,
required this.avatar,
required this.banner,
required this.description,
this.emailAddress,
required this.badges,
required this.emailAddress,
this.externalId,
});
@@ -29,13 +35,25 @@ class Account {
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
confirmedAt: json['confirmed_at'] != null
? DateTime.parse(json['confirmed_at'])
: null,
suspendedAt: json['suspended_at'] != null
? DateTime.parse(json['suspended_at'])
: null,
name: json['name'],
nick: json['nick'],
avatar: json['avatar'],
banner: json['banner'],
description: json['description'],
emailAddress: json['email_address'],
badges: json['badges']
?.map((e) => AccountBadge.fromJson(e))
.toList()
.cast<AccountBadge>(),
externalId: json['external_id'],
);
@@ -43,13 +61,58 @@ class Account {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'deleted_at': deletedAt?.toIso8601String(),
'confirmed_at': confirmedAt?.toIso8601String(),
'suspended_at': suspendedAt?.toIso8601String(),
'name': name,
'nick': nick,
'avatar': avatar,
'banner': banner,
'description': description,
'email_address': emailAddress,
'badges': badges?.map((e) => e.toJson()).toList(),
'external_id': externalId,
};
}
class AccountBadge {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
Map<String, dynamic>? metadata;
String type;
int accountId;
AccountBadge({
required this.id,
required this.accountId,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.metadata,
required this.type,
});
factory AccountBadge.fromJson(Map<String, dynamic> json) => AccountBadge(
id: json['id'],
accountId: json['account_id'],
updatedAt: DateTime.parse(json['updated_at']),
createdAt: DateTime.parse(json['created_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
metadata: json['metadata'],
type: json['type'],
);
Map<String, dynamic> toJson() => {
'id': id,
'account_id': accountId,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'metadata': metadata,
'type': type,
};
}

View File

@@ -0,0 +1,83 @@
class AccountStatus {
bool isDisturbable;
bool isOnline;
DateTime? lastSeenAt;
Status? status;
AccountStatus({
required this.isDisturbable,
required this.isOnline,
required this.lastSeenAt,
required this.status,
});
factory AccountStatus.fromJson(Map<String, dynamic> json) => AccountStatus(
isDisturbable: json['is_disturbable'],
isOnline: json['is_online'],
lastSeenAt: json['last_seen_at'] != null ? DateTime.parse(json['last_seen_at']) : null,
status: json['status'] != null ? Status.fromJson(json['status']) : null,
);
Map<String, dynamic> toJson() => {
'is_disturbable': isDisturbable,
'is_online': isOnline,
'last_seen_at': lastSeenAt?.toIso8601String(),
'status': status?.toJson(),
};
}
class Status {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String type;
String label;
int attitude;
bool isNoDisturb;
bool isInvisible;
DateTime? clearAt;
int accountId;
Status({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.type,
required this.label,
required this.attitude,
required this.isNoDisturb,
required this.isInvisible,
required this.clearAt,
required this.accountId,
});
factory Status.fromJson(Map<String, dynamic> json) => Status(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
type: json['type'],
label: json['label'],
attitude: json['attitude'],
isNoDisturb: json['is_no_disturb'],
isInvisible: json['is_invisible'],
clearAt: json['clear_at'] != null ? DateTime.parse(json['clear_at']) : null,
accountId: json['account_id'],
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'type': type,
'label': label,
'attitude': attitude,
'is_no_disturb': isNoDisturb,
'is_invisible': isInvisible,
'clear_at': clearAt?.toIso8601String(),
'account_id': accountId,
};
}

View File

@@ -4,7 +4,7 @@ class Attachment {
int id;
DateTime createdAt;
DateTime updatedAt;
dynamic deletedAt;
DateTime? deletedAt;
String uuid;
int size;
String name;
@@ -12,11 +12,12 @@ class Attachment {
String usage;
String mimetype;
String hash;
String destination;
int destination;
bool isAnalyzed;
Map<String, dynamic>? metadata;
bool isMature;
Account account;
int accountId;
Account? account;
int? accountId;
Attachment({
required this.id,
@@ -31,6 +32,7 @@ class Attachment {
required this.mimetype,
required this.hash,
required this.destination,
required this.isAnalyzed,
required this.metadata,
required this.isMature,
required this.account,
@@ -38,40 +40,42 @@ class Attachment {
});
factory Attachment.fromJson(Map<String, dynamic> json) => Attachment(
id: json["id"],
createdAt: DateTime.parse(json["created_at"]),
updatedAt: DateTime.parse(json["updated_at"]),
deletedAt: json["deleted_at"],
uuid: json["uuid"],
size: json["size"],
name: json["name"],
alt: json["alt"],
usage: json["usage"],
mimetype: json["mimetype"],
hash: json["hash"],
destination: json["destination"],
metadata: json["metadata"],
isMature: json["is_mature"],
account: Account.fromJson(json["account"]),
accountId: json["account_id"],
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
uuid: json['uuid'],
size: json['size'],
name: json['name'],
alt: json['alt'],
usage: json['usage'],
mimetype: json['mimetype'],
hash: json['hash'],
destination: json['destination'],
isAnalyzed: json['is_analyzed'],
metadata: json['metadata'],
isMature: json['is_mature'],
account: json['account'] != null ? Account.fromJson(json['account']) : null,
accountId: json['account_id'],
);
Map<String, dynamic> toJson() => {
"id": id,
"created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(),
"deleted_at": deletedAt,
"uuid": uuid,
"size": size,
"name": name,
"alt": alt,
"usage": usage,
"mimetype": mimetype,
"hash": hash,
"destination": destination,
"metadata": metadata,
"is_mature": isMature,
"account": account.toJson(),
"account_id": accountId,
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'uuid': uuid,
'size': size,
'name': name,
'alt': alt,
'usage': usage,
'mimetype': mimetype,
'hash': hash,
'destination': destination,
'is_analyzed': isAnalyzed,
'metadata': metadata,
'is_mature': isMature,
'account': account?.toJson(),
'account_id': accountId,
};
}

View File

@@ -1,3 +1,4 @@
import 'package:livekit_client/livekit_client.dart';
import 'package:solian/models/channel.dart';
class Call {
@@ -9,17 +10,19 @@ class Call {
String externalId;
int founderId;
int channelId;
List<dynamic> participants;
Channel channel;
Call({
required this.id,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
this.endedAt,
required this.deletedAt,
required this.endedAt,
required this.externalId,
required this.founderId,
required this.channelId,
required this.participants,
required this.channel,
});
@@ -33,6 +36,7 @@ class Call {
externalId: json['external_id'],
founderId: json['founder_id'],
channelId: json['channel_id'],
participants: json['participants'] ?? List.empty(),
channel: Channel.fromJson(json['channel']),
);
@@ -45,6 +49,26 @@ class Call {
'external_id': externalId,
'founder_id': founderId,
'channel_id': channelId,
'participants': participants,
'channel': channel.toJson(),
};
}
enum ParticipantStatsType {
unknown,
localAudioSender,
localVideoSender,
remoteAudioReceiver,
remoteVideoReceiver,
}
class ParticipantTrack {
ParticipantTrack(
{required this.participant,
required this.videoTrack,
required this.isScreenShare});
VideoTrack? videoTrack;
Participant participant;
bool isScreenShare;
}

View File

@@ -1,4 +1,5 @@
import 'package:solian/models/account.dart';
import 'package:solian/models/realm.dart';
class Channel {
int id;
@@ -9,8 +10,10 @@ class Channel {
String name;
String description;
int type;
List<ChannelMember>? members;
Account account;
int accountId;
Realm? realm;
int? realmId;
bool isEncrypted;
@@ -20,15 +23,17 @@ class Channel {
required this.id,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
required this.deletedAt,
required this.alias,
required this.name,
required this.description,
required this.type,
required this.members,
required this.account,
required this.accountId,
required this.isEncrypted,
this.realmId,
required this.realm,
required this.realmId,
});
factory Channel.fromJson(Map<String, dynamic> json) => Channel(
@@ -40,8 +45,13 @@ class Channel {
name: json['name'],
description: json['description'],
type: json['type'],
members: json['members']
?.map((e) => ChannelMember.fromJson(e))
.toList()
.cast<ChannelMember>(),
account: Account.fromJson(json['account']),
accountId: json['account_id'],
realm: json['realm'] != null ? Realm.fromJson(json['realm']) : null,
realmId: json['realm_id'],
isEncrypted: json['is_encrypted'],
);
@@ -55,8 +65,10 @@ class Channel {
'name': name,
'description': description,
'type': type,
'account': account,
'members': members?.map((e) => e.toJson()).toList(),
'account': account.toJson(),
'account_id': accountId,
'realm': realm?.toJson(),
'realm_id': realmId,
'is_encrypted': isEncrypted,
};

View File

@@ -1,82 +1,106 @@
import 'dart:convert';
import 'package:solian/models/account.dart';
import 'package:solian/models/channel.dart';
class Message {
class Event {
int id;
String uuid;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String rawContent;
Map<String, dynamic>? metadata;
Map<String, dynamic> body;
String type;
List<String>? attachments;
Channel? channel;
Sender sender;
int? replyId;
Message? replyTo;
int channelId;
int senderId;
bool isSending = false;
bool isPending = false;
Map<String, dynamic> get decodedContent {
return jsonDecode(utf8.fuse(base64).decode(rawContent));
}
Message({
Event({
required this.id,
required this.uuid,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
required this.rawContent,
required this.metadata,
required this.body,
required this.type,
this.attachments,
this.channel,
required this.sender,
required this.replyId,
required this.replyTo,
required this.channelId,
required this.senderId,
});
factory Message.fromJson(Map<String, dynamic> json) => Message(
factory Event.fromJson(Map<String, dynamic> json) => Event(
id: json['id'],
uuid: json['uuid'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
rawContent: json['content'],
metadata: json['metadata'],
body: json['body'],
type: json['type'],
attachments: json['attachments'],
channel: Channel.fromJson(json['channel']),
channel:
json['channel'] != null ? Channel.fromJson(json['channel']) : null,
sender: Sender.fromJson(json['sender']),
replyId: json['reply_id'],
replyTo: json['reply_to'] != null ? Message.fromJson(json['reply_to']) : null,
channelId: json['channel_id'],
senderId: json['sender_id'],
);
Map<String, dynamic> toJson() => {
'id': id,
'uuid': uuid,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'content': rawContent,
'metadata': metadata,
'body': body,
'type': type,
'attachments': attachments,
'channel': channel?.toJson(),
'sender': sender.toJson(),
'reply_id': replyId,
'reply_to': replyTo?.toJson(),
'channel_id': channelId,
'sender_id': senderId,
};
}
class EventMessageBody {
String text;
String algorithm;
List<int>? attachments;
int? quoteEvent;
int? relatedEvent;
List<int>? relatedUsers;
EventMessageBody({
required this.text,
required this.algorithm,
required this.attachments,
required this.quoteEvent,
required this.relatedEvent,
required this.relatedUsers,
});
factory EventMessageBody.fromJson(Map<String, dynamic> json) =>
EventMessageBody(
text: json['text'] ?? '',
algorithm: json['algorithm'] ?? 'plain',
attachments: json['attachments'] != null
? List<int>.from(json['attachments'].map((x) => x))
: null,
quoteEvent: json['quote_event'],
relatedEvent: json['related_event'],
relatedUsers: json['related_users'] != null
? List<int>.from(json['related_users'].map((x) => x))
: null,
);
Map<String, dynamic> toJson() => {
'text': text,
'algorithm': algorithm,
'attachments': attachments?.cast<dynamic>(),
'quote_event': quoteEvent,
'related_event': relatedEvent,
'related_users': relatedUsers?.cast<dynamic>(),
};
}
class Sender {
int id;
DateTime createdAt;

83
lib/models/feed.dart Normal file
View File

@@ -0,0 +1,83 @@
class Tag {
int id;
String alias;
String name;
String description;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
Tag({
required this.id,
required this.alias,
required this.name,
required this.description,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
});
factory Tag.fromJson(Map<String, dynamic> json) => Tag(
id: json['id'],
alias: json['alias'],
name: json['name'],
description: json['description'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
);
Map<String, dynamic> toJson() => {
'id': id,
'alias': alias,
'description': description,
'name': name,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
};
}
class Category {
int id;
String alias;
String name;
String description;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
Category({
required this.id,
required this.alias,
required this.name,
required this.description,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
});
factory Category.fromJson(Map<String, dynamic> json) => Category(
id: json['id'],
alias: json['alias'],
name: json['name'],
description: json['description'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
);
Map<String, dynamic> toJson() => {
'id': id,
'alias': alias,
'description': description,
'name': name,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
};
}

View File

@@ -3,43 +3,44 @@ class Notification {
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String subject;
String content;
List<Link>? links;
bool isImportant;
bool isRealtime;
DateTime? readAt;
String title;
String? subtitle;
String body;
String? avatar;
String? picture;
int? senderId;
int recipientId;
int accountId;
Notification({
required this.id,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
required this.subject,
required this.content,
this.links,
required this.isImportant,
required this.isRealtime,
this.readAt,
this.senderId,
required this.recipientId,
required this.deletedAt,
required this.title,
required this.subtitle,
required this.body,
required this.avatar,
required this.picture,
required this.senderId,
required this.accountId,
});
factory Notification.fromJson(Map<String, dynamic> json) => Notification(
id: json['id'] ?? 0,
createdAt: json['created_at'] == null ? DateTime.now() : DateTime.parse(json['created_at']),
updatedAt: json['updated_at'] == null ? DateTime.now() : DateTime.parse(json['updated_at']),
createdAt: json['created_at'] == null
? DateTime.now()
: DateTime.parse(json['created_at']),
updatedAt: json['updated_at'] == null
? DateTime.now()
: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
subject: json['subject'],
content: json['content'],
links: json['links'] != null ? List<Link>.from(json['links'].map((x) => Link.fromJson(x))) : List.empty(),
isImportant: json['is_important'],
isRealtime: json['is_realtime'],
readAt: json['read_at'],
title: json['title'],
subtitle: json['subtitle'],
body: json['body'],
avatar: json['avatar'],
picture: json['picture'],
senderId: json['sender_id'],
recipientId: json['recipient_id'],
accountId: json['account_id'],
);
Map<String, dynamic> toJson() => {
@@ -47,33 +48,12 @@ class Notification {
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'subject': subject,
'content': content,
'links': links != null ? List<dynamic>.from(links!.map((x) => x.toJson())) : List.empty(),
'is_important': isImportant,
'is_realtime': isRealtime,
'read_at': readAt,
'title': title,
'subtitle': subtitle,
'body': body,
'avatar': avatar,
'picture': picture,
'sender_id': senderId,
'recipient_id': recipientId,
};
}
class Link {
String label;
String url;
Link({
required this.label,
required this.url,
});
factory Link.fromJson(Map<String, dynamic> json) => Link(
label: json['label'],
url: json['url'],
);
Map<String, dynamic> toJson() => {
'label': label,
'url': url,
'account_id': accountId,
};
}

View File

@@ -1,47 +0,0 @@
class PersonalPage {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String content;
String script;
String style;
Map<String, String>? links;
int accountId;
PersonalPage({
required this.id,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
required this.content,
required this.script,
required this.style,
this.links,
required this.accountId,
});
factory PersonalPage.fromJson(Map<String, dynamic> json) => PersonalPage(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
content: json['content'],
script: json['script'],
style: json['style'],
links: json['links'],
accountId: json['account_id'],
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'content': content,
'script': script,
'style': style,
'links': links,
'account_id': accountId,
};
}

View File

@@ -1,18 +1,20 @@
import 'package:solian/models/account.dart';
import 'package:solian/models/feed.dart';
import 'package:solian/models/realm.dart';
class Post {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? editedAt;
DateTime? deletedAt;
String alias;
String content;
dynamic tags;
dynamic categories;
dynamic reactions;
String? alias;
String? areaAlias;
dynamic body;
List<Tag>? tags;
List<Category>? categories;
List<Post>? replies;
List<int>? attachments;
String type;
int? replyId;
int? repostId;
int? realmId;
@@ -20,24 +22,26 @@ class Post {
Post? repostTo;
Realm? realm;
DateTime? publishedAt;
DateTime? publishedUntil;
DateTime? pinnedAt;
bool? isDraft;
int authorId;
Account author;
int replyCount;
int reactionCount;
Map<String, int> reactionList;
PostMetric? metric;
Post({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.editedAt,
required this.deletedAt,
required this.alias,
required this.content,
required this.areaAlias,
required this.type,
required this.body,
required this.tags,
required this.categories,
required this.reactions,
required this.replies,
required this.attachments,
required this.replyId,
required this.repostId,
required this.realmId,
@@ -45,44 +49,103 @@ class Post {
required this.repostTo,
required this.realm,
required this.publishedAt,
required this.publishedUntil,
required this.pinnedAt,
required this.isDraft,
required this.authorId,
required this.author,
required this.replyCount,
required this.reactionCount,
required this.reactionList,
required this.metric,
});
factory Post.fromJson(Map<String, dynamic> json) => Post(
id: json["id"],
createdAt: DateTime.parse(json["created_at"]),
updatedAt: DateTime.parse(json["updated_at"]),
deletedAt: json["deleted_at"] != null
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
alias: json["alias"],
content: json["content"],
tags: json["tags"],
categories: json["categories"],
reactions: json["reactions"],
replies: json["replies"],
attachments: json["attachments"] != null
? List<int>.from(json["attachments"])
: null,
replyId: json["reply_id"],
repostId: json["repost_id"],
realmId: json["realm_id"],
alias: json['alias'],
areaAlias: json['area_alias'],
type: json['type'],
body: json['body'],
tags: json['tags']?.map((x) => Tag.fromJson(x)).toList().cast<Tag>(),
categories: json['categories']
?.map((x) => Category.fromJson(x))
.toList()
.cast<Category>(),
replies: json['replies'],
replyId: json['reply_id'],
repostId: json['repost_id'],
realmId: json['realm_id'],
replyTo:
json["reply_to"] != null ? Post.fromJson(json["reply_to"]) : null,
json['reply_to'] != null ? Post.fromJson(json['reply_to']) : null,
repostTo:
json["repost_to"] != null ? Post.fromJson(json["repost_to"]) : null,
realm: json["realm"],
publishedAt: json["published_at"],
authorId: json["author_id"],
author: Account.fromJson(json["author"]),
replyCount: json["reply_count"],
reactionCount: json["reaction_count"],
reactionList: json["reaction_list"] != null
? json["reaction_list"]
json['repost_to'] != null ? Post.fromJson(json['repost_to']) : null,
realm: json['realm'] != null ? Realm.fromJson(json['realm']) : null,
editedAt: json['edited_at'] != null
? DateTime.parse(json['edited_at'])
: null,
publishedAt: json['published_at'] != null
? DateTime.parse(json['published_at'])
: null,
publishedUntil: json['published_until'] != null
? DateTime.parse(json['published_until'])
: null,
pinnedAt: json['pinned_at'] != null
? DateTime.parse(json['pinned_at'])
: null,
isDraft: json['is_draft'],
authorId: json['author_id'],
author: Account.fromJson(json['author']),
metric:
json['metric'] != null ? PostMetric.fromJson(json['metric']) : null,
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'edited_at': editedAt?.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'alias': alias,
'area_alias': areaAlias,
'type': type,
'body': body,
'tags': tags,
'categories': categories,
'replies': replies,
'reply_id': replyId,
'repost_id': repostId,
'realm_id': realmId,
'reply_to': replyTo?.toJson(),
'repost_to': repostTo?.toJson(),
'realm': realm?.toJson(),
'published_at': publishedAt?.toIso8601String(),
'published_until': publishedUntil?.toIso8601String(),
'pinned_at': pinnedAt?.toIso8601String(),
'is_draft': isDraft,
'author_id': authorId,
'author': author.toJson(),
'metric': metric?.toJson(),
};
}
class PostMetric {
int reactionCount;
Map<String, int> reactionList;
int replyCount;
PostMetric({
required this.reactionCount,
required this.reactionList,
required this.replyCount,
});
factory PostMetric.fromJson(Map<String, dynamic> json) => PostMetric(
reactionCount: json['reaction_count'],
replyCount: json['reply_count'],
reactionList: json['reaction_list'] != null
? json['reaction_list']
.map((key, value) => MapEntry(
key,
int.tryParse(value.toString()) ??
@@ -92,28 +155,8 @@ class Post {
);
Map<String, dynamic> toJson() => {
"id": id,
"created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(),
"deleted_at": deletedAt,
"alias": alias,
"content": content,
"tags": tags,
"categories": categories,
"reactions": reactions,
"replies": replies,
"attachments": attachments,
"reply_id": replyId,
"repost_id": repostId,
"realm_id": realmId,
"reply_to": replyTo?.toJson(),
"repost_to": repostTo?.toJson(),
"realm": realm,
"published_at": publishedAt,
"author_id": authorId,
"author": author.toJson(),
"reply_count": replyCount,
"reaction_count": reactionCount,
"reaction_list": reactionList,
'reaction_count': reactionCount,
'reply_count': replyCount,
'reaction_list': reactionList,
};
}

View File

@@ -10,7 +10,7 @@ class Realm {
String description;
bool isPublic;
bool isCommunity;
int accountId;
int? accountId;
Realm({
required this.id,
@@ -22,14 +22,16 @@ class Realm {
required this.description,
required this.isPublic,
required this.isCommunity,
required this.accountId,
this.accountId,
});
factory Realm.fromJson(Map<String, dynamic> json) => Realm(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
alias: json['alias'],
name: json['name'],
description: json['description'],
@@ -50,6 +52,17 @@ class Realm {
'is_community': isCommunity,
'account_id': accountId,
};
@override
bool operator ==(Object other) {
if (other is Realm) {
return other.id == id;
}
return false;
}
@override
int get hashCode => id;
}
class RealmMember {
@@ -77,7 +90,9 @@ class RealmMember {
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
realmId: json['realm_id'],
accountId: json['account_id'],
account: Account.fromJson(json['account']),

View File

@@ -1,38 +1,35 @@
import 'package:solian/models/account.dart';
class Friendship {
class Relationship {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
int accountId;
int relatedId;
int? blockedBy;
Account account;
Account related;
int status;
Friendship({
Relationship({
required this.id,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
required this.deletedAt,
required this.accountId,
required this.relatedId,
this.blockedBy,
required this.account,
required this.related,
required this.status,
});
factory Friendship.fromJson(Map<String, dynamic> json) => Friendship(
factory Relationship.fromJson(Map<String, dynamic> json) => Relationship(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
accountId: json['account_id'],
relatedId: json['related_id'],
blockedBy: json['blocked_by'],
account: Account.fromJson(json['account']),
related: Account.fromJson(json['related']),
status: json['status'],
@@ -45,17 +42,8 @@ class Friendship {
'deleted_at': deletedAt,
'account_id': accountId,
'related_id': relatedId,
'blocked_by': blockedBy,
'account': account.toJson(),
'related': related.toJson(),
'status': status,
};
Account getOtherside(int selfId) {
if (accountId != selfId) {
return account;
} else {
return related;
}
}
}

131
lib/models/stickers.dart Normal file
View File

@@ -0,0 +1,131 @@
import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/services.dart';
class Sticker {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String alias;
String name;
int attachmentId;
Attachment attachment;
int packId;
StickerPack? pack;
int accountId;
Account account;
Sticker({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.alias,
required this.name,
required this.attachmentId,
required this.attachment,
required this.packId,
required this.pack,
required this.accountId,
required this.account,
});
String get textPlaceholder => '${pack?.prefix}$alias';
String get textWarpedPlaceholder => ':$textPlaceholder:';
String get imageUrl => ServiceFinder.buildUrl(
'files',
'/attachments/$attachmentId',
);
factory Sticker.fromJson(Map<String, dynamic> json) => Sticker(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: json['deleted_at'],
alias: json['alias'],
name: json['name'],
attachmentId: json['attachment_id'],
attachment: Attachment.fromJson(json['attachment']),
packId: json['pack_id'],
pack: json['pack'] != null ? StickerPack.fromJson(json['pack']) : null,
accountId: json['account_id'],
account: Account.fromJson(json['account']),
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'alias': alias,
'name': name,
'attachment_id': attachmentId,
'attachment': attachment.toJson(),
'pack_id': packId,
'account_id': accountId,
'account': account.toJson(),
};
}
class StickerPack {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String prefix;
String name;
String description;
List<Sticker>? stickers;
int accountId;
Account account;
StickerPack({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.prefix,
required this.name,
required this.description,
required this.stickers,
required this.accountId,
required this.account,
});
factory StickerPack.fromJson(Map<String, dynamic> json) => StickerPack(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: json['deleted_at'],
prefix: json['prefix'],
name: json['name'],
description: json['description'],
stickers: json['stickers'] == null
? []
: List<Sticker>.from(
json['stickers']!.map((x) => Sticker.fromJson(x))),
accountId: json['account_id'],
account: Account.fromJson(json['account']),
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'prefix': prefix,
'name': name,
'description': description,
'stickers': stickers == null
? []
: List<dynamic>.from(stickers!.map((x) => x.toJson())),
'account_id': accountId,
'account': account.toJson(),
};
}

41
lib/platform.dart Normal file
View File

@@ -0,0 +1,41 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
abstract class PlatformInfo {
static bool get isWeb => kIsWeb;
static bool get isLinux => !kIsWeb && Platform.isLinux;
static bool get isWindows => !kIsWeb && Platform.isWindows;
static bool get isMacOS => !kIsWeb && Platform.isMacOS;
static bool get isIOS => !kIsWeb && Platform.isIOS;
static bool get isAndroid => !kIsWeb && Platform.isAndroid;
static bool get isMobile => isAndroid || isIOS;
// Not first tier supported platform
static bool get isBetaDesktop => isWindows || isLinux;
static bool get isDesktop => isLinux || isWindows || isMacOS;
static bool get useTouchscreen => !isMobile;
static bool get canCacheImage => isAndroid || isIOS || isMacOS;
static bool get canRecord => (isMobile || isMacOS);
static bool get canPushNotification => isAndroid || isIOS || isMacOS;
static Future<String> getVersion() async {
var version = kIsWeb ? 'Web' : 'Unknown';
try {
version = (await PackageInfo.fromPlatform()).version;
} catch (_) {}
return version;
}
}

View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/account_status.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
class StatusProvider extends GetConnect {
static Map<String, (Widget, String, String?)> presetStatuses = {
'online': (
const Icon(Icons.circle, color: Colors.green),
'accountStatusOnline'.tr,
null,
),
'silent': (
const Icon(Icons.do_not_disturb_on, color: Colors.red),
'accountStatusSilent'.tr,
'accountStatusSilentDesc'.tr,
),
'invisible': (
const Icon(Icons.circle, color: Colors.grey),
'accountStatusInvisible'.tr,
'accountStatusInvisibleDesc'.tr,
),
};
@override
void onInit() {
final AuthProvider auth = Get.find();
httpClient.baseUrl = ServiceFinder.buildUrl('auth', null);
httpClient.addAuthenticator(auth.requestAuthenticator);
}
Future<Response> getCurrentStatus() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('auth');
return await client.get('/users/me/status');
}
Future<Response> getSomeoneStatus(String name) =>
get('/users/$name/status');
Future<Response> setStatus(
String type,
String? label,
int attitude, {
bool isUpdate = false,
bool isSilent = false,
bool isInvisible = false,
DateTime? clearAt,
}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('auth');
final payload = {
'type': type,
'label': label,
'attitude': attitude,
'is_no_disturb': isSilent,
'is_invisible': isInvisible,
'clear_at': clearAt?.toUtc().toIso8601String()
};
Response resp;
if (!isUpdate) {
resp = await client.post('/users/me/status', payload);
} else {
resp = await client.put('/users/me/status', payload);
}
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> clearStatus() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('auth');
final resp = await client.delete('/users/me/status');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
static (Widget, Color, String) determineStatus(AccountStatus status,
{double size = 14}) {
Widget icon;
Color color;
String? text;
if (!presetStatuses.keys.contains(status.status?.type)) {
text = status.status?.label;
}
if (status.isDisturbable && status.isOnline) {
color = Colors.green;
icon = Icon(Icons.circle, color: color, size: size);
text ??= 'accountStatusOnline'.tr;
} else if (!status.isDisturbable && status.isOnline) {
color = Colors.red;
icon = Icon(Icons.do_not_disturb_on, color: color, size: size);
text ??= 'accountStatusSilent'.tr;
} else {
color = Colors.grey;
icon = Icon(Icons.circle, color: color, size: size);
text ??= 'accountStatusOffline'.tr;
}
return (icon, color, text);
}
}

View File

@@ -0,0 +1,213 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:get/get.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/providers/content/attachment.dart';
class AttachmentUploadTask {
File file;
String usage;
Map<String, dynamic>? metadata;
double progress = 0;
bool isUploading = false;
bool isCompleted = false;
dynamic error;
AttachmentUploadTask({
required this.file,
required this.usage,
this.metadata,
});
}
class AttachmentUploaderController extends GetxController {
RxBool isUploading = false.obs;
RxDouble progressOfUpload = 0.0.obs;
RxList<AttachmentUploadTask> queueOfUpload = RxList.empty(growable: true);
Timer? _progressSyncTimer;
double _progressOfUpload = 0.0;
void _syncProgress() {
progressOfUpload.value = _progressOfUpload;
queueOfUpload.refresh();
}
void _startProgressSyncTimer() {
if (_progressSyncTimer != null) {
_progressSyncTimer!.cancel();
}
_progressSyncTimer = Timer.periodic(
const Duration(milliseconds: 500),
(_) => _syncProgress(),
);
}
void _stopProgressSyncTimer() {
if (_progressSyncTimer == null) return;
_progressSyncTimer!.cancel();
_progressSyncTimer = null;
}
void enqueueTask(AttachmentUploadTask task) {
if (isUploading.value) throw Exception('uploading blocked');
queueOfUpload.add(task);
}
void enqueueTaskBatch(Iterable<AttachmentUploadTask> tasks) {
if (isUploading.value) throw Exception('uploading blocked');
queueOfUpload.addAll(tasks);
}
void dequeueTask(AttachmentUploadTask task) {
if (isUploading.value) throw Exception('uploading blocked');
queueOfUpload.remove(task);
}
Future<Attachment?> performSingleTask(int queueIndex) async {
isUploading.value = true;
progressOfUpload.value = 0;
_startProgressSyncTimer();
queueOfUpload[queueIndex].isUploading = true;
final task = queueOfUpload[queueIndex];
final result = await _rawUploadAttachment(
await task.file.readAsBytes(),
task.file.path,
task.usage,
null,
onProgress: (value) {
queueOfUpload[queueIndex].progress = value;
_progressOfUpload = value;
},
onError: (err) {
queueOfUpload[queueIndex].error = err;
queueOfUpload[queueIndex].isUploading = false;
},
);
if (queueOfUpload[queueIndex].error == null) {
queueOfUpload.removeAt(queueIndex);
}
_stopProgressSyncTimer();
_syncProgress();
isUploading.value = false;
return result;
}
Future<void> performUploadQueue({
required Function(Attachment item) onData,
}) async {
isUploading.value = true;
progressOfUpload.value = 0;
_startProgressSyncTimer();
for (var idx = 0; idx < queueOfUpload.length; idx++) {
if (queueOfUpload[idx].isUploading || queueOfUpload[idx].error != null) {
continue;
}
queueOfUpload[idx].isUploading = true;
final task = queueOfUpload[idx];
final result = await _rawUploadAttachment(
await task.file.readAsBytes(),
task.file.path,
task.usage,
null,
onProgress: (value) {
queueOfUpload[idx].progress = value;
_progressOfUpload = (idx + value) / queueOfUpload.length;
},
onError: (err) {
queueOfUpload[idx].error = err;
queueOfUpload[idx].isUploading = false;
},
);
_progressOfUpload = (idx + 1) / queueOfUpload.length;
if (result != null) onData(result);
queueOfUpload[idx].isUploading = false;
queueOfUpload[idx].isCompleted = true;
}
queueOfUpload.removeWhere((x) => x.error == null);
_stopProgressSyncTimer();
_syncProgress();
isUploading.value = false;
}
Future<void> uploadAttachmentWithCallback(
Uint8List data,
String path,
String usage,
Map<String, dynamic>? metadata,
Function(Attachment?) callback,
) async {
if (isUploading.value) throw Exception('uploading blocked');
isUploading.value = true;
final result = await _rawUploadAttachment(
data,
path,
usage,
metadata,
onProgress: (progress) {
progressOfUpload.value = progress;
},
);
isUploading.value = false;
callback(result);
}
Future<Attachment?> uploadAttachment(
Uint8List data,
String path,
String usage,
Map<String, dynamic>? metadata,
) async {
if (isUploading.value) throw Exception('uploading blocked');
isUploading.value = true;
final result = await _rawUploadAttachment(
data,
path,
usage,
metadata,
onProgress: (progress) {
progressOfUpload.value = progress;
},
);
isUploading.value = false;
return result;
}
Future<Attachment?> _rawUploadAttachment(
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
{Function(double)? onProgress, Function(dynamic err)? onError}) async {
final AttachmentProvider provider = Get.find();
try {
final result = await provider.createAttachment(
data,
path,
usage,
metadata,
onProgress: onProgress,
);
return result;
} catch (err) {
if (onError != null) {
onError(err);
}
return null;
}
}
}

View File

@@ -1,115 +1,226 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get/get.dart';
import 'package:get/get_connect/http/src/request/request.dart';
import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart';
import 'package:oauth2/oauth2.dart' as oauth2;
class TokenSet {
final String accessToken;
final String refreshToken;
final DateTime? expiredAt;
TokenSet({
required this.accessToken,
required this.refreshToken,
this.expiredAt,
});
factory TokenSet.fromJson(Map<String, dynamic> json) => TokenSet(
accessToken: json['access_token'],
refreshToken: json['refresh_token'],
expiredAt: json['expired_at'] != null
? DateTime.parse(json['expired_at'])
: null,
);
Map<String, dynamic> toJson() => {
'access_token': accessToken,
'refresh_token': refreshToken,
'expired_at': expiredAt?.toIso8601String(),
};
bool get isExpired => expiredAt?.isBefore(DateTime.now()) ?? true;
}
class RiskyAuthenticateException implements Exception {
final int ticketId;
RiskyAuthenticateException(this.ticketId);
}
class AuthProvider extends GetConnect {
final deviceEndpoint = Uri.parse(
'${ServiceFinder.services['passport']}/api/notifications/subscribe');
final tokenEndpoint =
Uri.parse('${ServiceFinder.services['passport']}/api/auth/token');
final userinfoEndpoint =
Uri.parse('${ServiceFinder.services['passport']}/api/users/me');
final redirectUrl = Uri.parse('solian://auth');
Uri.parse(ServiceFinder.buildUrl('auth', '/auth/token'));
static const clientId = 'solian';
static const clientSecret = '_F4%q2Eea3';
static const storage = FlutterSecureStorage();
TokenSet? credentials;
@override
void onInit() {
httpClient.baseUrl = ServiceFinder.services['passport'];
applyAuthenticator();
httpClient.baseUrl = ServiceFinder.buildUrl('auth', null);
refreshAuthorizeStatus().then((_) {
loadCredentials();
refreshUserProfile();
});
}
oauth2.Credentials? credentials;
Completer<void>? _refreshCompleter;
Future<Request<T?>> reqAuthenticator<T>(Request<T?> request) async {
if (credentials != null && credentials!.isExpired) {
final resp = await post('/api/auth/token', {
Future<void> refreshCredentials() async {
if (_refreshCompleter != null) {
await _refreshCompleter!.future;
return;
} else {
_refreshCompleter = Completer<void>();
}
try {
if (!credentials!.isExpired) return;
final resp = await post('/auth/token', {
'refresh_token': credentials!.refreshToken,
'grant_type': 'refresh_token',
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
credentials = oauth2.Credentials(
resp.body['access_token'],
credentials = TokenSet(
accessToken: resp.body['access_token'],
refreshToken: resp.body['refresh_token'],
idToken: resp.body['access_token'],
tokenEndpoint: tokenEndpoint,
expiration: DateTime.now().add(const Duration(minutes: 3)),
expiredAt: DateTime.now().add(const Duration(minutes: 3)),
);
storage.write(
key: 'auth_credentials', value: jsonEncode(credentials!.toJson()));
key: 'auth_credentials',
value: jsonEncode(credentials!.toJson()),
);
_refreshCompleter!.complete();
log('Refreshed credentials at ${DateTime.now()}');
} catch (e) {
_refreshCompleter!.completeError(e);
rethrow;
} finally {
_refreshCompleter = null;
}
}
if (credentials != null) {
Future<Request<T?>> requestAuthenticator<T>(Request<T?> request) async {
try {
await ensureCredentials();
request.headers['Authorization'] = 'Bearer ${credentials!.accessToken}';
}
} catch (_) {}
return request;
}
void applyAuthenticator() {
isAuthorized.then((status) async {
if (status) {
final content = await storage.read(key: 'auth_credentials');
credentials = oauth2.Credentials.fromJson(jsonDecode(content!));
httpClient.addAuthenticator(reqAuthenticator);
}
});
GetConnect configureClient(
String service, {
timeout = const Duration(seconds: 5),
}) {
final client = GetConnect(
maxAuthRetries: 3,
timeout: timeout,
userAgent: 'Solian/1.1',
sendUserAgent: true,
);
client.httpClient.addAuthenticator(requestAuthenticator);
client.httpClient.baseUrl = ServiceFinder.buildUrl(service, null);
return client;
}
Future<oauth2.Credentials> signin(
BuildContext context, String username, String password) async {
final resp = await oauth2.resourceOwnerPasswordGrant(
tokenEndpoint,
username,
password,
identifier: clientId,
secret: clientSecret,
scopes: ['*'],
basicAuth: false,
);
Future<void> ensureCredentials() async {
if (isAuthorized.isFalse) throw Exception('unauthorized');
if (credentials == null) await loadCredentials();
credentials = oauth2.Credentials(
resp.credentials.accessToken,
refreshToken: resp.credentials.refreshToken!,
idToken: resp.credentials.accessToken,
tokenEndpoint: tokenEndpoint,
expiration: DateTime.now().add(const Duration(minutes: 3)),
if (credentials!.isExpired) {
await refreshCredentials();
}
}
Future<void> loadCredentials() async {
if (isAuthorized.isTrue) {
final content = await storage.read(key: 'auth_credentials');
credentials = TokenSet.fromJson(jsonDecode(content!));
}
}
Future<TokenSet> signin(
BuildContext context,
String username,
String password,
) async {
userProfile.value = null;
final client = ServiceFinder.configureClient('auth');
// Create ticket
final resp = await client.post('/auth', {
'username': username,
'password': password,
});
if (resp.statusCode != 200) {
throw Exception(resp.body);
} else if (resp.body['is_finished'] == false) {
throw RiskyAuthenticateException(resp.body['ticket']['id']);
}
// Assign token
final tokenResp = await post('/auth/token', {
'code': resp.body['ticket']['grant_token'],
'grant_type': 'grant_token',
});
if (tokenResp.statusCode != 200) {
throw Exception(tokenResp.bodyString);
}
credentials = TokenSet(
accessToken: tokenResp.body['access_token'],
refreshToken: tokenResp.body['refresh_token'],
expiredAt: DateTime.now().add(const Duration(minutes: 3)),
);
storage.write(
key: 'auth_credentials', value: jsonEncode(credentials!.toJson()));
applyAuthenticator();
key: 'auth_credentials',
value: jsonEncode(credentials!.toJson()),
);
Get.find<WebSocketProvider>().connect();
Get.find<WebSocketProvider>().notifyPrefetch();
return credentials!;
}
void signout() {
isAuthorized.value = false;
userProfile.value = null;
Get.find<WebSocketProvider>().disconnect();
Get.find<WebSocketProvider>().notifications.clear();
Get.find<WebSocketProvider>().notificationUnread.value = 0;
final chatHistory = ChatEventController();
chatHistory.initialize().then((_) async {
await chatHistory.database.localEvents.wipeLocalEvents();
});
storage.deleteAll();
}
Response? _cacheUserProfileResponse;
// Data Layer
Future<bool> get isAuthorized => storage.containsKey(key: 'auth_credentials');
RxBool isAuthorized = false.obs;
Rx<Map<String, dynamic>?> userProfile = Rx(null);
Future<Response> getProfile({noCache = false}) async {
if (!noCache && _cacheUserProfileResponse != null) {
return _cacheUserProfileResponse!;
Future<void> refreshAuthorizeStatus() async {
isAuthorized.value = await storage.containsKey(key: 'auth_credentials');
}
Future<void> refreshUserProfile() async {
final client = configureClient('auth');
final resp = await client.get('/users/me');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
final resp = await get('/api/users/me');
_cacheUserProfileResponse = resp;
return resp;
userProfile.value = resp.body;
}
}

387
lib/providers/call.dart Normal file
View File

@@ -0,0 +1,387 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:solian/models/call.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/screens/channel/call/call.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class ChatCallProvider extends GetxController {
Rx<Call?> current = Rx(null);
Rx<Channel?> channel = Rx(null);
RxBool isReady = false.obs;
RxBool isMounted = false.obs;
RxBool isInitialized = false.obs;
String? token;
String? endpoint;
StreamSubscription? hwSubscription;
RxList audioInputs = [].obs;
RxList videoInputs = [].obs;
RxBool enableAudio = true.obs;
RxBool enableVideo = false.obs;
Rx<LocalAudioTrack?> audioTrack = Rx(null);
Rx<LocalVideoTrack?> videoTrack = Rx(null);
Rx<MediaDevice?> videoDevice = Rx(null);
Rx<MediaDevice?> audioDevice = Rx(null);
late Room room;
late EventsListener<RoomEvent> listener;
RxList<ParticipantTrack> participantTracks = RxList.empty(growable: true);
Rx<ParticipantTrack?> focusTrack = Rx(null);
Future<void> checkPermissions() async {
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
return;
}
await Permission.camera.request();
await Permission.microphone.request();
await Permission.bluetooth.request();
await Permission.bluetoothConnect.request();
}
void setCall(Call call, Channel related) {
current.value = call;
channel.value = related;
}
Future<(String, String)> getRoomToken() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.post(
'/channels/global/${channel.value!.alias}/calls/ongoing/token',
{},
);
if (resp.statusCode == 200) {
token = resp.body['token'];
endpoint = 'wss://${resp.body['endpoint']}';
return (token!, endpoint!);
} else {
throw Exception(resp.bodyString);
}
}
void initHardware() {
if (isReady.value) {
return;
} else {
isReady.value = true;
}
hwSubscription = Hardware.instance.onDeviceChange.stream.listen(
revertDevices,
);
Hardware.instance.enumerateDevices().then(revertDevices);
}
void initRoom() {
initHardware();
room = Room();
listener = room.createListener();
WakelockPlus.enable();
}
void joinRoom(String url, String token) async {
if (isMounted.value) {
return;
} else {
isMounted.value = true;
}
try {
await room.connect(
url,
token,
roomOptions: const RoomOptions(
dynacast: true,
adaptiveStream: true,
defaultAudioPublishOptions: AudioPublishOptions(
name: 'call_voice',
stream: 'call_stream',
),
defaultVideoPublishOptions: VideoPublishOptions(
name: 'call_video',
stream: 'call_stream',
simulcast: true,
backupVideoCodec: BackupVideoCodec(enabled: true),
),
defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(
useiOSBroadcastExtension: true,
params: VideoParametersPresets.screenShareH1080FPS30,
),
defaultCameraCaptureOptions: CameraCaptureOptions(
maxFrameRate: 30,
params: VideoParametersPresets.h1080_169,
),
),
fastConnectOptions: FastConnectOptions(
microphone: TrackOption(track: audioTrack.value),
camera: TrackOption(track: videoTrack.value),
),
);
} catch (e) {
rethrow;
}
}
void autoPublish() async {
try {
if (enableVideo.value) {
await room.localParticipant?.setCameraEnabled(true);
}
if (enableAudio.value) {
await room.localParticipant?.setMicrophoneEnabled(true);
}
} catch (error) {
rethrow;
}
}
void onRoomDidUpdate() => sortParticipants();
void setupRoom() {
if(isInitialized.value) return;
sortParticipants();
room.addListener(onRoomDidUpdate);
WidgetsBindingCompatible.instance?.addPostFrameCallback(
(_) => autoPublish(),
);
if (lkPlatformIsMobile()) {
Hardware.instance.setSpeakerphoneOn(true);
}
isInitialized.value = true;
}
void setupRoomListeners({
required Function(DisconnectReason?) onDisconnected,
}) {
listener
..on<RoomDisconnectedEvent>((event) async {
onDisconnected(event.reason);
})
..on<ParticipantEvent>((event) => sortParticipants())
..on<LocalTrackPublishedEvent>((_) => sortParticipants())
..on<LocalTrackUnpublishedEvent>((_) => sortParticipants())
..on<TrackSubscribedEvent>((_) => sortParticipants())
..on<TrackUnsubscribedEvent>((_) => sortParticipants())
..on<ParticipantNameUpdatedEvent>((event) {
sortParticipants();
});
}
void sortParticipants() {
Map<String, ParticipantTrack> mediaTracks = {};
for (var participant in room.remoteParticipants.values) {
mediaTracks[participant.sid] = ParticipantTrack(
participant: participant,
videoTrack: null,
isScreenShare: false,
);
for (var t in participant.videoTrackPublications) {
mediaTracks[participant.sid]?.videoTrack = t.track;
mediaTracks[participant.sid]?.isScreenShare = t.isScreenShare;
}
}
final newTracks = List<ParticipantTrack>.empty(growable: true);
final mediaTrackList = mediaTracks.values.toList();
mediaTrackList.sort((a, b) {
// Loudest people first
if (a.participant.isSpeaking && b.participant.isSpeaking) {
if (a.participant.audioLevel > b.participant.audioLevel) {
return -1;
} else {
return 1;
}
}
// Last spoke first
final aSpokeAt = a.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
final bSpokeAt = b.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
if (aSpokeAt != bSpokeAt) {
return aSpokeAt > bSpokeAt ? -1 : 1;
}
// Has video first
if (a.participant.hasVideo != b.participant.hasVideo) {
return a.participant.hasVideo ? -1 : 1;
}
// First joined people first
return a.participant.joinedAt.millisecondsSinceEpoch -
b.participant.joinedAt.millisecondsSinceEpoch;
});
newTracks.addAll(mediaTrackList);
if (room.localParticipant != null) {
ParticipantTrack localTrack = ParticipantTrack(
participant: room.localParticipant!,
videoTrack: null,
isScreenShare: false,
);
final localParticipantTracks =
room.localParticipant?.videoTrackPublications;
if (localParticipantTracks != null) {
for (var t in localParticipantTracks) {
localTrack.videoTrack = t.track;
localTrack.isScreenShare = t.isScreenShare;
}
}
newTracks.add(localTrack);
}
participantTracks.value = newTracks;
if (focusTrack.value != null) {
final idx = participantTracks.indexWhere(
(x) => x.participant.sid == focusTrack.value!.participant.sid);
if (idx == -1) {
focusTrack.value = null;
}
}
if (focusTrack.value == null) {
focusTrack.value = participantTracks.firstOrNull;
} else {
final idx = participantTracks.indexWhere(
(x) => focusTrack.value!.participant.sid == x.participant.sid,
);
if (idx > -1) {
focusTrack.value = participantTracks[idx];
}
}
}
void revertDevices(List<MediaDevice> devices) async {
audioInputs.clear();
audioInputs.addAll(devices.where((d) => d.kind == 'audioinput'));
videoInputs.clear();
videoInputs.addAll(devices.where((d) => d.kind == 'videoinput'));
if (audioInputs.isNotEmpty) {
if (audioDevice.value == null && enableAudio.value) {
audioDevice.value = audioInputs.first;
Future.delayed(const Duration(milliseconds: 100), () async {
await changeLocalAudioTrack();
});
}
}
if (videoInputs.isNotEmpty) {
if (videoDevice.value == null && enableVideo.value) {
videoDevice.value = videoInputs.first;
Future.delayed(const Duration(milliseconds: 100), () async {
await changeLocalVideoTrack();
});
}
}
}
Future<void> setEnableVideo(value) async {
enableVideo.value = value;
if (!enableVideo.value) {
await videoTrack.value?.stop();
videoTrack.value = null;
} else {
await changeLocalVideoTrack();
}
}
Future<void> setEnableAudio(value) async {
enableAudio.value = value;
if (!enableAudio.value) {
await audioTrack.value?.stop();
audioTrack.value = null;
} else {
await changeLocalAudioTrack();
}
}
Future<void> changeLocalAudioTrack() async {
if (audioTrack.value != null) {
await audioTrack.value!.stop();
audioTrack.value = null;
}
if (audioDevice.value != null) {
audioTrack.value = await LocalAudioTrack.create(
AudioCaptureOptions(
deviceId: audioDevice.value!.deviceId,
),
);
await audioTrack.value!.start();
}
}
Future<void> changeLocalVideoTrack() async {
if (videoTrack.value != null) {
await videoTrack.value!.stop();
videoTrack.value = null;
}
if (videoDevice.value != null) {
videoTrack.value = await LocalVideoTrack.createCameraTrack(
CameraCaptureOptions(
deviceId: videoDevice.value!.deviceId,
params: VideoParametersPresets.h1080_169,
),
);
await videoTrack.value!.start();
}
}
void changeFocusTrack(ParticipantTrack track) {
focusTrack.value = track;
}
Future gotoScreen(BuildContext context) {
return Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(builder: (context) => const CallScreen()),
);
}
void deactivateHardware() {
hwSubscription?.cancel();
}
void disposeRoom() {
isMounted.value = false;
isInitialized.value = false;
current.value = null;
channel.value = null;
room.removeListener(onRoomDidUpdate);
room.disconnect();
room.dispose();
listener.dispose();
WakelockPlus.disable();
}
void disposeHardware() {
isReady.value = false;
audioTrack.value?.stop();
audioTrack.value = null;
videoTrack.value?.stop();
videoTrack.value = null;
}
}

View File

@@ -1,89 +1,148 @@
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:get/get.dart';
import 'package:path/path.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
import 'package:image/image.dart' as img;
Future<String> calculateFileSha256(File file) async {
final bytes = await Isolate.run(() => file.readAsBytesSync());
final digest = await Isolate.run(() => sha256.convert(bytes));
return digest.toString();
}
Future<double> calculateFileAspectRatio(File file) async {
final bytes = await Isolate.run(() => file.readAsBytesSync());
final decoder = await Isolate.run(() => img.findDecoderForData(bytes));
if (decoder == null) return 1;
final image = await Isolate.run(() => decoder.decode(bytes));
if (image == null) return 1;
return image.width / image.height;
}
import 'package:dio/dio.dart' as dio;
class AttachmentProvider extends GetConnect {
static Map<String, String> mimetypeOverrides = {
'mov': 'video/quicktime',
'mp4': 'video/mp4'
};
@override
void onInit() {
httpClient.baseUrl = ServiceFinder.services['paperclip'];
httpClient.baseUrl = ServiceFinder.buildUrl('files', null);
}
Future<Response> getMetadata(int id) => get('/api/attachments/$id/meta');
final Map<int, Attachment> _cachedResponses = {};
Future<Response> createAttachment(File file, String hash, String usage,
{double? ratio}) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
Future<List<Attachment?>> listMetadata(
List<int> id, {
noCache = false,
}) async {
if (id.isEmpty) return List.empty();
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
client.httpClient.addAuthenticator(auth.reqAuthenticator);
final filePayload =
MultipartFile(await file.readAsBytes(), filename: basename(file.path));
final fileAlt = basename(file.path).contains('.')
? basename(file.path).substring(0, basename(file.path).lastIndexOf('.'))
: basename(file.path);
final resp = await client.post(
'/api/attachments',
FormData({
'alt': fileAlt,
'file': filePayload,
'hash': hash,
'usage': usage,
'metadata': jsonEncode({
if (ratio != null) 'ratio': ratio,
}),
}),
);
if (resp.statusCode == 200) {
return resp;
List<Attachment?> result = List.filled(id.length, null);
List<int> pendingQuery = List.empty(growable: true);
if (!noCache) {
for (var idx = 0; idx < id.length; idx++) {
if (_cachedResponses.containsKey(id[idx])) {
result[idx] = _cachedResponses[id[idx]];
} else {
pendingQuery.add(id[idx]);
}
}
}
throw Exception(resp.bodyString);
final resp = await get(
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
);
if (resp.statusCode != 200) return result;
final rawOut = PaginationResult.fromJson(resp.body);
if (rawOut.data == null) return result;
final List<Attachment> out =
rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
for (final item in out) {
if (item.destination != 0 && item.isAnalyzed) {
_cachedResponses[item.id] = item;
}
}
for (var i = 0; i < out.length; i++) {
for (var j = 0; j < id.length; j++) {
if (out[i].id == id[j]) {
result[j] = out[i];
}
}
}
return result;
}
Future<Attachment?> getMetadata(int id, {noCache = false}) async {
if (!noCache && _cachedResponses.containsKey(id)) {
return _cachedResponses[id]!;
}
final resp = await get('/attachments/$id/meta');
if (resp.statusCode == 200) {
final result = Attachment.fromJson(resp.body);
if (result.destination != 0 && result.isAnalyzed) {
_cachedResponses[id] = result;
}
return result;
}
return null;
}
Future<Attachment> createAttachment(
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
{Function(double)? onProgress}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
await auth.ensureCredentials();
final filePayload =
dio.MultipartFile.fromBytes(data, filename: basename(path));
final fileAlt = basename(path).contains('.')
? basename(path).substring(0, basename(path).lastIndexOf('.'))
: basename(path);
final fileExt = basename(path)
.substring(basename(path).lastIndexOf('.') + 1)
.toLowerCase();
// Override for some files cannot be detected mimetype by server-side
String? mimetypeOverride;
if (mimetypeOverrides.keys.contains(fileExt)) {
mimetypeOverride = mimetypeOverrides[fileExt];
}
final payload = dio.FormData.fromMap({
'alt': fileAlt,
'file': filePayload,
'usage': usage,
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
'metadata': jsonEncode(metadata),
});
final resp = await dio.Dio(
dio.BaseOptions(
baseUrl: ServiceFinder.buildUrl('files', null),
headers: {'Authorization': 'Bearer ${auth.credentials!.accessToken}'},
),
).post(
'/attachments',
data: payload,
onSendProgress: (count, total) {
if (onProgress != null) onProgress(count / total);
},
);
if (resp.statusCode != 200) {
throw Exception(resp.data);
}
return Attachment.fromJson(resp.data);
}
Future<Response> updateAttachment(
int id,
String alt,
String usage, {
double? ratio,
bool isMature = false,
}) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
client.httpClient.addAuthenticator(auth.reqAuthenticator);
final client = auth.configureClient('files');
var resp = await client.put('/api/attachments/$id', {
'metadata': {
if (ratio != null) 'ratio': ratio,
},
var resp = await client.put('/attachments/$id', {
'alt': alt,
'usage': usage,
'is_mature': isMature,
@@ -93,22 +152,28 @@ class AttachmentProvider extends GetConnect {
throw Exception(resp.bodyString);
}
return resp.body;
return resp;
}
Future<Response> deleteAttachment(int id) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
client.httpClient.addAuthenticator(auth.reqAuthenticator);
final client = auth.configureClient('files');
var resp = await client.delete('/api/attachments/$id');
var resp = await client.delete('/attachments/$id');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
void clearCache({int? id}) {
if (id != null) {
_cachedResponses.remove(id);
} else {
_cachedResponses.clear();
}
}
}

View File

@@ -0,0 +1,163 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/account/relative_select.dart';
import 'package:uuid/uuid.dart';
class ChannelProvider extends GetxController {
RxBool isLoading = false.obs;
RxList<Channel> availableChannels = RxList.empty(growable: true);
List<Channel> get groupChannels =>
availableChannels.where((x) => x.type == 0).toList();
List<Channel> get directChannels =>
availableChannels.where((x) => x.type == 1).toList();
Future<void> refreshAvailableChannel() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
isLoading.value = true;
final resp = await listAvailableChannel();
isLoading.value = false;
availableChannels.value =
resp.body.map((x) => Channel.fromJson(x)).toList().cast<Channel>();
availableChannels.refresh();
}
Future<Response> getChannel(String alias, {String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$realm/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> getMyChannelProfile(String alias,
{String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$realm/$alias/me');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response?> getChannelOngoingCall(String alias,
{String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$realm/$alias/calls/ongoing');
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> listChannel({String scope = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$scope');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> listAvailableChannel({String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$realm/me/available');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> createChannel(String scope, dynamic payload) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.post('/channels/$scope', payload);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response?> createDirectChannel(
BuildContext context, String scope) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final related = await showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => RelativeSelector(
title: 'channelOrganizeDirectHint'.tr,
),
);
if (related == null) return null;
final prof = auth.userProfile.value!;
final client = auth.configureClient('messaging');
final resp = await client.post('/channels/$scope/dm', {
'alias': const Uuid().v4().replaceAll('-', '').substring(0, 12),
'name': 'DM',
'description':
'A direct message channel between @${prof['name']} and @${related.name}',
'related_user': related.id,
'is_encrypted': false,
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> updateChannel(String scope, int id, dynamic payload) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.put('/channels/$scope/$id', payload);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
}

View File

@@ -1,11 +0,0 @@
import 'package:get/get.dart';
import 'package:solian/services.dart';
class PostExploreProvider extends GetConnect {
@override
void onInit() {
httpClient.baseUrl = ServiceFinder.services['interactive'];
}
Future<Response> listPost(int page) => get('/api/feed?take=${10}&offset=$page');
}

View File

@@ -0,0 +1,91 @@
import 'package:get/get.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
class PostProvider extends GetConnect {
@override
void onInit() {
httpClient.baseUrl = ServiceFinder.buildUrl('interactive', null);
}
Future<Response> listRecommendations(int page,
{int? realm, String? channel}) async {
final queries = [
'take=${10}',
'offset=$page',
if (realm != null) 'realmId=$realm',
];
final resp = await get(
channel == null
? '/recommendations?${queries.join('&')}'
: '/recommendations/$channel?${queries.join('&')}',
);
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
return resp;
}
Future<Response> listDraft(int page) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final queries = [
'take=${10}',
'offset=$page',
];
final client = auth.configureClient('interactive');
final resp = await client.get('/posts/drafts?${queries.join('&')}');
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
return resp;
}
Future<Response> listPost(int page,
{int? realm, String? author, tag, category}) async {
final queries = [
'take=${10}',
'offset=$page',
if (tag != null) 'tag=$tag',
if (category != null) 'category=$category',
if (author != null) 'author=$author',
if (realm != null) 'realmId=$realm',
];
final resp = await get('/posts?${queries.join('&')}');
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
return resp;
}
Future<Response> listPostReplies(String alias, int page) async {
final resp = await get('/posts/$alias/replies?take=${10}&offset=$page');
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
return resp;
}
Future<Response> getPost(String alias) async {
final resp = await get('/posts/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
return resp;
}
Future<Response> getArticle(String alias) async {
final resp = await get('/articles/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
return resp;
}
}

View File

@@ -0,0 +1,49 @@
import 'package:get/get.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart';
class RealmProvider extends GetxController {
RxBool isLoading = false.obs;
RxList<Realm> availableRealms = RxList.empty(growable: true);
Future<void> refreshAvailableRealms() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
isLoading.value = true;
final resp = await listAvailableRealm();
isLoading.value = false;
availableRealms.value =
resp.body.map((x) => Realm.fromJson(x)).toList().cast<Realm>();
availableRealms.refresh();
}
Future<Response> getRealm(String alias) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('auth');
final resp = await client.get('/realms/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> listAvailableRealm() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('auth');
final resp = await client.get('/realms/me/available');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
}

View File

@@ -0,0 +1,151 @@
import 'package:floor/floor.dart';
import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/message/events.dart';
Future<MessageHistoryDb> createHistoryDb() async {
final migration1to2 = Migration(1, 2, (database) async {
await database.execute('DROP TABLE IF EXISTS LocalMessage');
});
return await $FloorMessageHistoryDb
.databaseBuilder('messaging_data.dart')
.addMigrations([migration1to2]).build();
}
Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/channels/$scope/${channel.alias}/events/$id',
);
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return Event.fromJson(resp.body);
}
Future<(List<Event>, int)?> getRemoteEvents(
Channel channel,
String scope, {
required int remainDepth,
bool Function(List<Event> items)? onBrake,
take = 10,
offset = 0,
}) async {
if (remainDepth <= 0) {
return null;
}
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
final result =
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
if (onBrake != null && onBrake(result)) {
return (result, response.count);
}
final expandResult = (await getRemoteEvents(
channel,
scope,
remainDepth: remainDepth - 1,
take: take,
offset: offset + result.length,
))
?.$1 ??
List.empty();
return ([...result, ...expandResult], response.count);
}
extension MessageHistoryAdaptor on MessageHistoryDb {
Future<LocalEvent> receiveEvent(Event remote) async {
final entry = LocalEvent(
remote.id,
remote,
remote.channelId,
remote.createdAt,
);
await localEvents.insert(entry);
switch (remote.type) {
case 'messages.edit':
final body = EventMessageBody.fromJson(remote.body);
if (body.relatedEvent != null) {
final target = await localEvents.findById(body.relatedEvent!);
if (target != null) {
target.data.body = remote.body;
target.data.updatedAt = remote.updatedAt;
await localEvents.update(target);
}
}
case 'messages.delete':
final body = EventMessageBody.fromJson(remote.body);
if (body.relatedEvent != null) {
await localEvents.delete(body.relatedEvent!);
}
}
return entry;
}
Future<LocalEvent?> getEvent(int id, Channel channel,
{String scope = 'global'}) async {
final localRecord = await localEvents.findById(id);
if (localRecord != null) return localRecord;
final remoteRecord = await getRemoteEvent(id, channel, scope);
if (remoteRecord == null) return null;
return await receiveEvent(remoteRecord);
}
Future<(List<Event>, int)?> syncRemoteEvents(Channel channel,
{String scope = 'global', depth = 10, offset = 0}) async {
final lastOne = await localEvents.findLastByChannel(channel.id);
final data = await getRemoteEvents(
channel,
scope,
remainDepth: depth,
offset: offset,
onBrake: (items) {
return items.any((x) => x.id == lastOne?.id);
},
);
if (data != null) {
await localEvents.insertBulk(
data.$1
.map((x) => LocalEvent(x.id, x, x.channelId, x.createdAt))
.toList(),
);
}
return data;
}
Future<List<LocalEvent>> listEvents(Channel channel) async {
return await localEvents.findAllByChannel(channel.id);
}
}

View File

@@ -0,0 +1,83 @@
import 'dart:async';
import 'dart:convert';
import 'package:floor/floor.dart';
import 'package:solian/models/event.dart';
import 'package:sqflite/sqflite.dart' as sqflite;
part 'events.g.dart';
@entity
class LocalEvent {
@primaryKey
final int id;
final Event data;
final int channelId;
final DateTime createdAt;
LocalEvent(this.id, this.data, this.channelId, this.createdAt);
}
class DateTimeConverter extends TypeConverter<DateTime, int> {
@override
DateTime decode(int databaseValue) {
return DateTime.fromMillisecondsSinceEpoch(databaseValue);
}
@override
int encode(DateTime value) {
return value.millisecondsSinceEpoch;
}
}
class RemoteEventConverter extends TypeConverter<Event, String> {
@override
Event decode(String databaseValue) {
return Event.fromJson(jsonDecode(databaseValue));
}
@override
String encode(Event value) {
return jsonEncode(value.toJson());
}
}
@dao
abstract class LocalEventDao {
@Query('SELECT COUNT(id) FROM LocalEvent WHERE channelId = :channelId')
Future<int?> countByChannel(int channelId);
@Query('SELECT * FROM LocalEvent WHERE id = :id')
Future<LocalEvent?> findById(int id);
@Query('SELECT * FROM LocalEvent WHERE channelId = :channelId ORDER BY createdAt DESC')
Future<List<LocalEvent>> findAllByChannel(int channelId);
@Query('SELECT * FROM LocalEvent WHERE channelId = :channelId ORDER BY createdAt DESC LIMIT 1')
Future<LocalEvent?> findLastByChannel(int channelId);
@Insert(onConflict: OnConflictStrategy.replace)
Future<void> insert(LocalEvent m);
@Insert(onConflict: OnConflictStrategy.replace)
Future<void> insertBulk(List<LocalEvent> m);
@Update(onConflict: OnConflictStrategy.replace)
Future<void> update(LocalEvent m);
@Query('DELETE FROM LocalEvent WHERE id = :id')
Future<void> delete(int id);
@Query('DELETE FROM LocalEvent WHERE channelId = :channelId')
Future<List<LocalEvent>> deleteByChannel(int channelId);
@Query('DELETE FROM LocalEvent')
Future<void> wipeLocalEvents();
}
@TypeConverters([DateTimeConverter, RemoteEventConverter])
@Database(version: 2, entities: [LocalEvent])
abstract class MessageHistoryDb extends FloorDatabase {
LocalEventDao get localEvents;
}

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