Compare commits

..

222 Commits

Author SHA1 Message Date
98749f42c0 ⬆️ Upgrade deps 2024-08-17 19:18:51 +08:00
f0e6bd64f4 ♻️ Refactor video player 2024-08-17 19:02:57 +08:00
3bea3a114a Post alias 2024-08-17 18:44:20 +08:00
454f711656 ⬆️ Upgrade deps 2024-08-16 23:27:38 +08:00
82e4c923e7 📈 Simple log user share 2024-08-16 23:08:05 +08:00
5b4d8282ae Re-google (firebase) 2024-08-16 22:59:34 +08:00
cf767a1d94 💄 Optimized post editor 2024-08-16 21:06:50 +08:00
af93a8386a ⬆️ Upgrade deps 2024-08-16 01:05:21 +08:00
29ca263130 🚀 Launch 1.2.1+13 2024-08-16 01:03:55 +08:00
7332f68d9c Live preview of post editor 2024-08-16 00:52:36 +08:00
e9e6f3313e 👽 Use capital to deal with mfa 2024-08-13 10:54:42 +08:00
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
ef1f29f905 🐛 Fix edit post won't rollback thumbnail 2024-08-11 02:07:09 +08:00
22026efa7d Thumbnail 2024-08-11 01:57:58 +08:00
4a3e6a9e15 🚀 Launch 1.2.1+12 2024-08-11 00:50:25 +08:00
00092ba7b6 Some useful options 2024-08-11 00:36:27 +08:00
b5da8ece4a Use capital share link 2024-08-10 18:24:47 +08:00
dfe9165bc9 🐛 Bug fixes on upload attachment 2024-08-10 01:17:31 +08:00
3d45b54236 ⬆️ Upgrade flutter & deps 2024-08-10 01:16:40 +08:00
7f63fe7f0e 💄 Better sidebar navigation 2024-08-10 00:51:54 +08:00
bc5dbab9c5 Dismissible refresh notification 2024-08-10 00:49:21 +08:00
9910fc7a92 Channel content auto refresh after long time background activity 2024-08-10 00:43:55 +08:00
2356eac118 Better side navigation bar 2024-08-09 22:59:24 +08:00
0135b8d838 Better screenshare 2024-08-09 22:40:05 +08:00
8ec33ccbf4 🚨 Fix CarouselController import issue 2024-08-07 19:21:01 +08:00
d267316a35 💄 Better emotes 2024-08-07 19:11:52 +08:00
138da60e55 🚸 Prevent user from sending empty message 2024-08-07 19:02:49 +08:00
4562c2f991 🐛 Fix able send space message 2024-08-07 18:31:26 +08:00
8009f4ca9b 💄 Better sidebar navigation 2024-08-07 18:24:16 +08:00
54dee9702b 🐛 Fix attachments max width 2024-08-07 14:34:41 +08:00
94385564bd 🐛 Fix dupe attachment notification 2024-08-07 14:27:23 +08:00
0b2309816f 🐛 Fix desktop panic when download things 2024-08-07 13:50:50 +08:00
8283272a3b 🗑️ Fix mis-import 2024-08-07 01:49:03 +08:00
eb02a47e9a 💄 Fixes and improvements 2024-08-07 01:47:53 +08:00
7c0c1ec94f 💄 Optimize styles 2024-08-07 01:20:23 +08:00
272044a77e 💄 Optimize logo in signup & signin popup 2024-08-07 01:06:57 +08:00
39c22b1cf6 Sticker has pack id 2024-08-07 00:56:06 +08:00
98c3bb912d Stickers auto resize 2024-08-07 00:52:34 +08:00
035b92d9b8 Rollback sized container 2024-08-07 00:12:44 +08:00
0bfc0bd61b 🌐 Update en translation 2024-08-07 00:08:29 +08:00
de00a20eee 💄 Better call ui 2024-08-06 23:23:02 +08:00
73982f48d6 🐛 Bug fixes 2024-08-06 20:00:13 +08:00
1d36b30361 Video won't load until click 2024-08-06 19:39:07 +08:00
dea743a307 Username hint 2024-08-06 18:34:46 +08:00
c48bd3e758 Stickers hint 2024-08-06 18:18:40 +08:00
56bbf73b5e Better sticker & able embed attachment into markdown 2024-08-06 16:24:47 +08:00
4f6c5aa053 🐛 Bug fixes 2024-08-04 21:12:35 +08:00
d8e79fb4f9 🚀 Launch 1.2.1+5 2024-08-04 20:49:11 +08:00
06e0fa465b Article has special badge 2024-08-04 20:48:51 +08:00
895a257f50 Better overflow effect 2024-08-04 20:43:25 +08:00
d9804ba00b 🚸 Enhanced share feature 2024-08-04 18:32:16 +08:00
62ff1c2f1c 🚀 Launch 1.2.1+4 2024-08-04 18:14:28 +08:00
a157596a2e Optimize and fixes 2024-08-04 18:13:59 +08:00
12102bf527 Limit content and read more in posts 2024-08-04 17:39:22 +08:00
c00a018380 🐛 Fix draft box 2024-08-04 17:15:56 +08:00
53b3cac4ca Show hint when dismissible error 2024-08-04 16:26:05 +08:00
19eabfaba1 🚀 Launch 1.2.1+2 2024-08-04 13:27:14 +08:00
ec2eadad6d 🐛 Fix bootstrapper icon issue 2024-08-04 12:59:13 +08:00
54e176e75d 🐛 Fix post editor cannot reply either repost 2024-08-04 12:55:05 +08:00
0a7ccaeefa 🐛 Fix attachment editor title overflow 2024-08-04 12:23:39 +08:00
a5f093e185 🐛 Fix unauthorized wont load stickers 2024-08-04 11:10:25 +08:00
a4f68dd175 🚀 Launch 1.2.1+1 2024-08-04 01:54:35 +08:00
8067c35c70 Follow the manifest to load emotes 2024-08-04 01:53:52 +08:00
ebe381053e Load emojis 2024-08-04 01:37:54 +08:00
03f2470dae Basic sticker management 2024-08-04 01:03:09 +08:00
ea434815cf Create sticker
 Single file mode attachment editor and more options
2024-08-03 21:29:48 +08:00
bbea4b4359 🍱 Update app icons 2024-08-03 17:44:36 +08:00
e0b485cc81 🐛 Fix mis-style 2024-08-03 14:00:52 +08:00
87bb37ac01 ⚗️ Markdown embed content 2024-08-03 12:29:13 +08:00
989b5babd9 Auto update checking 2024-08-03 01:14:42 +08:00
9ea364640d 🚀 Launch 1.2.0+8 2024-08-02 23:24:36 +08:00
a9f55a489d ⬆️ Clean and upgrade packages 2024-08-02 23:22:50 +08:00
4616f3a3e2 Friend request indicator 2024-08-02 23:15:28 +08:00
425bae9d13 💄 Better friend page loading indicator 2024-08-02 22:54:56 +08:00
07771e8979 Improve the speed of fetching attachments meta via batch api 2024-08-02 22:46:48 +08:00
0ad4854443 💄 Grid view in call 2024-08-02 21:12:37 +08:00
4238ea6fdc Call grid layout 2024-08-02 18:49:28 +08:00
7d45c06302 💄 Optimized signal indicator 2024-08-02 18:29:01 +08:00
7e8993fbd2 💫 Auto hide or show call controls 2024-08-02 18:09:07 +08:00
c88fcc84da Show call participants 2024-08-02 17:14:23 +08:00
11fb79623e Attachment can link exists things
 Optimize upload progress
2024-08-02 15:49:32 +08:00
98cc313a91 💫 Optimize chat event list animation 2024-08-02 14:14:09 +08:00
bc3401a897 🐛 Fix post item color mismatch 2024-08-02 05:10:10 +08:00
5b6a5d9046 🐛 Fix post popup color mismatch 2024-08-02 05:04:31 +08:00
6cbd78e836 💫 Optimize post editor transition 2024-08-02 04:59:35 +08:00
aefcbad02f 💫 Better animated post list 2024-08-02 04:42:38 +08:00
70617be687 💫 Animated chat 2024-08-02 04:24:12 +08:00
cccb3d5c16 🐛 Fix post won't refresh after post 2024-08-02 01:00:31 +08:00
a0a3a8d182 DM message last preview 2024-08-02 00:54:19 +08:00
c6b2ef8459 💄 Better about 2024-08-02 00:41:12 +08:00
34a2fe3988 Move about page link from account to settings 2024-08-02 00:29:51 +08:00
0a5604d0ff Crop image in personalize 2024-08-02 00:12:16 +08:00
5e754ad233 💫 About page icon will rotate 2024-08-01 23:51:03 +08:00
5b9c92e4d3 Crop image 2024-08-01 23:44:07 +08:00
b2a6ca7244 Improve attachments queue performance 2024-08-01 23:10:19 +08:00
27c60fc8cb Block user action when attachments isn't ready 2024-08-01 22:36:00 +08:00
8b3c45ab29 Queued upload 2024-08-01 22:13:08 +08:00
adb415700a 💄 Optimized attachment edit action 2024-08-01 17:19:55 +08:00
1e4b44a78b 💄 Better attachment editor previewing 2024-08-01 16:45:18 +08:00
9765b200b9 🐛 Fix content previewing will show attachments 2024-08-01 16:28:48 +08:00
47d03ce1e5 🐛 Bug fixes 2024-08-01 16:09:09 +08:00
c41a71388d Post with publish at and until 2024-08-01 15:49:42 +08:00
7655dfdf37 Post publish zone 2024-08-01 15:21:43 +08:00
190bb34958 Markdown toolbar 2024-08-01 14:46:01 +08:00
d02ed68afa Mention user in chat 2024-08-01 14:01:12 +08:00
2bc4513bb6 🐛 Fix post tag input issue 2024-08-01 11:49:28 +08:00
f10393f6d0 Download attachment 2024-08-01 02:10:57 +08:00
ecef8dab0c Fix post list ui jank 2024-08-01 01:21:27 +08:00
52e58fce3d Make theme switcher easier to use 2024-07-31 22:48:22 +08:00
31d50bfb1f 🐛 Fix web url issue 2024-07-31 21:01:32 +08:00
ca8ad12d93 🍱 Update font 2024-07-31 20:45:36 +08:00
f799900450 🐛 Fix crash on ratio 1 in attachment 2024-07-31 20:45:16 +08:00
dfdf7b23c8 🐛 Fix theme switching 2024-07-31 13:29:26 +08:00
771b2029b0 🍱 Add fonts 2024-07-31 13:29:17 +08:00
cc9c99f375 Global theme color 2024-07-31 02:44:49 +08:00
b70d3795d1 Better tags input 2024-07-31 02:00:03 +08:00
a16ff1b9a1 🍱 Update app icon 2024-07-30 21:24:30 +08:00
19751617cb Able to edit visibility 2024-07-30 20:49:01 +08:00
bb77b74356 Able to post article 2024-07-30 16:53:13 +08:00
fc77c8693f Post editor able to edit article 2024-07-30 16:44:04 +08:00
58bb549217 Post content local cache 2024-07-30 16:29:30 +08:00
6590062dcb Post overview w/ content length limit indicator 2024-07-30 14:49:26 +08:00
6ace977bf6 💄 Better fullscreen attachment viewer 2024-07-30 12:22:57 +08:00
387f0d14ac ⬆️ Upgrade packages 2024-07-30 12:21:39 +08:00
18bb0d3db2 🍱 Update app icon for v1.2.0 2024-07-30 11:50:26 +08:00
8ab3ca5633 ⬆️ Support latest Paperclip 2024-07-29 18:06:38 +08:00
3db6850d89 🐛 Fix attachment displaying according the latest server 2024-07-29 17:56:36 +08:00
3ca98fa58c 🐛 Fix share link issue 2024-07-27 20:37:04 +08:00
425c79d6fc 🚀 Launch the last version of 1.1.0 2024-07-27 20:34:02 +08:00
7e98edfbc9 🐛 Fix web issue 2024-07-27 20:27:29 +08:00
056b98db07 🐛 Fix web 404 issue 2024-07-27 19:58:44 +08:00
7bfbd37b76 🐛 Fix attachment fullscreen in dark mode 2024-07-27 19:52:22 +08:00
7800a70ef2 Deep link 2024-07-27 19:20:53 +08:00
74b6ccd5c7 🐛 Fix share 2024-07-27 14:32:31 +08:00
6ca4aad1c4 💄 Better bootstrapping 2024-07-27 14:16:49 +08:00
102df2ef1c Share 2024-07-27 02:11:59 +08:00
f08c9903b4 Bootstrapper 2024-07-27 01:39:20 +08:00
0d279842cf 💄 Better full screen attachment display 2024-07-27 00:20:11 +08:00
33d69908a6 Social credit points & quick send friend request 2024-07-26 22:37:08 +08:00
4552dfd3f3 Pinned post & Total vote counts 2024-07-26 18:23:51 +08:00
ae87e9ad31 💄 Optimized album page 2024-07-26 17:35:54 +08:00
277ba69513 Account profile page 2024-07-26 16:53:05 +08:00
6e3d0f9787 💄 Better attachments in posts 2024-07-26 14:21:00 +08:00
0237409d27 🐛 Fix search with tag won't work 2024-07-26 01:31:45 +08:00
a5b6ace79b 💄 Better attachments list styles 2024-07-26 01:16:32 +08:00
42c3e5ff0a Shuffle mode swiper 2024-07-25 16:08:46 +08:00
7dc198f0a7 ♻️ Post list controller layer 2024-07-25 14:42:50 +08:00
fa3ba0e188 Shuffle mode 2024-07-25 02:00:29 +08:00
02c28533db 💄 Optimized post create popup 2024-07-25 01:43:50 +08:00
6d92a16a62 ♻️ Refactored auth system 2024-07-25 01:18:47 +08:00
ef58430060 🐛 Fix error when body haven't attachment in post 2024-07-24 16:28:29 +08:00
8366bda846 ♻️ Refactored friend module 2024-07-24 01:17:41 +08:00
39c8597428 🐛 Fix notification list render issue 2024-07-23 22:09:20 +08:00
e91b4b0947 ⬆️ Support latest version of server 2024-07-23 18:09:41 +08:00
3545a0737d 🐛 Fix macos ITMS-90894 2024-07-23 11:19:27 +08:00
58b3d75896 🐛 Fix NSE 2024-07-23 11:18:06 +08:00
f69339292b ⚗️ Add NSE into macos platform 2024-07-22 00:04:12 +08:00
62edab0131 Bug fixes in notification and support iOS Communication Notification! 2024-07-21 23:43:18 +08:00
dbd05dbb79 🔨 Fix iOS building 2024-07-20 19:29:23 +08:00
dac7440477 🍺 Add experimental iOS notification service extensions 2024-07-20 16:12:26 +08:00
0573ee456e ♻️ Improved image analyzer in attachments 2024-07-19 23:56:59 +08:00
5a7432e330 ⬆️ Support new notification APIs 2024-07-19 23:38:25 +08:00
6811d8e9b1 Optimization and show stack trace in error dialog 2024-07-17 11:38:25 +08:00
e068c72b69 🔨 Build linux workflow 2024-07-17 11:20:18 +08:00
ca72a44a86 🔨 Make windows artificial smaller [skip ci] 2024-07-16 21:07:57 +08:00
47d3fc90a3 🔨 Fix gh actions workflow android missing java 2024-07-16 20:48:52 +08:00
e38d8339f1 🔨 Update github action workflow 2024-07-16 20:44:13 +08:00
8c04b81b7c 💚 Fix action workflow 2024-07-16 20:34:47 +08:00
9f3485a2a8 🔨 Add github action 2024-07-16 20:33:10 +08:00
da265da61d ⬆️ Upgrade to support latest version of server 2024-07-16 19:46:53 +08:00
286dd8193d 🐛 Fix crashes on android 2024-07-13 21:03:56 +08:00
6311322c4a Suspended account tip 2024-07-13 19:09:04 +08:00
a68a78597e 💄 Optimized for navigation drawer 2024-07-13 18:54:08 +08:00
201c38800b 🐛 Fix drawer unreasonable round corner 2024-07-12 23:43:41 +08:00
df7348e117 Add max height to attachments 2024-07-12 22:50:52 +08:00
156e6f1075 Adaptive app bar leading 2024-07-12 22:37:58 +08:00
a2db9a7ae4 App bar leading icon for drawer 2024-07-12 22:31:45 +08:00
1a26880719 ♻️ Chat listening on sidebar 2024-07-12 21:59:16 +08:00
aa43eaa0eb ♻️ Refactored navigation 2024-07-12 16:19:54 +08:00
48b76ed574 Account status on sidebar 2024-07-12 13:15:46 +08:00
3b1b6ec8d6 Drawer navigation 2024-07-12 11:39:44 +08:00
a6d8e2e311 💄 Better ui 2024-07-12 00:44:57 +08:00
8dbf6ff4f3 Articles 2024-07-10 10:50:10 +08:00
505290b2ae Basic article rendering (overview) 2024-07-10 00:44:10 +08:00
065cda27e9 Articles writing 2024-07-09 23:06:55 +08:00
fa600d6c69 Draft box 2024-07-09 22:39:44 +08:00
a0fe3f918e Post draft 2024-07-09 21:23:38 +08:00
10ed44d2e2 💄 Optimize tags 2024-07-08 19:56:03 +08:00
b241956ce7 🐛 Fix focus track still exists after that track disappeared 2024-07-07 14:45:26 +08:00
d4cd120431 🐛 Make system bar appear in call screen 2024-07-07 14:41:32 +08:00
60d7df4496 Search with tag & category 2024-07-07 14:22:53 +08:00
f7cc4420b3 Attachment preview 2024-07-07 13:38:43 +08:00
5864041e57 Replace pasteboard and drop zone deps 2024-07-07 13:16:08 +08:00
f231fc9ec0 Display post's tag 2024-07-07 12:33:54 +08:00
75c753ef63 Optimized refresh credentials 2024-07-07 11:56:25 +08:00
22ee817676 Support new feed API
 Able to add tag onto post
2024-07-07 11:46:48 +08:00
f8bed6946e 💄 Better posting page 2024-07-07 03:02:10 +08:00
b2a2d38c3d 💄 Better macos window 2024-07-07 02:45:13 +08:00
343b84e3e1 Better post list 2024-07-06 21:14:19 +08:00
66ddfea68d 💄 Better bottom navigation 2024-07-06 20:55:53 +08:00
a304b26c96 ♻️ Optimized video lib 2024-07-06 19:07:46 +08:00
7d087af4cd Optimized attachments 2024-07-06 18:35:43 +08:00
90daff5b97 Optimized channel list 2024-07-06 18:17:54 +08:00
cc59814b55 Optimized websocket 2024-07-06 17:39:19 +08:00
b808c76ea3 Optimized chat messages 2024-07-06 17:12:57 +08:00
20a82da2fa Basic optimization of repainting 2024-07-05 23:37:54 +08:00
867b024285 🐛 Bug fixes in sign up 2024-07-02 23:26:17 +08:00
f1abdad54d Password reset 2024-06-30 18:03:02 +08:00
9d54b04f77 💄 Better paste & drag 'n drop handling 2024-06-30 17:43:36 +08:00
8c7de68e7a 💄 Optimize window on windows 2024-06-29 23:49:18 +08:00
31513b0e84 🐛 Fix attachment meta won't load 2024-06-29 22:35:56 +08:00
65333ccef6 💄 Optimize platform specfic code 2024-06-29 22:29:21 +08:00
49f999871a Paste to upload 2024-06-29 21:03:15 +08:00
e336d2372a Drag to upload 2024-06-29 20:25:29 +08:00
85bba21285 💄 Sending message indicator 2024-06-29 18:40:26 +08:00
df2f04d2b2 🐛 Bug fixes 2024-06-29 18:29:30 +08:00
fdeb52bf38 💄 Better post rendering 2024-06-29 18:19:52 +08:00
fffad00f00 💄 Better multi-factor authenticate callback experience
 Support custom app protocol solink://
2024-06-29 18:09:56 +08:00
6b0f644353 🐛 Fix bugs and optimize your auth experience 2024-06-29 17:35:18 +08:00
7b45d95fd6 🐛 Fix call last duration issue 2024-06-28 19:36:48 +08:00
424be16ab0 🐛 Fix null value in event body 2024-06-28 19:33:59 +08:00
29a975235c 🍱 Update android manifest 2024-06-28 19:33:49 +08:00
232 changed files with 13505 additions and 5037 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

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

View File

@@ -1,6 +1,7 @@
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"
}
@@ -23,26 +24,36 @@ if (flutterVersionName == null) {
flutterVersionName = "1.0"
}
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
namespace = "dev.solsynth.solian"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
ndkVersion = "26.1.10909125"
defaultConfig {
multiDexEnabled true
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
defaultConfig {
applicationId = "dev.solsynth.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.
minSdk = flutter.minSdkVersion
minSdkVersion 23
multiDexEnabled true
targetSdk = flutter.targetSdkVersion
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
@@ -50,7 +61,8 @@ android {
buildTypes {
release {
signingConfig = signingConfigs.debug
// signingConfig = signingConfigs.debug
signingConfig = signingConfigs.release
}
}
}

View File

@@ -1,60 +1,86 @@
<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-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/launcher_icon"
android:supportsRtl="true">
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
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="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:taskAffinity=""
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 and

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

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.

Before

Width:  |  Height:  |  Size: 1.5 KiB

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.

Before

Width:  |  Height:  |  Size: 3.3 KiB

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.

Before

Width:  |  Height:  |  Size: 5.0 KiB

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.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -1,4 +1,6 @@
allprojects {
ext.kotlin_version = "2.0.0"
repositories {
google()
mavenCentral()
@@ -10,6 +12,17 @@ subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
// 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")
}

View File

@@ -20,7 +20,8 @@ plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.4.0' apply false
id "com.google.gms.google-services" version "4.3.15" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" 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: 537 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: 53 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 360 KiB

View File

@@ -1,3 +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

View File

@@ -38,30 +38,75 @@ PODS:
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/CoreOnly (10.27.0):
- FirebaseCore (= 10.27.0)
- Firebase/Messaging (10.27.0):
- Firebase/Analytics (10.29.0):
- Firebase/Core
- Firebase/Core (10.29.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 10.27.0)
- firebase_core (3.1.0):
- Firebase/CoreOnly (= 10.27.0)
- Flutter
- firebase_messaging (15.0.1):
- Firebase/Messaging (= 10.27.0)
- 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
- FirebaseCore (10.27.0):
- 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)
- FirebaseCoreInternal (10.28.0):
- FirebaseCoreExtension (10.29.0):
- FirebaseCore (~> 10.0)
- FirebaseCoreInternal (10.29.0):
- "GoogleUtilities/NSData+zlib (~> 7.8)"
- FirebaseInstallations (10.28.0):
- 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.27.0):
- FirebaseMessaging (10.29.0):
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleDataTransport (~> 9.3)
@@ -70,14 +115,47 @@ PODS:
- 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_local_notifications (0.0.1):
- flutter_keyboard_visibility (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- flutter_webrtc (0.9.36):
- flutter_webrtc (0.11.3):
- Flutter
- WebRTC-SDK (= 114.5735.10)
- 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)
@@ -93,6 +171,9 @@ PODS:
- 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"
@@ -107,11 +188,14 @@ PODS:
- 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.0):
- livekit_client (2.2.4):
- Flutter
- WebRTC-SDK (= 114.5735.10)
- WebRTC-SDK (= 125.6422.04)
- nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0)
@@ -119,20 +203,25 @@ PODS:
- 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
- permission_handler_apple (9.3.0):
- Flutter
- PromisesObjC (2.4.0)
- SDWebImage (5.19.2):
- SDWebImage/Core (= 5.19.2)
- SDWebImage/Core (5.19.2)
- Sentry/HybridSDK (8.29.0)
- sentry_flutter (8.3.0):
- 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
- FlutterMacOS
- Sentry/HybridSDK (= 8.29.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -140,6 +229,7 @@ PODS:
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- TOCropViewController (2.7.4)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
@@ -147,24 +237,31 @@ PODS:
- FlutterMacOS
- wakelock_plus (0.0.1):
- Flutter
- WebRTC-SDK (114.5735.10)
- 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_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- 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`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/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`)
@@ -176,17 +273,24 @@ SPEC REPOS:
- DKImagePickerController
- DKPhotoGallery
- Firebase
- FirebaseAnalytics
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations
- FirebaseMessaging
- FirebaseRemoteConfigInterop
- FirebaseSessions
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
- nanopb
- PromisesObjC
- PromisesSwift
- SDWebImage
- Sentry
- SwiftyGif
- TOCropViewController
- WebRTC-SDK
EXTERNAL SOURCES:
@@ -196,30 +300,44 @@ EXTERNAL SOURCES:
: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_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
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"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/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:
@@ -237,36 +355,50 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Firebase: 26b040b20866a55f55eb3611b9fcf3ae64816b86
firebase_core: 483cfad66d24d8f3c233f31db4263830c625c909
firebase_messaging: e60c0694699d8a2e56a319e043709583f6544123
FirebaseCore: a2b95ae4ce7c83ceecfbbbe3b6f1cddc7415a808
FirebaseCoreInternal: 58d07f1362fddeb0feb6a857d1d1d1c5e558e698
FirebaseInstallations: 60c1d3bc1beef809fd1ad1189a8057a040c59f2e
FirebaseMessaging: 585984d0a1df120617eb10b44cad8968b859815e
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_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_webrtc: b33475c3a57d59ff05bf87b4f5d3feceac63f291
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleAppMeasurement: f9de05ee17401e3355f68e8fc8b5064d429f5918
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
livekit_client: 2b3f5185f95d46d62d3570bf981f3d98ad3051e2
livekit_client: d079c5f040d4bf2b80440ff0ae997725a183e4bc
nanopb: 438bc412db1928dac798aa6fd75726007be04262
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
pointer_interceptor_ios: 508241697ff0947f853c061945a8b822463947c1
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a
Sentry: 016de45ee5ce5fca2a829996f1bfafeb5e62e8b4
sentry_flutter: 5fb57c5b7e6427a9dc1fedde4269eb65823982d4
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: 8c0edd05b880a39648118192c252667ea06dea51
WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3
PODFILE CHECKSUM: a57f30d18f102dd3ce366b1d62a55ecbef2158e5

View File

@@ -12,6 +12,8 @@
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 */; };
@@ -27,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;
@@ -50,6 +70,9 @@
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>"; };
@@ -79,6 +102,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
730D64882C4AC4D0005A1975 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -107,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 = (
@@ -136,6 +175,7 @@
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
730D648C2C4AC4D0005A1975 /* SolianNotifyExt */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
7BA6BD8939A7BE19A2C7086C /* Pods */,
@@ -149,6 +189,7 @@
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
730D648B2C4AC4D0005A1975 /* SolianNotifyExt.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -191,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" */;
@@ -200,14 +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;
@@ -221,6 +282,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1540;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@@ -228,6 +290,9 @@
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
730D648A2C4AC4D0005A1975 = {
CreatedOnToolsVersion = 15.4;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
@@ -249,6 +314,7 @@
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
730D648A2C4AC4D0005A1975 /* SolianNotifyExt */,
);
};
/* End PBXProject section */
@@ -261,6 +327,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
730D64892C4AC4D0005A1975 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -293,6 +366,24 @@
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;
@@ -396,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;
@@ -413,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 */
@@ -491,6 +595,7 @@
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;
@@ -498,6 +603,8 @@
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",
@@ -563,6 +670,120 @@
};
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 = {
@@ -678,6 +899,7 @@
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;
@@ -685,6 +907,8 @@
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",
@@ -702,6 +926,7 @@
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;
@@ -709,6 +934,8 @@
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",
@@ -734,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,7 +1,7 @@
import UIKit
import Flutter
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
@@ -13,6 +13,5 @@ import Flutter
override func applicationDidBecomeActive(_ application: UIApplication) {
application.applicationIconBadgeNumber = 0;
UNUserNotificationCenter.current().removeAllDeliveredNotifications();
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 B

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -2,6 +2,19 @@
<!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>
@@ -55,6 +68,12 @@
<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>

View File

@@ -4,5 +4,14 @@
<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

@@ -2,7 +2,7 @@ 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/helper.dart';
import 'package:solian/providers/message/adaptor.dart';
import 'package:solian/providers/message/events.dart';
class ChatEventController {
@@ -16,7 +16,7 @@ class ChatEventController {
Channel? channel;
String? scope;
initialize() async {
Future<void> initialize() async {
if (!PlatformInfo.isWeb) {
database = await createHistoryDb();
}
@@ -57,11 +57,13 @@ class ChatEventController {
totalEvents.value = result?.$2 ?? 0;
if (result != null) {
for (final x in result.$1.reversed) {
applyEvent(LocalEvent(x.id, x, x.channelId, x.createdAt));
final entry = LocalEvent(x.id, x, x.channelId, x.createdAt);
insertEvent(entry);
applyEvent(entry);
}
}
} else {
final result = await database.syncEvents(
final result = await database.syncRemoteEvents(
channel,
scope: scope,
);
@@ -80,14 +82,16 @@ class ChatEventController {
remainDepth: 3,
offset: currentEvents.length,
);
totalEvents.value = result?.$2 ?? 0;
if (result != null) {
totalEvents.value = result.$2;
for (final x in result.$1.reversed) {
applyEvent(LocalEvent(x.id, x, x.channelId, x.createdAt));
final entry = LocalEvent(x.id, x, x.channelId, x.createdAt);
currentEvents.add(entry);
applyEvent(entry);
}
}
} else {
final result = await database.syncEvents(
final result = await database.syncRemoteEvents(
channel,
depth: 3,
scope: scope,
@@ -102,6 +106,7 @@ class ChatEventController {
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);
}
@@ -121,10 +126,11 @@ class ChatEventController {
entry = await database.receiveEvent(remote);
}
insertEvent(entry);
applyEvent(entry);
}
applyEvent(LocalEvent entry) {
insertEvent(LocalEvent entry) {
if (entry.channelId != channel?.id) return;
final idx = currentEvents.indexWhere((x) => x.data.uuid == entry.data.uuid);
@@ -133,6 +139,10 @@ class ChatEventController {
} else {
currentEvents.insert(0, entry);
}
}
applyEvent(LocalEvent entry) {
if (entry.channelId != channel?.id) return;
switch (entry.data.type) {
case 'messages.edit':

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,19 +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) {
var stack = StackTrace.current;
var stackTrace = '$stack';
return showDialog<void>(
useRootNavigator: true,
context: this,
builder: (ctx) => AlertDialog(
title: Text('errorHappened'.tr),
content: Text(exception.toString().capitalize!),
content: Text('${exception.toString().capitalize!}\n\nStack Trace: $stackTrace'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),

View File

@@ -85,4 +85,5 @@ class DefaultFirebaseOptions {
storageBucket: 'solian-0x001.appspot.com',
measurementId: 'G-EF9BZMKBC3',
);
}
}

View File

@@ -1,110 +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:sentry_flutter/sentry_flutter.dart';
import 'package:solian/exts.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/account.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/chat.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/providers/content/call.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/post.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/friend.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() async {
await SentryFlutter.init(
(options) {
options.dsn =
'https://55438cdff9048aa2225df72fdc629c42@o4506965897117696.ingest.us.sentry.io/4507357676437504';
options.tracesSampleRate = 1.0;
options.profilesSampleRate = 1.0;
},
appRunner: () async {
WidgetsFlutterBinding.ensureInitialized();
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
await Future.wait([
_initializeFirebase(),
_initializePlatformComponents(),
]);
if (PlatformInfo.isDesktop) {
await Window.initialize();
await Window.setEffect(
effect: WindowEffect.transparent,
dark: true,
);
GoRouter.optionURLReflectsImperativeAPIs = true;
if (PlatformInfo.isMacOS) {
await Window.hideTitle();
await Window.hideCloseButton();
await Window.hideMiniaturizeButton();
await Window.hideZoomButton();
await Window.makeTitlebarTransparent();
await Window.enableFullSizeContentView();
}
}
runApp(const SolianApp());
},
);
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(() => FriendProvider());
Get.lazyPut(() => PostProvider());
Get.lazyPut(() => AttachmentProvider());
Get.lazyPut(() => ChatProvider());
Get.lazyPut(() => AccountProvider());
Get.lazyPut(() => StatusProvider());
Get.lazyPut(() => ChannelProvider());
Get.lazyPut(() => RealmProvider());
Get.lazyPut(() => ChatCallProvider());
return ChangeNotifierProvider.value(
value: themeSwitcher,
child: Builder(builder: (context) {
final theme = Provider.of<ThemeSwitcher>(context);
final AuthProvider auth = Get.find();
auth.isAuthorized.then((value) async {
if (value) {
Get.find<AccountProvider>().connect();
Get.find<ChatProvider>().connect();
try {
Get.find<AccountProvider>().registerPushNotifications();
} catch (err) {
context.showSnackbar('pushNotifyRegisterFailed'
.trParams({'reason': err.toString()}));
}
}
});
},
builder: (context, child) {
return ScaffoldMessenger(
child: child ?? Container(),
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,6 +3,8 @@ class Account {
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
DateTime? confirmedAt;
DateTime? suspendedAt;
String name;
String nick;
dynamic avatar;
@@ -17,6 +19,8 @@ class Account {
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.confirmedAt,
required this.suspendedAt,
required this.name,
required this.nick,
required this.avatar,
@@ -31,7 +35,15 @@ 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'],
@@ -49,7 +61,9 @@ 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,

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,
@@ -41,7 +43,7 @@ class Attachment {
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,
uuid: json['uuid'],
size: json['size'],
name: json['name'],
@@ -50,9 +52,10 @@ class Attachment {
mimetype: json['mimetype'],
hash: json['hash'],
destination: json['destination'],
isAnalyzed: json['is_analyzed'],
metadata: json['metadata'],
isMature: json['is_mature'],
account: Account.fromJson(json['account']),
account: json['account'] != null ? Account.fromJson(json['account']) : null,
accountId: json['account_id'],
);
@@ -60,7 +63,7 @@ class Attachment {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'deleted_at': deletedAt?.toIso8601String(),
'uuid': uuid,
'size': size,
'name': name,
@@ -69,9 +72,10 @@ class Attachment {
'mimetype': mimetype,
'hash': hash,
'destination': destination,
'is_analyzed': isAnalyzed,
'metadata': metadata,
'is_mature': isMature,
'account': account.toJson(),
'account': account?.toJson(),
'account_id': accountId,
};
}

View File

@@ -10,6 +10,7 @@ class Call {
String externalId;
int founderId;
int channelId;
List<dynamic> participants;
Channel channel;
Call({
@@ -21,6 +22,7 @@ class Call {
required this.externalId,
required this.founderId,
required this.channelId,
required this.participants,
required this.channel,
});
@@ -34,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']),
);
@@ -46,6 +49,7 @@ class Call {
'external_id': externalId,
'founder_id': founderId,
'channel_id': channelId,
'participants': participants,
'channel': channel.toJson(),
};
}
@@ -63,6 +67,7 @@ class ParticipantTrack {
{required this.participant,
required this.videoTrack,
required this.isScreenShare});
VideoTrack? videoTrack;
Participant participant;
bool isScreenShare;

View File

@@ -79,8 +79,8 @@ class EventMessageBody {
factory EventMessageBody.fromJson(Map<String, dynamic> json) =>
EventMessageBody(
text: json['text'],
algorithm: json['algorithm'],
text: json['text'] ?? '',
algorithm: json['algorithm'] ?? 'plain',
attachments: json['attachments'] != null
? List<int>.from(json['attachments'].map((x) => x))
: null,

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,24 +3,26 @@ class Notification {
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String subject;
String content;
List<Link>? links;
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,
required this.deletedAt,
required this.subject,
required this.content,
required this.links,
required this.readAt,
required this.title,
required this.subtitle,
required this.body,
required this.avatar,
required this.picture,
required this.senderId,
required this.recipientId,
required this.accountId,
});
factory Notification.fromJson(Map<String, dynamic> json) => Notification(
@@ -32,14 +34,13 @@ class Notification {
? 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(),
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(),
'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,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,11 +49,12 @@ 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(
@@ -60,14 +65,15 @@ class Post {
? DateTime.parse(json['deleted_at'])
: null,
alias: json['alias'],
content: json['content'],
tags: json['tags'],
categories: json['categories'],
reactions: json['reactions'],
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'],
attachments: json['attachments'] != null
? List<int>.from(json['attachments'])
: null,
replyId: json['reply_id'],
repostId: json['repost_id'],
realmId: json['realm_id'],
@@ -76,11 +82,68 @@ class Post {
repostTo:
json['repost_to'] != null ? Post.fromJson(json['repost_to']) : null,
realm: json['realm'] != null ? Realm.fromJson(json['realm']) : null,
publishedAt: json['published_at'] != null ? DateTime.parse(json['published_at']) : 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']),
replyCount: json['reply_count'],
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(
@@ -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?.toJson(),
'published_at': publishedAt?.toIso8601String(),
'author_id': authorId,
'author': author.toJson(),
'reply_count': replyCount,
'reaction_count': reactionCount,
'reply_count': replyCount,
'reaction_list': reactionList,
};
}

View File

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

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,
required this.deletedAt,
required this.accountId,
required this.relatedId,
required 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(),
};
}

View File

@@ -29,6 +29,8 @@ abstract class PlatformInfo {
static bool get canRecord => (isMobile || isMacOS);
static bool get canPushNotification => isAndroid || isIOS || isMacOS;
static Future<String> getVersion() async {
var version = kIsWeb ? 'Web' : 'Unknown';
try {

View File

@@ -27,21 +27,21 @@ class StatusProvider extends GetConnect {
void onInit() {
final AuthProvider auth = Get.find();
httpClient.baseUrl = ServiceFinder.services['passport'];
httpClient.baseUrl = ServiceFinder.buildUrl('auth', null);
httpClient.addAuthenticator(auth.requestAuthenticator);
}
Future<Response> getCurrentStatus() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('passport');
final client = auth.configureClient('auth');
return await client.get('/api/users/me/status');
return await client.get('/users/me/status');
}
Future<Response> getSomeoneStatus(String name) =>
get('/api/users/$name/status');
get('/users/$name/status');
Future<Response> setStatus(
String type,
@@ -53,9 +53,9 @@ class StatusProvider extends GetConnect {
DateTime? clearAt,
}) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('passport');
final client = auth.configureClient('auth');
final payload = {
'type': type,
@@ -68,9 +68,9 @@ class StatusProvider extends GetConnect {
Response resp;
if (!isUpdate) {
resp = await client.post('/api/users/me/status', payload);
resp = await client.post('/users/me/status', payload);
} else {
resp = await client.put('/api/users/me/status', payload);
resp = await client.put('/users/me/status', payload);
}
if (resp.statusCode != 200) {
@@ -82,11 +82,11 @@ class StatusProvider extends GetConnect {
Future<Response> clearStatus() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('passport');
final client = auth.configureClient('auth');
final resp = await client.delete('/api/users/me/status');
final resp = await client.delete('/users/me/status');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
@@ -94,9 +94,10 @@ class StatusProvider extends GetConnect {
return resp;
}
static (Widget, String) determineStatus(AccountStatus status,
static (Widget, Color, String) determineStatus(AccountStatus status,
{double size = 14}) {
Widget icon;
Color color;
String? text;
if (!presetStatuses.keys.contains(status.status?.type)) {
@@ -104,15 +105,18 @@ class StatusProvider extends GetConnect {
}
if (status.isDisturbable && status.isOnline) {
icon = Icon(Icons.circle, color: Colors.green, size: size);
color = Colors.green;
icon = Icon(Icons.circle, color: color, size: size);
text ??= 'accountStatusOnline'.tr;
} else if (!status.isDisturbable && status.isOnline) {
icon = Icon(Icons.do_not_disturb_on, color: Colors.red, size: size);
color = Colors.red;
icon = Icon(Icons.do_not_disturb_on, color: color, size: size);
text ??= 'accountStatusSilent'.tr;
} else {
icon = Icon(Icons.circle, color: Colors.grey, size: size);
color = Colors.grey;
icon = Icon(Icons.circle, color: color, size: size);
text ??= 'accountStatusOffline'.tr;
}
return (icon, text);
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

@@ -7,47 +7,99 @@ 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/account.dart';
import 'package:solian/providers/chat.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 tokenEndpoint =
Uri.parse('${ServiceFinder.services['passport']}/api/auth/token');
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'];
loadCredentials();
httpClient.baseUrl = ServiceFinder.buildUrl('auth', null);
refreshAuthorizeStatus().then((_) {
loadCredentials();
refreshUserProfile();
});
}
oauth2.Credentials? credentials;
Completer<void>? _refreshCompleter;
Future<void> refreshCredentials() async {
final resp = await post('/api/auth/token', {
'refresh_token': credentials!.refreshToken,
'grant_type': 'refresh_token',
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
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 = TokenSet(
accessToken: resp.body['access_token'],
refreshToken: resp.body['refresh_token'],
expiredAt: DateTime.now().add(const Duration(minutes: 3)),
);
storage.write(
key: 'auth_credentials',
value: jsonEncode(credentials!.toJson()),
);
_refreshCompleter!.complete();
log('Refreshed credentials at ${DateTime.now()}');
} catch (e) {
_refreshCompleter!.completeError(e);
rethrow;
} finally {
_refreshCompleter = null;
}
credentials = oauth2.Credentials(
resp.body['access_token'],
refreshToken: resp.body['refresh_token'],
idToken: resp.body['access_token'],
tokenEndpoint: tokenEndpoint,
expiration: DateTime.now().add(const Duration(minutes: 3)),
);
storage.write(
key: 'auth_credentials',
value: jsonEncode(credentials!.toJson()),
);
}
Future<Request<T?>> requestAuthenticator<T>(Request<T?> request) async {
@@ -66,54 +118,64 @@ class AuthProvider extends GetConnect {
final client = GetConnect(
maxAuthRetries: 3,
timeout: timeout,
allowAutoSignedCert: true,
userAgent: 'Solian/1.1',
sendUserAgent: true,
);
client.httpClient.addAuthenticator(requestAuthenticator);
client.httpClient.baseUrl = ServiceFinder.services[service];
client.httpClient.baseUrl = ServiceFinder.buildUrl(service, null);
return client;
}
Future<void> ensureCredentials() async {
if (!await isAuthorized) throw Exception('unauthorized');
if (isAuthorized.isFalse) throw Exception('unauthorized');
if (credentials == null) await loadCredentials();
if (credentials!.isExpired) {
await refreshCredentials();
log('Refreshed credentials at ${DateTime.now()}');
}
}
Future<void> loadCredentials() async {
if (await isAuthorized) {
if (isAuthorized.isTrue) {
final content = await storage.read(key: 'auth_credentials');
credentials = oauth2.Credentials.fromJson(jsonDecode(content!));
credentials = TokenSet.fromJson(jsonDecode(content!));
}
}
Future<oauth2.Credentials> signin(
Future<TokenSet> signin(
BuildContext context,
String username,
String password,
) async {
_cachedUserProfileResponse = null;
userProfile.value = null;
final resp = await oauth2.resourceOwnerPasswordGrant(
tokenEndpoint,
username,
password,
identifier: clientId,
secret: clientSecret,
scopes: ['*'],
basicAuth: false,
);
final client = ServiceFinder.configureClient('auth');
credentials = oauth2.Credentials(
resp.credentials.accessToken,
refreshToken: resp.credentials.refreshToken!,
idToken: resp.credentials.accessToken,
tokenEndpoint: tokenEndpoint,
expiration: DateTime.now().add(const Duration(minutes: 3)),
// 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(
@@ -121,20 +183,19 @@ class AuthProvider extends GetConnect {
value: jsonEncode(credentials!.toJson()),
);
Get.find<AccountProvider>().connect();
Get.find<AccountProvider>().notifyPrefetch();
Get.find<ChatProvider>().connect();
Get.find<WebSocketProvider>().connect();
Get.find<WebSocketProvider>().notifyPrefetch();
return credentials!;
}
void signout() {
_cachedUserProfileResponse = null;
isAuthorized.value = false;
userProfile.value = null;
Get.find<ChatProvider>().disconnect();
Get.find<AccountProvider>().disconnect();
Get.find<AccountProvider>().notifications.clear();
Get.find<AccountProvider>().notificationUnread.value = 0;
Get.find<WebSocketProvider>().disconnect();
Get.find<WebSocketProvider>().notifications.clear();
Get.find<WebSocketProvider>().notificationUnread.value = 0;
final chatHistory = ChatEventController();
chatHistory.initialize().then((_) async {
@@ -146,24 +207,20 @@ class AuthProvider extends GetConnect {
// Data Layer
Response? _cachedUserProfileResponse;
RxBool isAuthorized = false.obs;
Rx<Map<String, dynamic>?> userProfile = Rx(null);
Future<bool> get isAuthorized => storage.containsKey(key: 'auth_credentials');
Future<void> refreshAuthorizeStatus() async {
isAuthorized.value = await storage.containsKey(key: 'auth_credentials');
}
Future<Response> getProfile({noCache = false}) async {
if (!noCache && _cachedUserProfileResponse != null) {
return _cachedUserProfileResponse!;
}
final client = configureClient('passport');
final resp = await client.get('/api/users/me');
Future<void> refreshUserProfile() async {
final client = configureClient('auth');
final resp = await client.get('/users/me');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
} else {
_cachedUserProfileResponse = resp;
}
return resp;
userProfile.value = resp.body;
}
}

View File

@@ -16,6 +16,7 @@ class ChatCallProvider extends GetxController {
RxBool isReady = false.obs;
RxBool isMounted = false.obs;
RxBool isInitialized = false.obs;
String? token;
String? endpoint;
@@ -34,7 +35,7 @@ class ChatCallProvider extends GetxController {
late Room room;
late EventsListener<RoomEvent> listener;
RxList participantTracks = [].obs;
RxList<ParticipantTrack> participantTracks = RxList.empty(growable: true);
Rx<ParticipantTrack?> focusTrack = Rx(null);
Future<void> checkPermissions() async {
@@ -55,12 +56,12 @@ class ChatCallProvider extends GetxController {
Future<(String, String)> getRoomToken() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.post(
'/api/channels/global/${channel.value!.alias}/calls/ongoing/token',
'/channels/global/${channel.value!.alias}/calls/ongoing/token',
{},
);
if (resp.statusCode == 200) {
@@ -151,6 +152,8 @@ class ChatCallProvider extends GetxController {
void onRoomDidUpdate() => sortParticipants();
void setupRoom() {
if(isInitialized.value) return;
sortParticipants();
room.addListener(onRoomDidUpdate);
WidgetsBindingCompatible.instance?.addPostFrameCallback(
@@ -160,6 +163,8 @@ class ChatCallProvider extends GetxController {
if (lkPlatformIsMobile()) {
Hardware.instance.setSpeakerphoneOn(true);
}
isInitialized.value = true;
}
void setupRoomListeners({
@@ -194,7 +199,7 @@ class ChatCallProvider extends GetxController {
}
}
final newTracks = List.empty(growable: true);
final newTracks = List<ParticipantTrack>.empty(growable: true);
final mediaTrackList = mediaTracks.values.toList();
mediaTrackList.sort((a, b) {
@@ -247,6 +252,15 @@ class ChatCallProvider extends GetxController {
}
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 {
@@ -353,6 +367,7 @@ class ChatCallProvider extends GetxController {
void disposeRoom() {
isMounted.value = false;
isInitialized.value = false;
current.value = null;
channel.value = null;
room.removeListener(onRoomDidUpdate);

View File

@@ -1,74 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:get/get.dart';
import 'package:solian/models/packet.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
import 'package:web_socket_channel/io.dart';
class ChatProvider extends GetxController {
RxBool isConnected = false.obs;
RxBool isConnecting = false.obs;
IOWebSocketChannel? websocket;
StreamController<NetworkPackage> stream = StreamController.broadcast();
void connect({noRetry = false}) async {
if (isConnected.value) {
return;
} else {
disconnect();
}
final AuthProvider auth = Get.find();
await auth.ensureCredentials();
final uri = Uri.parse(
'${ServiceFinder.services['messaging']}/api/ws?tk=${auth.credentials!.accessToken}'
.replaceFirst('http', 'ws'),
);
isConnecting.value = true;
try {
websocket = IOWebSocketChannel.connect(uri);
await websocket?.ready;
} catch (e) {
if (!noRetry) {
await auth.refreshCredentials();
return connect(noRetry: true);
}
}
listen();
isConnected.value = true;
isConnecting.value = false;
}
void disconnect() {
websocket?.sink.close(WebSocketStatus.normalClosure);
websocket = null;
isConnected.value = false;
}
void listen() {
websocket?.stream.listen(
(event) {
final packet = NetworkPackage.fromJson(jsonDecode(event));
stream.sink.add(packet);
},
onDone: () {
isConnected.value = false;
Future.delayed(const Duration(seconds: 1), () => connect());
},
onError: (err) {
isConnected.value = false;
Future.delayed(const Duration(seconds: 3), () => connect());
},
);
}
}

View File

@@ -1,67 +1,103 @@
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'};
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);
}
final Map<int, Response> _cachedResponses = {};
final Map<int, Attachment> _cachedResponses = {};
Future<Response> getMetadata(int id, {noCache = false}) async {
Future<List<Attachment?>> listMetadata(
List<int> id, {
noCache = false,
}) async {
if (id.isEmpty) return List.empty();
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]);
}
}
}
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('/api/attachments/$id/meta');
_cachedResponses[id] = resp;
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 resp;
return null;
}
Future<Response> createAttachment(File file, String hash, String usage,
{double? ratio}) async {
Future<Attachment> createAttachment(
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
{Function(double)? onProgress}) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient(
'paperclip',
timeout: const Duration(minutes: 3),
);
await auth.ensureCredentials();
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 fileExt = basename(file.path)
.substring(basename(file.path).lastIndexOf('.') + 1)
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
@@ -69,40 +105,44 @@ class AttachmentProvider extends GetConnect {
if (mimetypeOverrides.keys.contains(fileExt)) {
mimetypeOverride = mimetypeOverrides[fileExt];
}
final payload = FormData({
final payload = dio.FormData.fromMap({
'alt': fileAlt,
'file': filePayload,
'hash': hash,
'usage': usage,
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
'metadata': jsonEncode({
if (ratio != null) 'ratio': ratio,
}),
'metadata': jsonEncode(metadata),
});
final resp = await client.post('/api/attachments', payload);
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.bodyString);
throw Exception(resp.data);
}
return resp;
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 = auth.configureClient('paperclip');
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,
@@ -117,11 +157,11 @@ class AttachmentProvider extends GetConnect {
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 = auth.configureClient('paperclip');
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);
}

View File

@@ -1,17 +1,39 @@
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/friend_select.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 (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.get('/api/channels/$realm/$alias');
final resp = await client.get('/channels/$realm/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
@@ -19,13 +41,14 @@ class ChannelProvider extends GetxController {
return resp;
}
Future<Response> getMyChannelProfile(String alias, {String realm = 'global'}) async {
Future<Response> getMyChannelProfile(String alias,
{String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.get('/api/channels/$realm/$alias/me');
final resp = await client.get('/channels/$realm/$alias/me');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
@@ -36,11 +59,11 @@ class ChannelProvider extends GetxController {
Future<Response?> getChannelOngoingCall(String alias,
{String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.get('/api/channels/$realm/$alias/calls/ongoing');
final resp = await client.get('/channels/$realm/$alias/calls/ongoing');
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
@@ -52,11 +75,11 @@ class ChannelProvider extends GetxController {
Future<Response> listChannel({String scope = 'global'}) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.get('/api/channels/$scope');
final resp = await client.get('/channels/$scope');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
@@ -66,11 +89,11 @@ class ChannelProvider extends GetxController {
Future<Response> listAvailableChannel({String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.get('/api/channels/$realm/me/available');
final resp = await client.get('/channels/$realm/me/available');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
@@ -80,11 +103,11 @@ class ChannelProvider extends GetxController {
Future<Response> createChannel(String scope, dynamic payload) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.post('/api/channels/$scope', payload);
final resp = await client.post('/channels/$scope', payload);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
@@ -95,25 +118,25 @@ class ChannelProvider extends GetxController {
Future<Response?> createDirectChannel(
BuildContext context, String scope) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final related = await showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => FriendSelect(
builder: (context) => RelativeSelector(
title: 'channelOrganizeDirectHint'.tr,
),
);
if (related == null) return null;
final prof = await auth.getProfile();
final prof = auth.userProfile.value!;
final client = auth.configureClient('messaging');
final resp = await client.post('/api/channels/$scope/dm', {
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.body['name']} and @${related.name}',
'A direct message channel between @${prof['name']} and @${related.name}',
'related_user': related.id,
'is_encrypted': false,
});
@@ -126,11 +149,11 @@ class ChannelProvider extends GetxController {
Future<Response> updateChannel(String scope, int id, dynamic payload) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('messaging');
final resp = await client.put('/api/channels/$scope/$id', payload);
final resp = await client.put('/channels/$scope/$id', payload);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}

View File

@@ -1,41 +0,0 @@
import 'package:get/get.dart';
import 'package:solian/services.dart';
class PostProvider extends GetConnect {
@override
void onInit() {
httpClient.baseUrl = ServiceFinder.services['interactive'];
}
Future<Response> listPost(int page, {int? realm}) async {
final queries = [
'take=${10}',
'offset=$page',
if (realm != null) 'realmId=$realm',
];
final resp = await get('/api/feed?${queries.join('&')}');
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
return resp;
}
Future<Response> listPostReplies(String alias, int page) async {
final resp = await get('/api/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('/api/posts/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
return resp;
}
}

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

@@ -1,14 +1,31 @@
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 (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('passport');
final client = auth.configureClient('auth');
final resp = await client.get('/api/realms/$alias');
final resp = await client.get('/realms/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
@@ -18,11 +35,11 @@ class RealmProvider extends GetxController {
Future<Response> listAvailableRealm() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient('passport');
final client = auth.configureClient('auth');
final resp = await client.get('/api/realms/me/available');
final resp = await client.get('/realms/me/available');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}

View File

@@ -1,43 +0,0 @@
import 'package:get/get.dart';
import 'package:solian/models/friendship.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
class FriendProvider extends GetConnect {
@override
void onInit() {
final AuthProvider auth = Get.find();
httpClient.baseUrl = ServiceFinder.services['passport'];
httpClient.addAuthenticator(auth.requestAuthenticator);
}
Future<Response> listFriendship() => get('/api/users/me/friends');
Future<Response> listFriendshipWithStatus(int status) =>
get('/api/users/me/friends?status=$status');
Future<Response> createFriendship(String username) async {
final resp = await post('/api/users/me/friends?related=$username', {});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> updateFriendship(Friendship relationship, int status) async {
final AuthProvider auth = Get.find();
final prof = await auth.getProfile();
final otherside = relationship.getOtherside(prof.body['id']);
final resp = await put('/api/users/me/friends/${otherside.id}', {
'status': status,
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
}

View File

@@ -18,12 +18,12 @@ Future<MessageHistoryDb> createHistoryDb() async {
Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return null;
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/api/channels/$scope/${channel.alias}/events/$id',
'/channels/$scope/${channel.alias}/events/$id',
);
if (resp.statusCode == 404) {
@@ -48,12 +48,12 @@ Future<(List<Event>, int)?> getRemoteEvents(
}
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return null;
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/api/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
'/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
);
if (resp.statusCode != 200) {
@@ -81,7 +81,7 @@ Future<(List<Event>, int)?> getRemoteEvents(
return ([...result, ...expandResult], response.count);
}
extension MessageHistoryHelper on MessageHistoryDb {
extension MessageHistoryAdaptor on MessageHistoryDb {
Future<LocalEvent> receiveEvent(Event remote) async {
final entry = LocalEvent(
remote.id,
@@ -121,7 +121,7 @@ extension MessageHistoryHelper on MessageHistoryDb {
return await receiveEvent(remoteRecord);
}
Future<(List<Event>, int)?> syncEvents(Channel channel,
Future<(List<Event>, int)?> syncRemoteEvents(Channel channel,
{String scope = 'global', depth = 10, offset = 0}) async {
final lastOne = await localEvents.findLastByChannel(channel.id);
@@ -145,7 +145,7 @@ extension MessageHistoryHelper on MessageHistoryDb {
return data;
}
Future<List<LocalEvent>> listMessages(Channel channel) async {
Future<List<LocalEvent>> listEvents(Channel channel) async {
return await localEvents.findAllByChannel(channel.id);
}
}

View File

@@ -0,0 +1,79 @@
import 'package:get/get.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/relations.dart';
import 'package:solian/providers/auth.dart';
class RelationshipProvider extends GetxController {
final RxInt friendRequestCount = 0.obs;
final RxList<Relationship> _friends = RxList.empty(growable: true);
Future<void> refreshRelativeList() async {
final resp = await listRelation();
final List<Relationship> result = resp.body
.map((e) => Relationship.fromJson(e))
.toList()
.cast<Relationship>();
_friends.value = result.where((x) => x.status == 1).toList();
_friends.refresh();
friendRequestCount.value = result.where((x) => x.status == 0).length;
}
bool hasFriend(Account account) {
final auth = Get.find<AuthProvider>();
if (auth.userProfile.value!['id'] == account.id) return true;
return _friends.any((x) => x.relatedId == account.id);
}
Future<Response> listRelation() {
final AuthProvider auth = Get.find();
final client = auth.configureClient('auth');
return client.get('/users/me/relations');
}
Future<Response> listRelationWithStatus(int status) {
final AuthProvider auth = Get.find();
final client = auth.configureClient('auth');
return client.get('/users/me/relations?status=$status');
}
Future<Response> makeFriend(String username) async {
final AuthProvider auth = Get.find();
final client = auth.configureClient('auth');
final resp = await client.post('/users/me/relations?related=$username', {});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> handleRelation(
Relationship relationship, bool doAccept) async {
final AuthProvider auth = Get.find();
final client = auth.configureClient('auth');
final resp = await client.post(
'/users/me/relations/${relationship.relatedId}/${doAccept ? 'accept' : 'decline'}',
{},
);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> editRelation(Relationship relationship, int status) async {
final AuthProvider auth = Get.find();
final client = auth.configureClient('auth');
final resp = await client.patch(
'/users/me/relations/${relationship.relatedId}',
{'status': status},
);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
}

View File

@@ -0,0 +1,34 @@
import 'package:get/get.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/stickers.dart';
import 'package:solian/services.dart';
class StickerProvider extends GetxController {
final RxMap<String, String> aliasImageMapping = RxMap();
final RxList<Sticker> availableStickers = RxList.empty(growable: true);
Future<void> refreshAvailableStickers() async {
availableStickers.clear();
aliasImageMapping.clear();
final client = ServiceFinder.configureClient('files');
final resp = await client.get(
'/stickers/manifest?take=100',
);
if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body);
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
if (out == null) return;
for (final pack in out) {
for (final sticker in (pack.stickers ?? List<Sticker>.empty())) {
sticker.pack = pack;
aliasImageMapping[sticker.textPlaceholder.toUpperCase()] =
sticker.imageUrl;
availableStickers.add(sticker);
}
}
}
availableStickers.refresh();
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/theme.dart';
class ThemeSwitcher extends ChangeNotifier {
ThemeData lightThemeData;
ThemeData darkThemeData;
ThemeSwitcher({
required this.lightThemeData,
required this.darkThemeData,
});
Future<void> restoreTheme() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('global_theme_color')) {
final value = prefs.getInt('global_theme_color')!;
final color = Color(value);
lightThemeData = SolianTheme.build(Brightness.light, seedColor: color);
darkThemeData = SolianTheme.build(Brightness.dark, seedColor: color);
notifyListeners();
}
}
void setTheme(ThemeData light, dark) {
lightThemeData = light;
darkThemeData = dark;
notifyListeners();
}
}

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'dart:math' as math;
import 'package:device_info_plus/device_info_plus.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
@@ -13,13 +12,9 @@ import 'package:solian/models/pagination.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
import 'package:web_socket_channel/io.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class AccountProvider extends GetxController {
final FlutterLocalNotificationsPlugin localNotify =
FlutterLocalNotificationsPlugin();
import 'package:web_socket_channel/web_socket_channel.dart';
class WebSocketProvider extends GetxController {
RxBool isConnected = false.obs;
RxBool isConnecting = false.obs;
@@ -27,7 +22,9 @@ class AccountProvider extends GetxController {
RxList<Notification> notifications =
List<Notification>.empty(growable: true).obs;
IOWebSocketChannel? websocket;
WebSocketChannel? websocket;
StreamController<NetworkPackage> stream = StreamController.broadcast();
@override
onInit() {
@@ -39,14 +36,13 @@ class AccountProvider extends GetxController {
badge: true,
sound: true)
.then((status) {
notifyInitialization();
notifyPrefetch();
});
super.onInit();
}
void connect({noRetry = false}) async {
Future<void> connect({noRetry = false}) async {
if (isConnected.value) {
return;
} else {
@@ -58,15 +54,15 @@ class AccountProvider extends GetxController {
if (auth.credentials == null) await auth.loadCredentials();
final uri = Uri.parse(
'${ServiceFinder.services['passport']}/api/ws?tk=${auth.credentials!.accessToken}'
.replaceFirst('http', 'ws'),
);
final uri = Uri.parse(ServiceFinder.buildUrl(
'dealer',
'/api/ws?tk=${auth.credentials!.accessToken}',
).replaceFirst('http', 'ws'));
isConnecting.value = true;
try {
websocket = IOWebSocketChannel.connect(uri);
websocket = WebSocketChannel.connect(uri);
await websocket?.ready;
} catch (e) {
if (!noRetry) {
@@ -91,14 +87,7 @@ class AccountProvider extends GetxController {
websocket?.stream.listen(
(event) {
final packet = NetworkPackage.fromJson(jsonDecode(event));
switch (packet.method) {
case 'notifications.new':
final notification = Notification.fromJson(packet.payload!);
notificationUnread++;
notifications.add(notification);
notifyMessage(notification.subject, notification.content);
break;
}
stream.sink.add(packet);
},
onDone: () {
isConnected.value = false;
@@ -111,62 +100,13 @@ class AccountProvider extends GetxController {
);
}
void notifyInitialization() {
const androidSettings = AndroidInitializationSettings('app_icon');
const darwinSettings = DarwinInitializationSettings(
notificationCategories: [
DarwinNotificationCategory('general'),
],
);
const linuxSettings =
LinuxInitializationSettings(defaultActionName: 'Open notification');
const InitializationSettings initializationSettings =
InitializationSettings(
android: androidSettings,
iOS: darwinSettings,
macOS: darwinSettings,
linux: linuxSettings,
);
localNotify.initialize(initializationSettings);
}
void notifyMessage(String title, String body) {
const androidSettings = AndroidNotificationDetails(
'general',
'General',
importance: Importance.high,
priority: Priority.high,
silent: true,
);
const darwinSettings = DarwinNotificationDetails(
presentAlert: true,
presentBanner: true,
presentBadge: true,
presentSound: false,
);
const linuxSettings = LinuxNotificationDetails();
localNotify.show(
math.max(1, math.Random().nextInt(100000000)),
title,
body,
const NotificationDetails(
android: androidSettings,
iOS: darwinSettings,
macOS: darwinSettings,
linux: linuxSettings,
),
);
}
Future<void> notifyPrefetch() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
if (auth.isAuthorized.isFalse) return;
final client = auth.configureClient('passport');
final client = auth.configureClient('auth');
final resp = await client.get('/api/notifications?skip=0&take=100');
final resp = await client.get('/notifications?skip=0&take=100');
if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body);
final data = result.data?.map((x) => Notification.fromJson(x)).toList();
@@ -179,7 +119,7 @@ class AccountProvider extends GetxController {
Future<void> registerPushNotifications() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) return;
late final String? token;
late final String provider;
@@ -187,6 +127,8 @@ class AccountProvider extends GetxController {
if (deviceUuid == null || deviceUuid.isEmpty) {
log("Unable to active push notifications, couldn't get device uuid");
} else {
log('Device UUID is $deviceUuid');
}
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
@@ -196,10 +138,11 @@ class AccountProvider extends GetxController {
provider = 'firebase';
token = await FirebaseMessaging.instance.getToken();
}
log('Device Push Token is $token');
final client = auth.configureClient('passport');
final client = auth.configureClient('auth');
final resp = await client.post('/api/notifications/subscribe', {
final resp = await client.post('/notifications/subscribe', {
'provider': provider,
'device_token': token,
'device_id': deviceUuid,
@@ -212,27 +155,31 @@ class AccountProvider extends GetxController {
Future<String?> _getDeviceUuid() async {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (PlatformInfo.isWeb) {
final WebBrowserInfo webInfo = await deviceInfo.webBrowserInfo;
final webInfo = await deviceInfo.webBrowserInfo;
return webInfo.vendor! +
webInfo.userAgent! +
webInfo.hardwareConcurrency.toString();
}
if (PlatformInfo.isAndroid) {
final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
final androidInfo = await deviceInfo.androidInfo;
return androidInfo.id;
}
if (PlatformInfo.isIOS) {
final IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
final iosInfo = await deviceInfo.iosInfo;
return iosInfo.identifierForVendor!;
}
if (PlatformInfo.isLinux) {
final LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo;
final linuxInfo = await deviceInfo.linuxInfo;
return linuxInfo.machineId!;
}
if (PlatformInfo.isWindows) {
final WindowsDeviceInfo windowsInfo = await deviceInfo.windowsInfo;
final windowsInfo = await deviceInfo.windowsInfo;
return windowsInfo.deviceId;
}
if (PlatformInfo.isMacOS) {
final macosInfo = await deviceInfo.macOsInfo;
return macosInfo.systemGUID;
}
return null;
}
}

View File

@@ -1,228 +1,254 @@
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/screens/about.dart';
import 'package:solian/screens/account.dart';
import 'package:solian/screens/account/friend.dart';
import 'package:solian/screens/account/personalize.dart';
import 'package:solian/screens/account/profile_page.dart';
import 'package:solian/screens/account/stickers.dart';
import 'package:solian/screens/channel/channel_chat.dart';
import 'package:solian/screens/channel/channel_detail.dart';
import 'package:solian/screens/channel/channel_organize.dart';
import 'package:solian/screens/chat.dart';
import 'package:solian/screens/feed/search.dart';
import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/screens/feed/draft_box.dart';
import 'package:solian/screens/realms.dart';
import 'package:solian/screens/realms/realm_detail.dart';
import 'package:solian/screens/realms/realm_organize.dart';
import 'package:solian/screens/realms/realm_view.dart';
import 'package:solian/screens/social.dart';
import 'package:solian/screens/posts/post_publish.dart';
import 'package:solian/shells/basic_shell.dart';
import 'package:solian/shells/nav_shell.dart';
import 'package:solian/screens/home.dart';
import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/screens/settings.dart';
import 'package:solian/shells/root_shell.dart';
import 'package:solian/shells/title_shell.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/sidebar/empty_placeholder.dart';
abstract class AppRouter {
static GoRouter instance = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) => NavShell(
builder: (context, state, child) => RootShell(
state: state,
showAppBar: false,
showSidebar: false,
child: child,
),
routes: [
ShellRoute(
builder: (context, state, child) => BasicShell(
_feedRoute,
_chatRoute,
_realmRoute,
_accountRoute,
GoRoute(
path: '/about',
name: 'about',
builder: (context, state) => TitleShell(
state: state,
sidebarFirst: true,
showAppBar: false,
sidebar: const SocialScreen(),
child: child,
child: const AboutScreen(),
),
routes: [
GoRoute(
path: '/',
name: 'social',
builder: (context, state) =>
SolianTheme.isExtraLargeScreen(context)
? const EmptyPagePlaceholder()
: const SocialScreen(),
),
GoRoute(
path: '/posts/view/:alias',
name: 'postDetail',
builder: (context, state) => TitleShell(
state: state,
child: PostDetailScreen(
alias: state.pathParameters['alias']!,
),
),
),
GoRoute(
path: '/posts/publish',
name: 'postPublishing',
builder: (context, state) {
final arguments = state.extra as PostPublishingArguments?;
return PostPublishingScreen(
edit: arguments?.edit,
reply: arguments?.reply,
repost: arguments?.repost,
realm: arguments?.realm,
);
},
),
],
),
ShellRoute(
builder: (context, state, child) => BasicShell(
GoRoute(
path: '/settings',
name: 'settings',
builder: (context, state) => TitleShell(
state: state,
sidebarFirst: true,
showAppBar: false,
sidebar: const ChatScreen(),
child: child,
child: const SettingScreen(),
),
routes: [
GoRoute(
path: '/chat',
name: 'chat',
builder: (context, state) =>
SolianTheme.isExtraLargeScreen(context)
? const EmptyPagePlaceholder()
: const ChatScreen(),
),
GoRoute(
path: '/chat/organize',
name: 'channelOrganizing',
builder: (context, state) {
final arguments = state.extra as ChannelOrganizeArguments?;
return ChannelOrganizeScreen(
edit: arguments?.edit,
realm: arguments?.realm,
);
},
),
GoRoute(
path: '/chat/:alias',
name: 'channelChat',
builder: (context, state) {
return ChannelChatScreen(
alias: state.pathParameters['alias']!,
realm: state.uri.queryParameters['realm'] ?? 'global',
);
},
),
GoRoute(
path: '/chat/:alias/detail',
name: 'channelDetail',
builder: (context, state) {
final arguments = state.extra as ChannelDetailArguments;
return TitleShell(
state: state,
child: ChannelDetailScreen(
channel: arguments.channel,
profile: arguments.profile,
realm: state.uri.queryParameters['realm'] ?? 'global',
),
);
},
),
],
),
ShellRoute(
builder: (context, state, child) => BasicShell(
state: state,
sidebarFirst: true,
showAppBar: false,
sidebar: const RealmListScreen(),
child: child,
),
routes: [
GoRoute(
path: '/realms',
name: 'realms',
builder: (context, state) =>
SolianTheme.isExtraLargeScreen(context)
? const EmptyPagePlaceholder()
: const RealmListScreen(),
),
GoRoute(
path: '/realms/:alias/detail',
name: 'realmDetail',
builder: (context, state) => TitleShell(
state: state,
child: RealmDetailScreen(
realm: state.extra as Realm,
alias: state.pathParameters['alias']!,
),
),
),
GoRoute(
path: '/realm/organize',
name: 'realmOrganizing',
builder: (context, state) {
final arguments = state.extra as RealmOrganizeArguments?;
return RealmOrganizeScreen(
edit: arguments?.edit,
);
},
),
GoRoute(
path: '/realm/:alias',
name: 'realmView',
builder: (context, state) {
return RealmViewScreen(
alias: state.pathParameters['alias']!,
);
},
),
],
),
ShellRoute(
builder: (context, state, child) => BasicShell(
state: state,
sidebarFirst: true,
showAppBar: false,
sidebar: const AccountScreen(),
child: child,
),
routes: [
GoRoute(
path: '/account',
name: 'account',
builder: (context, state) =>
SolianTheme.isExtraLargeScreen(context)
? const EmptyPagePlaceholder()
: const AccountScreen(),
),
GoRoute(
path: '/account/friend',
name: 'accountFriend',
builder: (context, state) => TitleShell(
state: state,
child: const FriendScreen(),
),
),
GoRoute(
path: '/account/personalize',
name: 'accountPersonalize',
builder: (context, state) => TitleShell(
state: state,
child: const PersonalizeScreen(),
),
),
GoRoute(
path: '/about',
name: 'about',
builder: (context, state) => TitleShell(
state: state,
child: const AboutScreen(),
),
),
],
),
],
),
],
);
static final ShellRoute _feedRoute = ShellRoute(
builder: (context, state, child) => child,
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/feed/search',
name: 'feedSearch',
builder: (context, state) => TitleShell(
state: state,
child: FeedSearchScreen(
tag: state.uri.queryParameters['tag'],
category: state.uri.queryParameters['category'],
),
),
),
GoRoute(
path: '/drafts',
name: 'draftBox',
builder: (context, state) => const DraftBoxScreen(),
),
GoRoute(
path: '/posts/view/:id',
name: 'postDetail',
builder: (context, state) => TitleShell(
state: state,
child: PostDetailScreen(
id: state.pathParameters['id']!,
),
),
),
GoRoute(
path: '/posts/editor',
name: 'postEditor',
pageBuilder: (context, state) {
final arguments = state.extra as PostPublishArguments?;
return CustomTransitionPage(
child: PostPublishScreen(
edit: arguments?.edit,
reply: arguments?.reply,
repost: arguments?.repost,
realm: arguments?.realm,
mode: int.tryParse(state.uri.queryParameters['mode'] ?? '0') ?? 0,
),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
fillColor: Theme.of(context).scaffoldBackgroundColor,
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
);
},
),
],
);
static final ShellRoute _chatRoute = ShellRoute(
builder: (context, state, child) => child,
routes: [
GoRoute(
path: '/chat',
name: 'chat',
builder: (context, state) => const ChatScreen(),
),
GoRoute(
path: '/chat/organize',
name: 'channelOrganizing',
builder: (context, state) {
final arguments = state.extra as ChannelOrganizeArguments?;
return ChannelOrganizeScreen(
edit: arguments?.edit,
realm: arguments?.realm,
);
},
),
GoRoute(
path: '/chat/:alias',
name: 'channelChat',
builder: (context, state) {
return ChannelChatScreen(
alias: state.pathParameters['alias']!,
realm: state.uri.queryParameters['realm'] ?? 'global',
);
},
),
GoRoute(
path: '/chat/:alias/detail',
name: 'channelDetail',
builder: (context, state) {
final arguments = state.extra as ChannelDetailArguments;
return TitleShell(
state: state,
child: ChannelDetailScreen(
channel: arguments.channel,
profile: arguments.profile,
realm: state.uri.queryParameters['realm'] ?? 'global',
),
);
},
),
],
);
static final ShellRoute _realmRoute = ShellRoute(
builder: (context, state, child) => child,
routes: [
GoRoute(
path: '/realms',
name: 'realms',
builder: (context, state) => const RealmListScreen(),
),
GoRoute(
path: '/realms/:alias/detail',
name: 'realmDetail',
builder: (context, state) => TitleShell(
state: state,
child: RealmDetailScreen(
realm: state.extra as Realm,
alias: state.pathParameters['alias']!,
),
),
),
GoRoute(
path: '/realm/organize',
name: 'realmOrganizing',
builder: (context, state) {
final arguments = state.extra as RealmOrganizeArguments?;
return RealmOrganizeScreen(
edit: arguments?.edit,
);
},
),
GoRoute(
path: '/realm/:alias',
name: 'realmView',
builder: (context, state) {
return RealmViewScreen(
alias: state.pathParameters['alias']!,
);
},
),
],
);
static final ShellRoute _accountRoute = ShellRoute(
builder: (context, state, child) => child,
routes: [
GoRoute(
path: '/account',
name: 'account',
builder: (context, state) => TitleShell(
state: state,
isCenteredTitle: true,
child: const AccountScreen(),
),
),
GoRoute(
path: '/account/friend',
name: 'accountFriend',
builder: (context, state) => const FriendScreen(),
),
GoRoute(
path: '/account/stickers',
name: 'accountStickers',
builder: (context, state) => TitleShell(
state: state,
child: const StickerScreen(),
),
),
GoRoute(
path: '/account/personalize',
name: 'accountPersonalize',
builder: (context, state) => TitleShell(
state: state,
child: const PersonalizeScreen(),
),
),
GoRoute(
path: '/account/view/:name',
name: 'accountProfilePage',
builder: (context, state) => AccountProfilePage(
name: state.pathParameters['name']!,
),
),
],
);
}

View File

@@ -17,7 +17,11 @@ class AboutScreen extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/logo.png', width: 64, height: 64),
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Image.asset('assets/logo.png', width: 120, height: 120),
),
const SizedBox(height: 8),
Text(
'Solian',
style: Theme.of(context).textTheme.headlineMedium,
@@ -44,17 +48,28 @@ class AboutScreen extends StatelessWidget {
const SizedBox(height: 16),
TextButton(
style: denseButtonStyle,
child: const Text('More Information'),
onPressed: () {
launchUrlString('https://solsynth.dev/products/solar-network');
child: const Text('App Details'),
onPressed: () async {
final info = await PackageInfo.fromPlatform();
showAboutDialog(
context: context,
applicationVersion: '${info.version} (${info.buildNumber})',
applicationLegalese:
'The Solar Network App is an intuitive and self-hostable social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Image.asset('assets/logo.png', width: 60, height: 60),
),
);
},
),
TextButton(
style: denseButtonStyle,
child: const Text('Project Website'),
onPressed: () {
launchUrlString('https://solsynth.dev');
launchUrlString('https://solsynth.dev/products/solar-network');
},
child: const Text('Official Website'),
),
const SizedBox(height: 16),
const Text(

View File

@@ -3,10 +3,13 @@ import 'package:get/get.dart';
import 'package:solian/models/account.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/auth/signin.dart';
import 'package:solian/screens/auth/signup.dart';
import 'package:solian/widgets/account/account_heading.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:badges/badges.dart' as badges;
class AccountScreen extends StatefulWidget {
const AccountScreen({super.key});
@@ -22,66 +25,95 @@ class _AccountScreenState extends State<AccountScreen> {
(
const Icon(Icons.color_lens),
'accountPersonalize'.tr,
'accountPersonalize'
'accountPersonalize',
),
(
Obx(() {
final RelationshipProvider relations = Get.find();
return badges.Badge(
badgeContent: Text(
relations.friendRequestCount.value.toString(),
style: const TextStyle(color: Colors.white),
),
showBadge: relations.friendRequestCount.value > 0,
position: badges.BadgePosition.topEnd(
top: -12,
end: -8,
),
child: const Icon(Icons.diversity_1),
);
}),
'accountFriend'.tr,
'accountFriend',
),
(
const Icon(Icons.emoji_symbols),
'accountStickers'.tr,
'accountStickers',
),
(const Icon(Icons.diversity_1), 'accountFriend'.tr, 'accountFriend'),
(const Icon(Icons.info_outline), 'about'.tr, 'about'),
];
final AuthProvider provider = Get.find();
final AuthProvider auth = Get.find();
return Material(
color: Theme.of(context).colorScheme.surface,
child: SafeArea(
child: FutureBuilder(
future: provider.isAuthorized,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == false) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ActionCard(
icon: const Icon(Icons.login, color: Colors.white),
title: 'signin'.tr,
caption: 'signinCaption'.tr,
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isDismissible: false,
isScrollControlled: true,
context: context,
builder: (context) => const SignInPopup(),
).then((_) async {
await provider.getProfile(noCache: true);
setState(() {});
});
},
child: Obx(() {
if (auth.isAuthorized.isFalse) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ActionCard(
icon: Icon(
Icons.login,
color: Theme.of(context).colorScheme.onPrimary,
),
ActionCard(
icon: const Icon(Icons.add, color: Colors.white),
title: 'signup'.tr,
caption: 'signupCaption'.tr,
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isDismissible: false,
isScrollControlled: true,
context: context,
builder: (context) => const SignUpPopup(),
).then((_) {
setState(() {});
});
},
title: 'signin'.tr,
caption: 'signinCaption'.tr,
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isDismissible: false,
isScrollControlled: true,
context: context,
builder: (context) => const SignInPopup(),
).then((val) async {
if (val == true) {
await auth.refreshUserProfile();
}
});
},
),
ActionCard(
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.onPrimary,
),
],
),
);
}
title: 'signup'.tr,
caption: 'signupCaption'.tr,
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isDismissible: false,
isScrollControlled: true,
context: context,
builder: (context) => const SignUpPopup(),
).then((_) {
setState(() {});
});
},
),
],
),
);
}
return ListView(
return CenteredContainer(
child: ListView(
children: [
const AccountHeading().paddingOnly(bottom: 8, top: 16),
if (auth.userProfile.value != null)
const AccountHeading().paddingOnly(bottom: 8, top: 8),
...(actionItems.map(
(x) => ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
@@ -99,14 +131,14 @@ class _AccountScreenState extends State<AccountScreen> {
leading: const Icon(Icons.logout),
title: Text('signout'.tr),
onTap: () {
provider.signout();
auth.signout();
setState(() {});
},
),
],
);
},
),
),
);
}),
),
);
}
@@ -130,33 +162,25 @@ class _AccountHeadingState extends State<AccountHeading> {
@override
Widget build(BuildContext context) {
final AuthProvider provider = Get.find();
final AuthProvider auth = Get.find();
return FutureBuilder(
future: provider.getProfile(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Container();
}
final prof = auth.userProfile.value!;
final prof = snapshot.data!;
return AccountHeadingWidget(
avatar: prof.body['avatar'],
banner: prof.body['banner'],
name: prof.body['name'],
nick: prof.body['nick'],
desc: prof.body['description'],
status: _status,
badges: prof.body['badges']
?.map((e) => AccountBadge.fromJson(e))
.toList()
.cast<AccountBadge>(),
onEditStatus: () {
setState(() {
_status = Get.find<StatusProvider>().getCurrentStatus();
});
},
);
return AccountHeadingWidget(
avatar: prof['avatar'],
banner: prof['banner'],
name: prof['name'],
nick: prof['nick'],
desc: prof['description'],
status: _status,
badges: prof['badges']
?.map((e) => AccountBadge.fromJson(e))
.toList()
.cast<AccountBadge>(),
onEditStatus: () {
setState(() {
_status = Get.find<StatusProvider>().getCurrentStatus();
});
},
);
}
@@ -190,7 +214,7 @@ class ActionCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
backgroundColor: Colors.indigo,
backgroundColor: Theme.of(context).colorScheme.primary,
child: icon,
).paddingOnly(bottom: 12),
Text(

View File

@@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/friendship.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/friend.dart';
import 'package:solian/widgets/account/friend_list.dart';
import 'package:solian/models/relations.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/account/relative_list.dart';
class FriendScreen extends StatefulWidget {
const FriendScreen({super.key});
@@ -14,71 +13,41 @@ class FriendScreen extends StatefulWidget {
State<FriendScreen> createState() => _FriendScreenState();
}
class _FriendScreenState extends State<FriendScreen> {
class _FriendScreenState extends State<FriendScreen>
with SingleTickerProviderStateMixin {
late final TabController _tabController;
bool _isBusy = false;
int? _accountId;
List<Friendship> _friendships = List.empty();
List<Relationship> _relations = List.empty();
List<Friendship> filterWithStatus(int status) {
return _friendships.where((x) => x.status == status).toList();
List<Relationship> _filterByStatus(int status) {
return _relations.where((x) => x.status == status).toList();
}
Future<void> getFriendship() async {
Future<void> _loadRelations() async {
setState(() => _isBusy = true);
final FriendProvider provider = Get.find();
final resp = await provider.listFriendship();
final RelationshipProvider relations = Get.find();
final resp = await relations.listRelation();
setState(() {
_friendships = resp.body
.map((e) => Friendship.fromJson(e))
_relations = resp.body
.map((e) => Relationship.fromJson(e))
.toList()
.cast<Friendship>();
.cast<Relationship>();
_isBusy = false;
});
relations.friendRequestCount.value =
_relations.where((x) => x.status == 0).length;
}
void showScopedListPopup(String title, int status) {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.85,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded(
child: CustomScrollView(
slivers: [
SliverFriendList(
accountId: _accountId!,
items: filterWithStatus(status),
onUpdate: () {
getFriendship();
},
),
],
),
),
],
),
);
},
);
}
void promptAddFriend() async {
final FriendProvider provider = Get.find();
void _promptAddFriend() async {
final RelationshipProvider provider = Get.find();
final controller = TextEditingController();
final input = await showDialog(
final input = await showDialog<String?>(
context: context,
builder: (context) {
return AlertDialog(
@@ -125,7 +94,7 @@ class _FriendScreenState extends State<FriendScreen> {
try {
setState(() => _isBusy = true);
await provider.createFriendship(input);
await provider.makeFriend(input);
} catch (e) {
context.showErrorDialog(e);
} finally {
@@ -135,12 +104,14 @@ class _FriendScreenState extends State<FriendScreen> {
@override
void initState() {
Get.find<AuthProvider>().getProfile().then((value) {
_accountId = value.body['id'];
});
super.initState();
_tabController = TabController(length: 3, vsync: this);
Future.delayed(Duration.zero, () => getFriendship());
_loadRelations().then((_) {
if (_filterByStatus(0).isEmpty) {
_tabController.animateTo(1);
}
});
}
@override
@@ -148,64 +119,73 @@ class _FriendScreenState extends State<FriendScreen> {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => promptAddFriend(),
),
body: RefreshIndicator(
onRefresh: () => getFriendship(),
child: CustomScrollView(
slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
SliverToBoxAdapter(
child: ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: const Icon(Icons.person_add),
trailing: const Icon(Icons.chevron_right),
title: Text(
'${'accountFriendPending'.tr} (${filterWithStatus(0).length})',
),
onTap: () =>
showScopedListPopup('accountFriendPending'.tr, 0),
),
),
SliverToBoxAdapter(
child: ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: const Icon(Icons.block),
trailing: const Icon(Icons.chevron_right),
title: Text(
'${'accountFriendBlocked'.tr} (${filterWithStatus(2).length})',
),
onTap: () =>
showScopedListPopup('accountFriendBlocked'.tr, 2),
),
),
SliverFriendList(
accountId: _accountId!,
items: filterWithStatus(1),
onUpdate: () {
getFriendship();
},
),
const SliverToBoxAdapter(
child: Divider(thickness: 0.3, height: 0.3),
),
SliverToBoxAdapter(
child: Text(
'accountFriendListHint'.tr,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
).paddingOnly(top: 16, bottom: 32),
appBar: AppBar(
centerTitle: false,
title: Text('accountFriend'.tr),
actions: [
if (_isBusy)
SizedBox(
height: 48,
width: 48,
child: const CircularProgressIndicator(
strokeWidth: 3,
).paddingAll(14),
),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
),
],
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(icon: Icon(Icons.call_received)),
Tab(icon: Icon(Icons.people)),
Tab(icon: Icon(Icons.call_made)),
],
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => _promptAddFriend(),
),
body: TabBarView(
controller: _tabController,
children: [
RefreshIndicator(
onRefresh: () => _loadRelations(),
child: CustomScrollView(
slivers: [
SilverRelativeList(
items: _filterByStatus(0),
onUpdate: () => _loadRelations(),
),
],
),
),
RefreshIndicator(
onRefresh: () => _loadRelations(),
child: CustomScrollView(
slivers: [
SilverRelativeList(
items: _filterByStatus(1),
onUpdate: () => _loadRelations(),
),
],
),
),
RefreshIndicator(
onRefresh: () => _loadRelations(),
child: CustomScrollView(
slivers: [
SilverRelativeList(
items: _filterByStatus(3),
onUpdate: () => _loadRelations(),
),
],
),
),
],
),
),
);
}

View File

@@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/providers/account.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/models/notification.dart' as notify;
import 'package:url_launcher/url_launcher_string.dart';
import 'package:uuid/uuid.dart';
class NotificationScreen extends StatefulWidget {
@@ -19,11 +18,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
Future<void> markAllRead() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
if (auth.isAuthorized.isFalse) return;
setState(() => _isBusy = true);
final AccountProvider provider = Get.find();
final WebSocketProvider provider = Get.find();
List<int> markList = List.empty(growable: true);
for (final element in provider.notifications) {
@@ -32,8 +31,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
}
if (markList.isNotEmpty) {
final client = auth.configureClient('passport');
await client.put('/api/notifications/batch/read', {'messages': markList});
final client = auth.configureClient('auth');
await client.put('/notifications/read', {'messages': markList});
}
provider.notifications.clear();
@@ -43,9 +42,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
Future<void> markOneRead(notify.Notification element, int index) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
if (auth.isAuthorized.isFalse) return;
final AccountProvider provider = Get.find();
final WebSocketProvider provider = Get.find();
if (element.id <= 0) {
provider.notifications.removeAt(index);
@@ -54,9 +53,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
setState(() => _isBusy = true);
final client = auth.configureClient('passport');
final client = auth.configureClient('auth');
await client.put('/api/notifications/${element.id}/read', {});
await client.put('/notifications/read/${element.id}', {});
provider.notifications.removeAt(index);
@@ -65,7 +64,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
@override
Widget build(BuildContext context) {
final AccountProvider provider = Get.find();
final WebSocketProvider provider = Get.find();
return SizedBox(
height: MediaQuery.of(context).size.height * 0.85,
@@ -99,14 +98,14 @@ class _NotificationScreenState extends State<NotificationScreen> {
),
if (provider.notifications.isNotEmpty)
SliverToBoxAdapter(
child: ListTile(
tileColor:
Theme.of(context).colorScheme.secondaryContainer,
leading: const Icon(Icons.checklist),
title: Text('notifyAllRead'.tr),
contentPadding:
const EdgeInsets.symmetric(horizontal: 28),
onTap: _isBusy ? null : () => markAllRead(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
color: Theme.of(context).colorScheme.secondaryContainer,
child: ListTile(
leading: const Icon(Icons.checklist),
title: Text('notifyAllRead'.tr),
onTap: _isBusy ? null : () => markAllRead(),
),
),
),
SliverList.separated(
@@ -126,31 +125,13 @@ class _NotificationScreenState extends State<NotificationScreen> {
horizontal: 24,
vertical: 8,
),
title: Text(element.subject),
title: Text(element.title),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(element.content),
if (element.links != null)
Row(
children: element.links!
.map((e) => InkWell(
child: Text(
e.label,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
decoration:
TextDecoration.underline,
),
),
onTap: () {
launchUrlString(e.url);
},
).paddingOnly(right: 5))
.toList(),
),
if (element.subtitle != null)
Text(element.subtitle!),
Text(element.body),
],
),
),
@@ -175,7 +156,7 @@ class NotificationButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final AccountProvider provider = Get.find();
final WebSocketProvider provider = Get.find();
final button = IconButton(
icon: const Icon(Icons.notifications),

View File

@@ -3,9 +3,11 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/services.dart';
@@ -34,7 +36,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
bool _isBusy = false;
void selectBirthday() async {
void _selectBirthday() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _birthday?.toLocal(),
@@ -49,49 +51,73 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
}
}
void syncWidget() async {
setState(() => _isBusy = true);
void _syncWidget() async {
_isBusy = true;
final AuthProvider auth = Get.find();
final prof = await auth.getProfile(noCache: true);
setState(() {
_usernameController.text = prof.body['name'];
_nicknameController.text = prof.body['nick'];
_descriptionController.text = prof.body['description'];
_firstNameController.text = prof.body['profile']['first_name'];
_lastNameController.text = prof.body['profile']['last_name'];
_avatar = prof.body['avatar'];
_banner = prof.body['banner'];
if (prof.body['profile']['birthday'] != null) {
_birthday = DateTime.parse(prof.body['profile']['birthday']);
_birthdayController.text =
DateFormat('yyyy-MM-dd').format(_birthday!.toLocal());
}
final prof = auth.userProfile.value!;
_usernameController.text = prof['name'];
_nicknameController.text = prof['nick'];
_descriptionController.text = prof['description'];
_firstNameController.text = prof['profile']['first_name'];
_lastNameController.text = prof['profile']['last_name'];
_avatar = prof['avatar'];
_banner = prof['banner'];
if (prof['profile']['birthday'] != null) {
_birthday = DateTime.parse(prof['profile']['birthday']);
_birthdayController.text =
DateFormat('yyyy-MM-dd').format(_birthday!.toLocal());
}
_isBusy = false;
});
_isBusy = false;
}
Future<void> updateImage(String position) async {
Future<void> _editImage(String position) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
if (auth.isAuthorized.isFalse) return;
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return;
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: image.path,
uiSettings: [
AndroidUiSettings(
toolbarTitle: 'cropImage'.tr,
toolbarColor: Theme.of(context).colorScheme.primary,
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
),
IOSUiSettings(
title: 'cropImage'.tr,
aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
),
WebUiSettings(
context: context,
),
],
);
if (croppedFile == null) return;
final file = File(croppedFile.path);
setState(() => _isBusy = true);
final AttachmentProvider provider = Get.find();
Response? attachResp;
Attachment? attachResult;
try {
final file = File(image.path);
final hash = await calculateFileSha256(file);
attachResp = await provider.createAttachment(
file,
hash,
attachResult = await provider.createAttachment(
await file.readAsBytes(),
file.path,
'p.$position',
ratio: await calculateFileAspectRatio(file),
null,
);
} catch (e) {
setState(() => _isBusy = false);
@@ -99,14 +125,14 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
return;
}
final client = auth.configureClient('passport');
final client = auth.configureClient('auth');
final resp = await client.put(
'/api/users/me/$position',
{'attachment': attachResp.body['id']},
'/users/me/$position',
{'attachment': attachResult.id},
);
if (resp.statusCode == 200) {
syncWidget();
_syncWidget();
context.showSnackbar('accountPersonalizeApplied'.tr);
} else {
context.showErrorDialog(resp.bodyString);
@@ -115,17 +141,17 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
setState(() => _isBusy = false);
}
void updatePersonalize() async {
void _editUserInfo() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
if (auth.isAuthorized.isFalse) return;
setState(() => _isBusy = true);
final client = auth.configureClient('passport');
final client = auth.configureClient('auth');
_birthday?.toIso8601String();
final resp = await client.put(
'/api/users/me',
'/users/me',
{
'nick': _nicknameController.value.text,
'description': _descriptionController.value.text,
@@ -135,7 +161,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
},
);
if (resp.statusCode == 200) {
syncWidget();
_syncWidget();
context.showSnackbar('accountPersonalizeApplied'.tr);
} else {
context.showErrorDialog(resp.bodyString);
@@ -147,8 +173,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () => syncWidget());
_syncWidget();
}
@override
@@ -169,7 +194,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
left: 40,
child: FloatingActionButton.small(
heroTag: const Key('avatar-editor'),
onPressed: () => updateImage('avatar'),
onPressed: () => _editImage('avatar'),
child: const Icon(
Icons.camera,
),
@@ -188,7 +213,8 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: _banner != null
? Image.network(
'${ServiceFinder.services['paperclip']}/api/attachments/$_banner',
ServiceFinder.buildUrl(
'files', '/attachments/$_banner'),
fit: BoxFit.cover,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
@@ -213,7 +239,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
right: 16,
child: FloatingActionButton(
heroTag: const Key('banner-editor'),
onPressed: () => updateImage('banner'),
onPressed: () => _editImage('banner'),
child: const Icon(
Icons.camera_alt,
),
@@ -294,18 +320,18 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
border: const OutlineInputBorder(),
labelText: 'birthday'.tr,
),
onTap: () => selectBirthday(),
onTap: () => _selectBirthday(),
).paddingSymmetric(horizontal: padding),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _isBusy ? null : () => syncWidget(),
onPressed: _isBusy ? null : () => _syncWidget(),
child: Text('reset'.tr),
),
ElevatedButton(
onPressed: _isBusy ? null : () => updatePersonalize(),
onPressed: _isBusy ? null : () => _editUserInfo(),
child: Text('apply'.tr),
),
],
@@ -314,4 +340,23 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
),
);
}
@override
void dispose() {
_usernameController.dispose();
_nicknameController.dispose();
_firstNameController.dispose();
_lastNameController.dispose();
_descriptionController.dispose();
_birthdayController.dispose();
super.dispose();
}
}
class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData {
@override
(int, int)? get data => (16, 7);
@override
String get name => '16x7';
}

View File

@@ -0,0 +1,355 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/services.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/posts/post_list.dart';
import 'package:solian/widgets/posts/post_warped_list.dart';
import 'package:solian/widgets/sized_container.dart';
class AccountProfilePage extends StatefulWidget {
final String name;
const AccountProfilePage({super.key, required this.name});
@override
State<AccountProfilePage> createState() => _AccountProfilePageState();
}
class _AccountProfilePageState extends State<AccountProfilePage> {
late final RelationshipProvider _relationshipProvider;
late final PostListController _postController;
final PagingController<int, Attachment> _albumPagingController =
PagingController(firstPageKey: 0);
bool _isBusy = true;
bool _isMakingFriend = false;
bool _showMature = false;
Account? _userinfo;
List<Post> _pinnedPosts = List.empty();
int _totalUpvote = 0, _totalDownvote = 0;
Future<void> _getUserinfo() async {
setState(() => _isBusy = true);
var client = ServiceFinder.configureClient('auth');
var resp = await client.get('/users/${widget.name}');
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) {
Navigator.pop(context);
});
} else {
_userinfo = Account.fromJson(resp.body);
}
client = ServiceFinder.configureClient('interactive');
resp = await client.get('/users/${widget.name}');
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) {
Navigator.pop(context);
});
} else {
_totalUpvote = resp.body['total_upvote'];
_totalDownvote = resp.body['total_downvote'];
}
setState(() => _isBusy = false);
}
Future<void> getPinnedPosts() async {
final client = ServiceFinder.configureClient('interactive');
final resp = await client.get('/users/${widget.name}/pin');
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) {
Navigator.pop(context);
});
} else {
setState(() {
_pinnedPosts =
resp.body.map((x) => Post.fromJson(x)).toList().cast<Post>();
});
}
}
int get _userSocialCreditPoints {
int birthPart =
DateTime.now().difference(_userinfo!.createdAt.toLocal()).inSeconds;
birthPart = birthPart >> 16;
return _totalUpvote * 2 - _totalDownvote + birthPart;
}
@override
void initState() {
super.initState();
_relationshipProvider = Get.find();
_postController = PostListController(author: widget.name);
_albumPagingController.addPageRequestListener((pageKey) async {
final client = ServiceFinder.configureClient('files');
final resp = await client
.get('/attachments?take=10&offset=$pageKey&author=${widget.name}');
if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body);
final out = result.data
?.map((e) => Attachment.fromJson(e))
.where((x) => x.mimetype.split('/').firstOrNull == 'image')
.toList();
if (out != null && result.data!.length >= 10) {
_albumPagingController.appendPage(out, pageKey + out.length);
} else if (out != null) {
_albumPagingController.appendLastPage(out);
}
} else {
_albumPagingController.error = resp.bodyString;
}
});
_getUserinfo();
getPinnedPosts();
}
Widget _buildStatisticsEntry(String label, String content) {
return Expanded(
child: Column(
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall,
),
Text(
content,
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
);
}
@override
Widget build(BuildContext context) {
if (_isBusy || _userinfo == null) {
return const Center(child: CircularProgressIndicator());
}
return Material(
color: Theme.of(context).colorScheme.surface,
child: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
centerTitle: false,
floating: true,
toolbarHeight: SolianTheme.toolbarHeight(context),
leadingWidth: 24,
automaticallyImplyLeading: false,
flexibleSpace: Row(
children: [
AppBarLeadingButton.adaptive(context) ??
const SizedBox(width: 8),
const SizedBox(width: 8),
if (_userinfo != null)
AccountAvatar(content: _userinfo!.avatar, radius: 16),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_userinfo != null)
Text(
_userinfo!.nick,
style: Theme.of(context).textTheme.bodyLarge,
),
if (_userinfo != null)
Text(
'@${_userinfo!.name}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
if (_userinfo != null &&
!_relationshipProvider.hasFriend(_userinfo!))
IconButton(
icon: const Icon(Icons.person_add),
onPressed: _isMakingFriend
? null
: () async {
setState(() => _isMakingFriend = true);
try {
await _relationshipProvider
.makeFriend(widget.name);
context.showSnackbar(
'accountFriendRequestSent'.tr,
);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isMakingFriend = false);
}
},
)
else
const IconButton(
icon: Icon(Icons.handshake),
onPressed: null,
),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
),
],
),
bottom: TabBar(
tabs: [
Tab(text: 'profilePosts'.tr),
Tab(text: 'profileAlbum'.tr),
],
),
)
];
},
body: TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: [
RefreshIndicator(
onRefresh: () => Future.wait([
_postController.reloadAllOver(),
getPinnedPosts(),
]),
child: CustomScrollView(slivers: [
SliverToBoxAdapter(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatisticsEntry(
'totalSocialCreditPoints'.tr,
_userinfo != null
? _userSocialCreditPoints.toString()
: 0.toString(),
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Obx(
() => _buildStatisticsEntry(
'totalPostCount'.tr,
_postController.postTotal.value.toString(),
),
),
_buildStatisticsEntry(
'totalUpvote'.tr,
_totalUpvote.toString(),
),
_buildStatisticsEntry(
'totalDownvote'.tr,
_totalDownvote.toString(),
),
],
),
],
).paddingOnly(top: 16, bottom: 12),
),
const SliverToBoxAdapter(
child: Divider(thickness: 0.3, height: 0.3),
),
SliverList.separated(
itemCount: _pinnedPosts.length,
itemBuilder: (context, idx) {
final element = _pinnedPosts[idx];
return Material(
color:
Theme.of(context).colorScheme.surfaceContainerLow,
child: PostListEntryWidget(
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
item: element,
isClickable: true,
isNestedClickable: true,
isShowEmbed: true,
onUpdate: () {
_postController.reloadAllOver();
},
),
);
},
separatorBuilder: (context, idx) =>
const Divider(thickness: 0.3, height: 0.3),
),
if (_userinfo == null)
const SliverFillRemaining(
child: Center(child: CircularProgressIndicator()),
),
if (_userinfo != null)
PostWarpedListWidget(
isPinned: false,
controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
),
]),
),
CenteredContainer(
child: RefreshIndicator(
onRefresh: () =>
Future.sync(() => _albumPagingController.refresh()),
child: PagedGridView<int, Attachment>(
padding: EdgeInsets.zero,
pagingController: _albumPagingController,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
),
builderDelegate: PagedChildBuilderDelegate<Attachment>(
itemBuilder: (BuildContext context, item, int index) {
const radius = BorderRadius.all(Radius.circular(8));
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 0.3,
),
borderRadius: radius,
),
child: ClipRRect(
borderRadius: radius,
child: AttachmentListEntry(
item: item,
isDense: true,
parentId: 'album',
showMature: _showMature,
onReveal: (value) {
setState(() => _showMature = value);
},
),
),
);
},
),
).paddingAll(16),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,191 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/stickers.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/stickers/sticker_uploader.dart';
class StickerScreen extends StatefulWidget {
const StickerScreen({super.key});
@override
State<StickerScreen> createState() => _StickerScreenState();
}
class _StickerScreenState extends State<StickerScreen> {
final PagingController<int, StickerPack> _pagingController =
PagingController(firstPageKey: 0);
Future<bool> _promptDelete(Sticker item, String prefix) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return false;
final confirm = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('stickerDeletionConfirm'.tr),
content: Text(
'stickerDeletionConfirmCaption'.trParams({
'name': ':${'$prefix${item.alias}'.camelCase}:',
}),
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('cancel'.tr),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('confirm'.tr),
),
],
),
);
if (confirm != true) return false;
final client = auth.configureClient('files');
final resp = await client.delete('/stickers/${item.id}');
return resp.statusCode == 200;
}
Future<bool?> _promptUploadSticker({Sticker? edit}) {
return showDialog(
context: context,
builder: (context) => StickerUploadDialog(
edit: edit,
),
);
}
Widget _buildEmoteEntry(Sticker item, String prefix) {
final imageUrl = ServiceFinder.buildUrl(
'files',
'/attachments/${item.attachmentId}',
);
return ListTile(
title: Text(item.name),
subtitle: Text(item.textWarpedPlaceholder),
contentPadding: const EdgeInsets.only(left: 16, right: 14),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit_square),
onPressed: () {
_promptUploadSticker(edit: item).then((value) {
if (value == true) _pagingController.refresh();
});
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
_promptDelete(item, prefix).then((value) {
if (value == true) _pagingController.refresh();
});
},
),
],
),
leading: PlatformInfo.canCacheImage
? CachedNetworkImage(
imageUrl: imageUrl,
width: 28,
height: 28,
)
: Image.network(
imageUrl,
width: 28,
height: 28,
),
);
}
@override
void initState() {
final AuthProvider auth = Get.find();
final name = auth.userProfile.value!['name'];
_pagingController.addPageRequestListener((pageKey) async {
final client = ServiceFinder.configureClient('files');
final resp = await client.get(
'/stickers/manifest?take=10&offset=$pageKey&author=$name',
);
if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body);
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
if (out != null && result.data!.length >= 10) {
_pagingController.appendPage(out, pageKey + out.length);
} else if (out != null) {
_pagingController.appendLastPage(out);
}
} else {
_pagingController.error = resp.bodyString;
}
});
super.initState();
}
@override
void dispose() {
final StickerProvider sticker = Get.find();
sticker.refreshAvailableStickers();
_pagingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
_promptUploadSticker().then((value) {
if (value == true) _pagingController.refresh();
});
},
),
body: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView(
slivers: [
PagedSliverList<int, StickerPack>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
itemBuilder: (BuildContext context, item, int index) {
return ExpansionTile(
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(item.name),
const SizedBox(width: 6),
Badge(
label: Text('#${item.id}'),
)
],
),
subtitle: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
children: item.stickers?.map((x) {
x.pack = item;
return _buildEmoteEntry(x, item.prefix);
}).toList() ??
List.empty(),
);
},
),
),
],
),
),
);
}
}

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