Compare commits
120 Commits
2c7dc8c2ea
...
2.2.2+62
Author | SHA1 | Date | |
---|---|---|---|
|
52f1826e91 | ||
|
28a4c86dbf | ||
|
85e48ce03b | ||
|
efef61a8ea | ||
|
10ead95af9 | ||
|
838ee4d55d | ||
|
13e42429a9 | ||
|
c6ce3fe2b7 | ||
|
ae9a7eb0fd | ||
|
5d6fb2442f | ||
|
5a85985534 | ||
|
c80499db03 | ||
|
b8dcdb2315 | ||
|
b7b921f1f4 | ||
|
319d5c7d7f | ||
|
4b5b001739 | ||
|
db8871a455 | ||
|
38dcaa6066 | ||
|
03275b46ca | ||
|
cf3b482fef | ||
|
aa4c04d4ef | ||
|
73b82f65e4 | ||
|
9471fe40fe | ||
|
0d1e18735e | ||
|
8bb62b5992 | ||
|
1e8a6dea5b | ||
|
5c2804cc4d | ||
|
0dbb8f132a | ||
|
3395f3dbd0 | ||
|
d258ba776e | ||
|
0dcfcaad56 | ||
|
687e720956 | ||
|
180876949e | ||
|
9718965809 | ||
|
5377161fb0 | ||
|
963e538ae5 | ||
|
a355e3bf90 | ||
|
cb4a2598c8 | ||
|
950612dc07 | ||
|
cbd1eaf1af | ||
|
ac41cbd99f | ||
|
9f9c90abc4 | ||
|
87029e3538 | ||
|
127d9adc09 | ||
|
c82dc7ad85 | ||
|
36bcff7a7c | ||
|
38201b547a | ||
|
ed0334fcda | ||
|
fbb486b90b | ||
|
9b34f385d5 | ||
|
bb7b731602 | ||
|
19076f8136 | ||
|
dc77a936ce | ||
|
7f58710c6f | ||
|
068ddcdcdc | ||
|
f4e9252ca0 | ||
|
3b1e918117 | ||
|
ed7981fdaf | ||
|
9698ca53e4 | ||
|
ddc1dc7daf | ||
|
1625a957f8 | ||
|
2dc50d627e | ||
|
2ffde9a3dd | ||
|
5967a91ae1 | ||
|
32c1effcb5 | ||
|
9d0e19c56f | ||
|
acf4e634fe | ||
|
25942c2338 | ||
|
a4f81f6ba1 | ||
|
c1b9090e51 | ||
|
f494f70003 | ||
|
fb2a55a909 | ||
|
4edfa7fd50 | ||
|
d699cac9b1 | ||
|
c0428e12c1 | ||
|
55f434ff05 | ||
|
f2b3bdda2d | ||
|
1f6bf33b0e | ||
|
e2027b1a32 | ||
|
2b3a58b55e | ||
|
6ac536412a | ||
|
52f8ffe4e4 | ||
|
aca81431aa | ||
|
1fadd850b7 | ||
|
ed2a9a21b6 | ||
|
57279eb3e4 | ||
|
c403a2914a | ||
|
bcb176344c | ||
|
ecf362cffc | ||
|
f4ab7671d8 | ||
|
a2a3018917 | ||
|
0bdb664000 | ||
|
9c3b61ce57 | ||
|
d06df3d278 | ||
|
547ba19e61 | ||
|
cb05ff2e9e | ||
|
f614da7918 | ||
|
a3c8dafff9 | ||
|
fa978a7cd1 | ||
|
aaa0a562b4 | ||
|
590a4ce2a6 | ||
|
f26edce071 | ||
|
603799ea32 | ||
|
a32baf7798 | ||
|
498c9af663 | ||
|
202dbff6d3 | ||
|
96fd64d85d | ||
|
e236b7f98b | ||
|
5c7929e618 | ||
|
7ba5260246 | ||
|
a6d4947a23 | ||
|
7fbd4e9647 | ||
|
95d926b29f | ||
|
f6cf6d0440 | ||
|
e503c3f02f | ||
|
d4fbdd397e | ||
|
03943a7138 | ||
|
44f2c5fe0e | ||
|
bb66d5b684 | ||
|
1fca36293d |
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"sync": {
|
||||
"region": "solian-next",
|
||||
"region": "solian",
|
||||
"configPath": "roadsign.toml"
|
||||
},
|
||||
"deployments": [
|
||||
{
|
||||
"region": "solian-next",
|
||||
"site": "solian-next-web",
|
||||
"region": "solian",
|
||||
"site": "solian-web",
|
||||
"path": "build/web"
|
||||
}
|
||||
]
|
||||
|
@@ -17,11 +17,16 @@
|
||||
android:label="Solian"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:launchMode="singleInstance"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
|
26
api/Paperclip/Activate Boost.bru
Normal file
26
api/Paperclip/Activate Boost.bru
Normal file
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Activate Boost
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/uc/boosts/1/activate
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"client_id": "{{third_client_id}}",
|
||||
"client_secret":"{{third_client_tk}}",
|
||||
"type": "general",
|
||||
"subject": "Merry Christmas!",
|
||||
"subtitle": "一条来自 Solar Network 团队的信息",
|
||||
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
|
||||
"metadata": {
|
||||
"image": "6EqsYQwmFRCkbmhR"
|
||||
},
|
||||
"priority": 10
|
||||
}
|
||||
}
|
19
api/Paperclip/Stickers/Create Sticker Pack.bru
Normal file
19
api/Paperclip/Stickers/Create Sticker Pack.bru
Normal file
@@ -0,0 +1,19 @@
|
||||
meta {
|
||||
name: Create Sticker Pack
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/uc/stickers/packs
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"prefix": "cat",
|
||||
"name": "Solar Network full of Cats!",
|
||||
"description": "The sticker packs is full of stickers which related with cats!"
|
||||
}
|
||||
}
|
20
api/Paperclip/Stickers/Create Sticker.bru
Normal file
20
api/Paperclip/Stickers/Create Sticker.bru
Normal file
@@ -0,0 +1,20 @@
|
||||
meta {
|
||||
name: Create Sticker
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/uc/stickers
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"alias": "Meltdown",
|
||||
"name": "Meltdown",
|
||||
"attachment_id": "IpDPHEbWDDCbBofX",
|
||||
"pack_id": 4
|
||||
}
|
||||
}
|
11
api/Paperclip/Stickers/Get Sticker Packs.bru
Normal file
11
api/Paperclip/Stickers/Get Sticker Packs.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Get Sticker Packs
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/cgi/uc/stickers/packs
|
||||
body: none
|
||||
auth: none
|
||||
}
|
15
api/Paperclip/Stickers/Get Stickers.bru
Normal file
15
api/Paperclip/Stickers/Get Stickers.bru
Normal file
@@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: Get Stickers
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/cgi/uc/stickers?take=10
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
params:query {
|
||||
take: 10
|
||||
}
|
@@ -7,11 +7,7 @@ meta {
|
||||
post {
|
||||
url: {{endpoint}}/cgi/id/dev/notify/all
|
||||
body: json
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{atk}}
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
@@ -19,11 +15,11 @@ body:json {
|
||||
"client_id": "{{third_client_id}}",
|
||||
"client_secret":"{{third_client_tk}}",
|
||||
"type": "general",
|
||||
"subject": "Merry Christmas!",
|
||||
"subject": "新年快乐!",
|
||||
"subtitle": "一条来自 Solar Network 团队的信息",
|
||||
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
|
||||
"content": "今天是农历正月初一,小羊祝您新年快乐 🎉",
|
||||
"metadata": {
|
||||
"image": "6EqsYQwmFRCkbmhR"
|
||||
"image": "D2EDbcrsTugs3xk5"
|
||||
},
|
||||
"priority": 10
|
||||
}
|
||||
|
26
api/Passport/Developer Notify One User.bru
Normal file
26
api/Passport/Developer Notify One User.bru
Normal file
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Developer Notify One User
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/id/dev/notify/1
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"client_id": "{{third_client_id}}",
|
||||
"client_secret":"{{third_client_tk}}",
|
||||
"type": "general",
|
||||
"subject": "测试",
|
||||
"subtitle": "Alphabot です",
|
||||
"content": "全新通知动画",
|
||||
"metadata": {
|
||||
"image": "D2EDbcrsTugs3xk5"
|
||||
},
|
||||
"priority": 10
|
||||
}
|
||||
}
|
11
api/Reader/List News Sources.bru
Normal file
11
api/Reader/List News Sources.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: List News Sources
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/cgi/re/well-known/sources
|
||||
body: none
|
||||
auth: none
|
||||
}
|
17
api/Reader/List News.bru
Normal file
17
api/Reader/List News.bru
Normal file
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: List News
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/cgi/re/news?take=10&offset=0&source=shadiao
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
params:query {
|
||||
take: 10
|
||||
offset: 0
|
||||
source: shadiao
|
||||
}
|
18
api/Reader/Trigger Scan News.bru
Normal file
18
api/Reader/Trigger Scan News.bru
Normal file
@@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: Trigger Scan News
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/re/admin/scan
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"sources": ["taiwan-ltn"],
|
||||
"eager": true
|
||||
}
|
||||
}
|
20
api/Wallet/Create Order.bru
Normal file
20
api/Wallet/Create Order.bru
Normal file
@@ -0,0 +1,20 @@
|
||||
meta {
|
||||
name: Create Order
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/wa/orders
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"client_id": "highland-mc",
|
||||
"client_secret": "(3^DLAvo3v",
|
||||
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
|
||||
"amount": 500
|
||||
}
|
||||
}
|
21
api/Wallet/Create Transaction.bru
Normal file
21
api/Wallet/Create Transaction.bru
Normal file
@@ -0,0 +1,21 @@
|
||||
meta {
|
||||
name: Create Transaction
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/wa/transactions
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"client_id": "highland-mc",
|
||||
"client_secret": "(3^DLAvo3v",
|
||||
"remark": "秦始皇的赞赏",
|
||||
"amount": 500,
|
||||
"payee_id": 1
|
||||
}
|
||||
}
|
20
api/Wallet/Get Order.bru
Normal file
20
api/Wallet/Get Order.bru
Normal file
@@ -0,0 +1,20 @@
|
||||
meta {
|
||||
name: Get Order
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/cgi/wa/orders/4
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"client_id": "highland-mc",
|
||||
"client_secret": "(3^DLAvo3v",
|
||||
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
|
||||
"amount": 500
|
||||
}
|
||||
}
|
20
api/Wallet/Get Transaction.bru
Normal file
20
api/Wallet/Get Transaction.bru
Normal file
@@ -0,0 +1,20 @@
|
||||
meta {
|
||||
name: Get Transaction
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/cgi/wa/transactions/67
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"client_id": "highland-mc",
|
||||
"client_secret": "(3^DLAvo3v",
|
||||
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
|
||||
"amount": 500
|
||||
}
|
||||
}
|
11
api/WatchTower/Run Database Maintenance.bru
Normal file
11
api/WatchTower/Run Database Maintenance.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Run Database Maintenance
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/wt/maintenance/database
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
7
api/collection.bru
Normal file
7
api/collection.bru
Normal file
@@ -0,0 +1,7 @@
|
||||
auth {
|
||||
mode: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{atk}}
|
||||
}
|
BIN
assets/icon/tray-icon.ico
Normal file
BIN
assets/icon/tray-icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
assets/icon/tray-icon.png
Normal file
BIN
assets/icon/tray-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 228 KiB |
@@ -17,6 +17,10 @@
|
||||
"screenAccountProfileEdit": "Edit Profile",
|
||||
"screenAbuseReport": "Abuse Reports",
|
||||
"screenSettings": "Settings",
|
||||
"screenAccountSettings": "Account Settings",
|
||||
"screenFactorSettings": "Auth Factors",
|
||||
"screenAccountWallet": "Wallet",
|
||||
"screenNews": "News",
|
||||
"screenAlbum": "Album",
|
||||
"screenChat": "Chat",
|
||||
"screenChatManage": "Edit Channel",
|
||||
@@ -103,8 +107,18 @@
|
||||
},
|
||||
"loginEnterPassword": "Enter the code",
|
||||
"loginSuccess": "Logged in as {}",
|
||||
"authFactorDelete": "Delete Auth Factor",
|
||||
"authFactorDeleteDescription": "Are you sure you want delete auth factor {}?",
|
||||
"authFactorPassword": "Password",
|
||||
"authFactorPasswordDescription": "The password you set when you registered.",
|
||||
"authFactorEmail": "Email verification code",
|
||||
"authFactorEmailDescription": "An one-time code sent to the email address you set when you registered.",
|
||||
"authFactorTOTP": "Time-based OTP",
|
||||
"authFactorTOTPDescription": "A one-time code generated by a TOTP authenticator such as Google Authenticator or Authy.",
|
||||
"authFactorInAppNotify": "In-app notification",
|
||||
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
|
||||
"authFactorAdd": "Add a factor",
|
||||
"authFactorAddSubtitle": "Provide another way to login your account.",
|
||||
"accountIntroTitle": "Hello there!",
|
||||
"accountIntroSubtitle": "Pick an option below to get started.",
|
||||
"accountLogout": "Logout",
|
||||
@@ -113,8 +127,14 @@
|
||||
"accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.",
|
||||
"accountPublishers": "Your publishers",
|
||||
"accountPublishersSubtitle": "Manage your publish identities.",
|
||||
"accountSettings": "Account Settings",
|
||||
"accountSettingsSubtitle": "Manage your account and make it yours.",
|
||||
"accountProfileEdit": "Edit your profile",
|
||||
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
|
||||
"accountWallet": "Wallet",
|
||||
"accountWalletSubtitle": "View your balance and transactions.",
|
||||
"factorSettings": "Auth Factors",
|
||||
"factorSettingsSubtitle": "Manage your authentication factors.",
|
||||
"accountProfileEditApplied": "Profile modification applied.",
|
||||
"publishersNew": "New Publisher",
|
||||
"publisherNewSubtitle": "Create a new publisher identity.",
|
||||
@@ -179,8 +199,13 @@
|
||||
"other": "{} comments"
|
||||
},
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsDisplayLanguage": "Display Language",
|
||||
"settingsDisplayLanguageDescription": "Set the application language.",
|
||||
"settingsDisplayLanguageSystem": "Follow System",
|
||||
"settingsAppBarTransparent": "Transparent App Bar",
|
||||
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
|
||||
"settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
|
||||
"settingsDrawerPreferCollapseDescription": "Make the drawer to collapse even when the screen is wide enough.",
|
||||
"settingsBackgroundImage": "Background Image",
|
||||
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
|
||||
"settingsBackgroundImageClear": "Clear Existing Background Image",
|
||||
@@ -191,6 +216,13 @@
|
||||
"settingsColorSchemeDescription": "Set the application primary color.",
|
||||
"settingsColorSeed": "Color Seed",
|
||||
"settingsColorSeedDescription": "Select one of the present color schemes.",
|
||||
"settingsFeatures": "Features",
|
||||
"settingsNotifyWithHaptic": "Haptic when Notified",
|
||||
"settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.",
|
||||
"settingsExpandPostLink": "Expand Post Link",
|
||||
"settingsExpandPostLinkDescription": "Expand the post link in the post list.",
|
||||
"settingsExpandChatLink": "Expand Chat Link",
|
||||
"settingsExpandChatLinkDescription": "Expand the chat link in the chat list.",
|
||||
"settingsNetwork": "Network",
|
||||
"settingsNetworkServer": "HyperNet Server",
|
||||
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
|
||||
@@ -209,12 +241,15 @@
|
||||
"settingsMisc": "Misc",
|
||||
"settingsMiscAbout": "About",
|
||||
"settingsMiscAboutDescription": "View the version information of Solian.",
|
||||
"settingsAccountLanguage": "Account Language",
|
||||
"settingsAccountLanguageDescription": "Set the language for email, notification, and other account-related content.",
|
||||
"sensitiveContent": "Sensitive Content",
|
||||
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
||||
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
||||
"sensitiveContentReveal": "Reveal",
|
||||
"serverConnecting": "Connecting to server...",
|
||||
"serverDisconnected": "Lost connection from server",
|
||||
"serverConnecting": "Connecting...",
|
||||
"serverDisconnected": "Connection Lost",
|
||||
"serverConnected": "Connected",
|
||||
"fieldChatAlias": "Channel Alias",
|
||||
"fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.",
|
||||
"fieldChatName": "Name",
|
||||
@@ -281,23 +316,50 @@
|
||||
"one": "{} attachment",
|
||||
"other": "{} attachments"
|
||||
},
|
||||
"messageTyping": {
|
||||
"one": "{} is typing...",
|
||||
"other": "{} are typing..."
|
||||
},
|
||||
"fieldAttachmentRandomId": "Random ID",
|
||||
"fieldAttachmentAlt": "Alternative text",
|
||||
"addAttachmentFromAlbum": "Add from album",
|
||||
"addAttachmentFromClipboard": "Paste file",
|
||||
"addAttachmentFromCameraPhoto": "Take photo",
|
||||
"addAttachmentFromCameraVideo": "Take video",
|
||||
"addAttachmentFromRandomId": "Link via RID",
|
||||
"attachmentDetailInfo": "Attachment details",
|
||||
"attachmentPastedImage": "Pasted Image",
|
||||
"attachmentInsertLink": "Insert Link",
|
||||
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
|
||||
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
||||
"attachmentCompressVideo": "Re-encode video",
|
||||
"attachmentSetThumbnail": "Set thumbnail",
|
||||
"attachmentSetAlt": "Set alternative text",
|
||||
"attachmentCopyRandomId": "Copy RID",
|
||||
"attachmentUpload": "Upload",
|
||||
"attachmentInputDialog": "Upload attachments",
|
||||
"attachmentInputUseRandomId": "Use Random ID",
|
||||
"attachmentInputNew": "New Upload",
|
||||
"waitingForUpload": "Waiting for upload",
|
||||
"attachmentVideoCompressHint": "Compress a copy of this video",
|
||||
"attachmentVideoCompressHintDescription": "Do you want to upload a compress copy of video {}? It will help your audience to preview this video faster and they still can watch the original video. It will take some while to process the video on your device, so please be patient.",
|
||||
"attachmentCompressQuality": "Compress quality",
|
||||
"attachmentCompressQualityHighest": "Highest",
|
||||
"attachmentCompressQualityDefault": "Default",
|
||||
"attachmentCompressQualityMedium": "Medium",
|
||||
"attachmentCompressQualityLow": "Low",
|
||||
"attachmentCompressQualityHint": "Solar Network doesn't prevent you from uploading large files, high resolution, high bitrate videos. But for your network conditions, we suggest you choose a suitable compression quality.",
|
||||
"attachmentUploaded": "Uploaded",
|
||||
"attachmentPending": "Pending",
|
||||
"attachmentCopyCompressed": "Copy compressed",
|
||||
"attachmentGotBoosted": "Boosted",
|
||||
"attachmentBoost": "Boost",
|
||||
"attachmentCreateBoost": "Create Boost",
|
||||
"attachmentBoostHint": "Boost is a feature that allows you to upload attachments to a server closer to your audience or a faster content network. This feature is currently in beta and is subject to change. It's all free for now, you can feel free to try, you will get notified when the pricing plan changed.",
|
||||
"attachmentDestinationRegion": "Destination Region",
|
||||
"attachmentDestinationRegionAPAC": "Asia Pacific",
|
||||
"attachmentDestinationRegionNGB": "Ning Bo, China, Zhejiang",
|
||||
"attachmentDestinationRegionHKG": "Hong Kong",
|
||||
"notification": "Notification",
|
||||
"notificationUnreadCount": {
|
||||
"zero": "All notifications read",
|
||||
@@ -388,6 +450,9 @@
|
||||
"celebrateBirthday": "Happy birthday, {}!",
|
||||
"celebrateMerryXmas": "Merry christmas, {}!",
|
||||
"celebrateNewYear": "Happy new year, {}!",
|
||||
"celebrateLunarNewYear": "Happy lunar new year, {}!",
|
||||
"celebrateMidAutumn": "Happy mid-autumn festival, {}!",
|
||||
"celebrateDragonBoat": "Happy dragon boat festival, {}!",
|
||||
"celebrateValentineDay": "Today is valentine's day, {}!",
|
||||
"celebrateLaborDay": "Today is labor day, {}.",
|
||||
"celebrateMotherDay": "Today is mother's day, {}.",
|
||||
@@ -397,6 +462,9 @@
|
||||
"celebrateThanksgiving": "Today is thanksgiving day, {}!",
|
||||
"pendingBirthday": "Birthday in {}",
|
||||
"pendingMerryXmas": "Christmas in {}",
|
||||
"pendingLunarNewYear": "Lunar new year in {}",
|
||||
"pendingMidAutumn": "Mid-autumn festival in {}",
|
||||
"pendingDragonBoat": "Dragon boat festival in {}",
|
||||
"pendingNewYear": "New year in {}",
|
||||
"pendingValentineDay": "Valentine's day in {}",
|
||||
"pendingLaborDay": "Labor day in {}",
|
||||
@@ -438,6 +506,7 @@
|
||||
"accountJoinedAt": "Joined at {}",
|
||||
"accountBirthday": "Born on {}",
|
||||
"accountBadge": "Badge",
|
||||
"accountCheckInNoRecords": "No check-in records",
|
||||
"badgeCompanyStaff": "Solsynth Staff",
|
||||
"badgeSiteMigration": "Solar Network Native",
|
||||
"accountStatus": "Status",
|
||||
@@ -446,6 +515,7 @@
|
||||
"accountStatusLastSeen": "Last seen at {}",
|
||||
"postArticle": "Article on the Solar Network",
|
||||
"postStory": "Story on the Solar Network",
|
||||
"postLocalDraftRestored": "Restored from device",
|
||||
"articleWrittenAt": "Written at {}",
|
||||
"articleEditedAt": "Edited at {}",
|
||||
"attachmentSaved": "Saved to album",
|
||||
@@ -486,11 +556,15 @@
|
||||
"postImageShareAds": "Explore posts on the Solar Network",
|
||||
"postShare": "Share",
|
||||
"postShareImage": "Share via Image",
|
||||
"postGetInsight": "Get Insight",
|
||||
"postGetInsightTitle": "AI Insight",
|
||||
"postGetInsightDescription": "AI may make mistakes, check important information.",
|
||||
"appInitializing": "Initializing",
|
||||
"poweredBy": "Powered by {}",
|
||||
"shareIntent": "Share",
|
||||
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
||||
"shareIntentPostStory": "Post a Story",
|
||||
"shareIntentSendChannel": "Share to Channel",
|
||||
"updateAvailable": "Update Available",
|
||||
"updateOngoing": "Updating, please wait...",
|
||||
"custom": "Custom",
|
||||
@@ -503,6 +577,7 @@
|
||||
"colorSchemeWhite": "White",
|
||||
"colorSchemeBlack": "Black",
|
||||
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
||||
"postFeaturedComment": "Featured Comment",
|
||||
"postCategoryTechnology": "Technology",
|
||||
"postCategoryGaming": "Gaming",
|
||||
"postCategoryLife": "Life",
|
||||
@@ -514,11 +589,26 @@
|
||||
"postCategoryLiterature": "Literature",
|
||||
"postCategoryFunny": "Funny",
|
||||
"postCategoryUncategorized": "Uncategorized",
|
||||
"waitingForUpload": "Waiting for upload",
|
||||
"attachmentCompressQuality": "Compress quality",
|
||||
"attachmentCompressQualityHighest": "Highest",
|
||||
"attachmentCompressQualityDefault": "Default",
|
||||
"attachmentCompressQualityMedium": "Medium",
|
||||
"attachmentCompressQualityLow": "Low",
|
||||
"attachmentCompressQualityHint": "Solar Network doesn't prevent you from uploading large files, high resolution, high bitrate videos. But for your network conditions, we suggest you choose a suitable compression quality."
|
||||
"newsAllSources": "All News",
|
||||
"newsReadingProviderSwap": "Swap",
|
||||
"newsReadingFromReader": "You're reading from HyperNet.Reader",
|
||||
"newsReadingFromOriginal": "You're reading the original article",
|
||||
"newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.",
|
||||
"newsToday": "Today's News",
|
||||
"totpPostSetup": "One More Thing",
|
||||
"totpPostSetupDescription": "Scan the QR Code below with Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden or any of kind of authenticator app which supports TOTP.",
|
||||
"totpNeverShare": "Never share this QR Code",
|
||||
"needHelp": "Need Help?",
|
||||
"needHelpLaunch": "Check out our Goatpedia!",
|
||||
"walletCreate": "Create a Wallet",
|
||||
"walletCreateSubtitle": "Create a wallet to start using Source Points",
|
||||
"walletCreatePassword": "Set a payment password for your new wallet below",
|
||||
"walletCurrencyShort": "SRC",
|
||||
"walletCurrency": {
|
||||
"one": "{} Source Point",
|
||||
"other": "{} Source Points"
|
||||
},
|
||||
"aiThinkingProcess": "AI Thinking Process",
|
||||
"accountSettingsApplied": "Account settings have been applied.",
|
||||
"trayMenuExit": "Exit"
|
||||
}
|
||||
|
@@ -15,6 +15,10 @@
|
||||
"screenAccountProfileEdit": "编辑资料",
|
||||
"screenAbuseReport": "滥用检举",
|
||||
"screenSettings": "设置",
|
||||
"screenAccountSettings": "账号设置",
|
||||
"screenFactorSettings": "验证因子",
|
||||
"screenAccountWallet": "钱包",
|
||||
"screenNews": "新闻",
|
||||
"screenAlbum": "相册",
|
||||
"screenChat": "聊天",
|
||||
"screenChatManage": "编辑聊天频道",
|
||||
@@ -87,8 +91,18 @@
|
||||
},
|
||||
"loginEnterPassword": "验证代码",
|
||||
"loginSuccess": "登录为 {}",
|
||||
"authFactorDelete": "删除验证因子",
|
||||
"authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
|
||||
"authFactorPassword": "密码",
|
||||
"authFactorPasswordDescription": "注册时选择设置的密码。",
|
||||
"authFactorEmail": "电邮一次性验证码",
|
||||
"authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
|
||||
"authFactorTOTP": "时序验证码",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
|
||||
"authFactorInAppNotify": "应用内通知验证码",
|
||||
"authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
|
||||
"authFactorAdd": "添加新验证因子",
|
||||
"authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
|
||||
"accountIntroTitle": "喜欢您来!",
|
||||
"accountIntroSubtitle": "登陆以探索更广大的世界。",
|
||||
"accountLogout": "退出登录",
|
||||
@@ -97,8 +111,14 @@
|
||||
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
|
||||
"accountPublishers": "你的发布者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帐号设置",
|
||||
"accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
|
||||
"accountProfileEdit": "编辑资料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
|
||||
"accountWallet": "钱包",
|
||||
"accountWalletSubtitle": "查看你的余额和交易记录。",
|
||||
"factorSettings": "验证因子",
|
||||
"factorSettingsSubtitle": "管理你的登陆验证方式。",
|
||||
"accountProfileEditApplied": "个人资料修改已被应用。",
|
||||
"publishersNew": "新发布者",
|
||||
"publisherNewSubtitle": "创建一个新的公共身份。",
|
||||
@@ -177,6 +197,9 @@
|
||||
"other": "{} 条评论"
|
||||
},
|
||||
"settingsAppearance": "外观",
|
||||
"settingsDisplayLanguage": "显示语言",
|
||||
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
|
||||
"settingsDisplayLanguageSystem": "跟随系统",
|
||||
"settingsBackgroundImage": "背景图片",
|
||||
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
|
||||
"settingsBackgroundImageClear": "清除现存背景图",
|
||||
@@ -185,10 +208,19 @@
|
||||
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
|
||||
"settingsAppBarTransparent": "透明顶栏",
|
||||
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
|
||||
"settingsDrawerPreferCollapse": "侧边栏偏好折叠",
|
||||
"settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
|
||||
"settingsColorScheme": "主题色",
|
||||
"settingsColorSchemeDescription": "设置应用主题色。",
|
||||
"settingsColorSeed": "预设色彩主题",
|
||||
"settingsColorSeedDescription": "选择一个预设色彩主题。",
|
||||
"settingsFeatures": "功能",
|
||||
"settingsNotifyWithHaptic": "新通知时振动",
|
||||
"settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
|
||||
"settingsExpandPostLink": "展开帖子链接",
|
||||
"settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。",
|
||||
"settingsExpandChatLink": "展开聊天链接",
|
||||
"settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。",
|
||||
"settingsNetwork": "网络",
|
||||
"settingsNetworkServer": "HyperNet 服务器",
|
||||
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
|
||||
@@ -207,12 +239,15 @@
|
||||
"settingsMisc": "杂项",
|
||||
"settingsMiscAbout": "关于",
|
||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||
"settingsAccountLanguage": "帐号偏好语言",
|
||||
"settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。",
|
||||
"sensitiveContent": "敏感内容",
|
||||
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
||||
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
||||
"sensitiveContentReveal": "显示内容",
|
||||
"serverConnecting": "正在连接服务器…",
|
||||
"serverDisconnected": "已与服务器断开连接",
|
||||
"serverConnecting": "正在连接…",
|
||||
"serverDisconnected": "已断开连接",
|
||||
"serverConnected": "已连接",
|
||||
"fieldChatAlias": "频道别名",
|
||||
"fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
|
||||
"fieldChatName": "名称",
|
||||
@@ -279,23 +314,50 @@
|
||||
"one": "{} 个附件",
|
||||
"other": "{} 个附件"
|
||||
},
|
||||
"messageTyping": {
|
||||
"one": "{} 正在输入",
|
||||
"other": "{} 正在输入"
|
||||
},
|
||||
"fieldAttachmentRandomId": "访问 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||
"addAttachmentFromClipboard": "粘贴附件",
|
||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||
"addAttachmentFromCameraVideo": "拍摄视频",
|
||||
"addAttachmentFromRandomId": "通过访问 ID 链接",
|
||||
"attachmentDetailInfo": "附件详细信息",
|
||||
"attachmentPastedImage": "粘贴的图片",
|
||||
"attachmentInsertLink": "插入连接",
|
||||
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
||||
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
||||
"attachmentCompressVideo": "重新编码视频",
|
||||
"attachmentSetThumbnail": "设置缩略图",
|
||||
"attachmentSetAlt": "设置概述文字",
|
||||
"attachmentCopyRandomId": "复制访问 ID",
|
||||
"attachmentUpload": "上传",
|
||||
"attachmentInputDialog": "上传附件",
|
||||
"attachmentInputUseRandomId": "使用访问 ID",
|
||||
"attachmentInputNew": "新上传附件",
|
||||
"waitingForUpload": "等待上传",
|
||||
"attachmentVideoCompressHint": "压缩一份视频的副本",
|
||||
"attachmentVideoCompressHintDescription": "你想上传压缩视频 {} 的副本吗?它将帮助你的观众快速预览视频,并且他们仍然可以观看原始视频。这将会在在你的设备上处理视频,所以需要一些时间,所以请耐心等待。",
|
||||
"attachmentCompressQuality": "压缩质量",
|
||||
"attachmentCompressQualityHighest": "最高",
|
||||
"attachmentCompressQualityDefault": "默认",
|
||||
"attachmentCompressQualityMedium": "中等",
|
||||
"attachmentCompressQualityLow": "低",
|
||||
"attachmentCompressQualityHint": "Solar Network 并没有阻止你上传大文件、高分辨率、高码率的视频,但是为了你的网络情况观众考虑,我们建议你选择一个合适的压缩质量。",
|
||||
"attachmentUploaded": "已上传",
|
||||
"attachmentPending": "未上传",
|
||||
"attachmentCopyCompressed": "有压缩副本",
|
||||
"attachmentGotBoosted": "有加速传递",
|
||||
"attachmentBoost": "加速包",
|
||||
"attachmentCreateBoost": "加速传递",
|
||||
"attachmentBoostHint": "加速传递允许您将附件上传到更近的受众或更快的内容网络。该功能目前处于 Beta 阶段。该功能限时免费,当有价格计划更改时,您将会被通知。",
|
||||
"attachmentDestinationRegion": "目标节点",
|
||||
"attachmentDestinationRegionAPAC": "亚太地区",
|
||||
"attachmentDestinationRegionNGB": "中国 · 浙江 · 宁波",
|
||||
"attachmentDestinationRegionHKG": "香港",
|
||||
"notification": "通知",
|
||||
"notificationUnreadCount": {
|
||||
"zero": "无未读通知",
|
||||
@@ -384,6 +446,9 @@
|
||||
"dailyCheckNegativeHint6": "出门",
|
||||
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
|
||||
"celebrateBirthday": "生日快乐,{}!",
|
||||
"celebrateLunarNewYear": "春节快乐,{}!",
|
||||
"celebrateMidAutumn": "中秋节快乐,{}!",
|
||||
"celebrateDragonBoat": "端午节快乐,{}!",
|
||||
"celebrateMerryXmas": "圣诞快乐,{}!",
|
||||
"celebrateNewYear": "新年快乐,{}!",
|
||||
"celebrateValentineDay": "今天是情人节,{}!",
|
||||
@@ -393,6 +458,9 @@
|
||||
"celebrateFatherDay": "今天是父亲节,{}。",
|
||||
"celebrateHalloween": "快乐在圣诞节,{}!",
|
||||
"celebrateThanksgiving": "今天是感恩节,{}!",
|
||||
"pendingLunarNewYear": "{} 过春节",
|
||||
"pendingMidAutumn": "{} 过中秋节",
|
||||
"pendingDragonBoat": "{} 过端午节",
|
||||
"pendingBirthday": "{} 过生日",
|
||||
"pendingMerryXmas": "{} 过圣诞节",
|
||||
"pendingNewYear": "{} 跨年",
|
||||
@@ -436,6 +504,7 @@
|
||||
"accountJoinedAt": "加入于 {}",
|
||||
"accountBirthday": "出生于 {}",
|
||||
"accountBadge": "徽章",
|
||||
"accountCheckInNoRecords": "暂无运势记录",
|
||||
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
|
||||
"badgeSiteMigration": "Solar Network 原住民",
|
||||
"accountStatus": "状态",
|
||||
@@ -444,6 +513,7 @@
|
||||
"accountStatusLastSeen": "最后一次上线于 {}",
|
||||
"postArticle": "Solar Network 上的文章",
|
||||
"postStory": "Solar Network 上的故事",
|
||||
"postLocalDraftRestored": "从本地恢复草稿",
|
||||
"articleWrittenAt": "发表于 {}",
|
||||
"articleEditedAt": "编辑于 {}",
|
||||
"attachmentSaved": "已保存到相册",
|
||||
@@ -484,11 +554,15 @@
|
||||
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖图",
|
||||
"postGetInsight": "获取见解",
|
||||
"postGetInsightTitle": "AI 见解",
|
||||
"postGetInsightDescription": "AI 可能会出错,检查信息真实性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
"shareIntentDescription": "您想对您分享的内容做些什么?",
|
||||
"shareIntentPostStory": "发布动态",
|
||||
"shareIntentSendChannel": "分享到聊天频道",
|
||||
"updateAvailable": "检测到更新可用",
|
||||
"updateOngoing": "正在更新,请稍后……",
|
||||
"custom": "自定义",
|
||||
@@ -501,6 +575,7 @@
|
||||
"colorSchemeWhite": "白色",
|
||||
"colorSchemeBlack": "黑色",
|
||||
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
||||
"postFeaturedComment": "精选评论",
|
||||
"postCategoryTechnology": "技术",
|
||||
"postCategoryGaming": "游戏",
|
||||
"postCategoryLife": "生活",
|
||||
@@ -512,11 +587,26 @@
|
||||
"postCategoryLiterature": "文学",
|
||||
"postCategoryFunny": "搞笑",
|
||||
"postCategoryUncategorized": "未分类",
|
||||
"waitingForUpload": "等待上传",
|
||||
"attachmentCompressQuality": "压缩质量",
|
||||
"attachmentCompressQualityHighest": "最高",
|
||||
"attachmentCompressQualityDefault": "默认",
|
||||
"attachmentCompressQualityMedium": "中等",
|
||||
"attachmentCompressQualityLow": "低",
|
||||
"attachmentCompressQualityHint": "Solar Network 并没有阻止你上传大文件、高分辨率、高码率的视频,但是为了你的网络情况观众考虑,我们建议你选择一个合适的压缩质量。"
|
||||
"newsAllSources": "所有新闻",
|
||||
"newsReadingProviderSwap": "切换",
|
||||
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
|
||||
"newsReadingFromOriginal": "你正在阅读原始文章",
|
||||
"newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。",
|
||||
"newsToday": "快讯",
|
||||
"totpPostSetup": "还有一件事",
|
||||
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的验证器扫描本 QR Code 来添加。",
|
||||
"totpNeverShare": "永远不要分享这个 QR Code",
|
||||
"needHelp": "需要帮助?",
|
||||
"needHelpLaunch": "查看我们的山羊维基!",
|
||||
"walletCreate": "创建钱包",
|
||||
"walletCreateSubtitle": "创建于一个钱包来开始使用源点。",
|
||||
"walletCreatePassword": "在下方设置你的付款密码",
|
||||
"walletCurrencyShort": "源点",
|
||||
"walletCurrency": {
|
||||
"one": "{} 源点",
|
||||
"other": "{} 源点"
|
||||
},
|
||||
"aiThinkingProcess": "AI 思考过程",
|
||||
"accountSettingsApplied": "帐号设置已应用。",
|
||||
"trayMenuExit": "退出"
|
||||
}
|
||||
|
@@ -15,6 +15,10 @@
|
||||
"screenAccountProfileEdit": "編輯資料",
|
||||
"screenAbuseReport": "濫用檢舉",
|
||||
"screenSettings": "設置",
|
||||
"screenAccountSettings": "賬號設置",
|
||||
"screenFactorSettings": "驗證因子",
|
||||
"screenAccountWallet": "錢包",
|
||||
"screenNews": "新聞",
|
||||
"screenAlbum": "相冊",
|
||||
"screenChat": "聊天",
|
||||
"screenChatManage": "編輯聊天頻道",
|
||||
@@ -87,8 +91,18 @@
|
||||
},
|
||||
"loginEnterPassword": "驗證代碼",
|
||||
"loginSuccess": "登錄為 {}",
|
||||
"authFactorDelete": "刪除驗證因子",
|
||||
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||
"authFactorPassword": "密碼",
|
||||
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||
"authFactorEmail": "電郵一次性驗證碼",
|
||||
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||
"authFactorTOTP": "時序驗證碼",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||
"authFactorAdd": "添加新驗證因子",
|
||||
"authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。",
|
||||
"accountIntroTitle": "喜歡您來!",
|
||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||
"accountLogout": "退出登錄",
|
||||
@@ -97,8 +111,14 @@
|
||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||
"accountPublishers": "你的發佈者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帳號設置",
|
||||
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||
"accountProfileEdit": "編輯資料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
|
||||
"accountWallet": "錢包",
|
||||
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||
"factorSettings": "驗證因子",
|
||||
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||
"publishersNew": "新發布者",
|
||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||
@@ -177,6 +197,9 @@
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsDisplayLanguage": "顯示語言",
|
||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||
@@ -185,10 +208,19 @@
|
||||
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||
"settingsAppBarTransparent": "透明頂欄",
|
||||
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
|
||||
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
|
||||
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
|
||||
"settingsColorScheme": "主題色",
|
||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||
"settingsColorSeed": "預設色彩主題",
|
||||
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||
"settingsFeatures": "功能",
|
||||
"settingsNotifyWithHaptic": "新通知時振動",
|
||||
"settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
|
||||
"settingsExpandPostLink": "展開帖子鏈接",
|
||||
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
|
||||
"settingsExpandChatLink": "展開聊天鏈接",
|
||||
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
|
||||
"settingsNetwork": "網絡",
|
||||
"settingsNetworkServer": "HyperNet 服務器",
|
||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||
@@ -207,12 +239,15 @@
|
||||
"settingsMisc": "雜項",
|
||||
"settingsMiscAbout": "關於",
|
||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||
"settingsAccountLanguage": "帳號偏好語言",
|
||||
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
|
||||
"sensitiveContent": "敏感內容",
|
||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||
"sensitiveContentReveal": "顯示內容",
|
||||
"serverConnecting": "正在連接服務器…",
|
||||
"serverDisconnected": "已與服務器斷開連接",
|
||||
"serverConnecting": "正在連接…",
|
||||
"serverDisconnected": "已斷開連接",
|
||||
"serverConnected": "已連接",
|
||||
"fieldChatAlias": "頻道別名",
|
||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||
"fieldChatName": "名稱",
|
||||
@@ -279,23 +314,50 @@
|
||||
"one": "{} 個附件",
|
||||
"other": "{} 個附件"
|
||||
},
|
||||
"messageTyping": {
|
||||
"one": "{} 正在輸入",
|
||||
"other": "{} 正在輸入"
|
||||
},
|
||||
"fieldAttachmentRandomId": "訪問 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||
"addAttachmentFromClipboard": "粘貼附件",
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||
"attachmentDetailInfo": "附件詳細信息",
|
||||
"attachmentPastedImage": "粘貼的圖片",
|
||||
"attachmentInsertLink": "插入連接",
|
||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||
"attachmentCompressVideo": "重新編碼視頻",
|
||||
"attachmentSetThumbnail": "設置縮略圖",
|
||||
"attachmentSetAlt": "設置概述文字",
|
||||
"attachmentCopyRandomId": "複製訪問 ID",
|
||||
"attachmentUpload": "上傳",
|
||||
"attachmentInputDialog": "上傳附件",
|
||||
"attachmentInputUseRandomId": "使用訪問 ID",
|
||||
"attachmentInputNew": "新上傳附件",
|
||||
"waitingForUpload": "等待上傳",
|
||||
"attachmentVideoCompressHint": "壓縮一份視頻的副本",
|
||||
"attachmentVideoCompressHintDescription": "你想上傳壓縮視頻 {} 的副本嗎?它將幫助你的觀眾快速預覽視頻,並且他們仍然可以觀看原始視頻。這將會在在你的設備上處理視頻,所以需要一些時間,所以請耐心等待。",
|
||||
"attachmentCompressQuality": "壓縮質量",
|
||||
"attachmentCompressQualityHighest": "最高",
|
||||
"attachmentCompressQualityDefault": "默認",
|
||||
"attachmentCompressQualityMedium": "中等",
|
||||
"attachmentCompressQualityLow": "低",
|
||||
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。",
|
||||
"attachmentUploaded": "已上傳",
|
||||
"attachmentPending": "未上傳",
|
||||
"attachmentCopyCompressed": "有壓縮副本",
|
||||
"attachmentGotBoosted": "有加速傳遞",
|
||||
"attachmentBoost": "加速包",
|
||||
"attachmentCreateBoost": "加速傳遞",
|
||||
"attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。",
|
||||
"attachmentDestinationRegion": "目標節點",
|
||||
"attachmentDestinationRegionAPAC": "亞太地區",
|
||||
"attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波",
|
||||
"attachmentDestinationRegionHKG": "香港",
|
||||
"notification": "通知",
|
||||
"notificationUnreadCount": {
|
||||
"zero": "無未讀通知",
|
||||
@@ -384,6 +446,9 @@
|
||||
"dailyCheckNegativeHint6": "出門",
|
||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||
"celebrateBirthday": "生日快樂,{}!",
|
||||
"celebrateLunarNewYear": "春節快樂,{}!",
|
||||
"celebrateMidAutumn": "中秋節快樂,{}!",
|
||||
"celebrateDragonBoat": "端午節快樂,{}!",
|
||||
"celebrateMerryXmas": "聖誕快樂,{}!",
|
||||
"celebrateNewYear": "新年快樂,{}!",
|
||||
"celebrateValentineDay": "今天是情人節,{}!",
|
||||
@@ -393,6 +458,9 @@
|
||||
"celebrateFatherDay": "今天是父親節,{}。",
|
||||
"celebrateHalloween": "快樂在聖誕節,{}!",
|
||||
"celebrateThanksgiving": "今天是感恩節,{}!",
|
||||
"pendingLunarNewYear": "{} 過春節",
|
||||
"pendingMidAutumn": "{} 過中秋節",
|
||||
"pendingDragonBoat": "{} 過端午節",
|
||||
"pendingBirthday": "{} 過生日",
|
||||
"pendingMerryXmas": "{} 過聖誕節",
|
||||
"pendingNewYear": "{} 跨年",
|
||||
@@ -436,6 +504,7 @@
|
||||
"accountJoinedAt": "加入於 {}",
|
||||
"accountBirthday": "出生於 {}",
|
||||
"accountBadge": "徽章",
|
||||
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||
"badgeSiteMigration": "Solar Network 原住民",
|
||||
"accountStatus": "狀態",
|
||||
@@ -444,6 +513,7 @@
|
||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||
"postArticle": "Solar Network 上的文章",
|
||||
"postStory": "Solar Network 上的故事",
|
||||
"postLocalDraftRestored": "從本地恢復草稿",
|
||||
"articleWrittenAt": "發表於 {}",
|
||||
"articleEditedAt": "編輯於 {}",
|
||||
"attachmentSaved": "已保存到相冊",
|
||||
@@ -484,11 +554,15 @@
|
||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖圖",
|
||||
"postGetInsight": "獲取見解",
|
||||
"postGetInsightTitle": "AI 見解",
|
||||
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||
"shareIntentPostStory": "發佈動態",
|
||||
"shareIntentSendChannel": "分享到聊天頻道",
|
||||
"updateAvailable": "檢測到更新可用",
|
||||
"updateOngoing": "正在更新,請稍後……",
|
||||
"custom": "自定義",
|
||||
@@ -501,6 +575,7 @@
|
||||
"colorSchemeWhite": "白色",
|
||||
"colorSchemeBlack": "黑色",
|
||||
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
||||
"postFeaturedComment": "精選評論",
|
||||
"postCategoryTechnology": "技術",
|
||||
"postCategoryGaming": "遊戲",
|
||||
"postCategoryLife": "生活",
|
||||
@@ -512,11 +587,25 @@
|
||||
"postCategoryLiterature": "文學",
|
||||
"postCategoryFunny": "搞笑",
|
||||
"postCategoryUncategorized": "未分類",
|
||||
"waitingForUpload": "等待上傳",
|
||||
"attachmentCompressQuality": "壓縮質量",
|
||||
"attachmentCompressQualityHighest": "最高",
|
||||
"attachmentCompressQualityDefault": "默認",
|
||||
"attachmentCompressQualityMedium": "中等",
|
||||
"attachmentCompressQualityLow": "低",
|
||||
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。"
|
||||
"newsAllSources": "所有新聞",
|
||||
"newsReadingProviderSwap": "切換",
|
||||
"newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
|
||||
"newsReadingFromOriginal": "你正在閲讀原始文章",
|
||||
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
|
||||
"newsToday": "快訊",
|
||||
"totpPostSetup": "還有一件事",
|
||||
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。",
|
||||
"totpNeverShare": "永遠不要分享這個 QR Code",
|
||||
"needHelp": "需要幫助?",
|
||||
"needHelpLaunch": "查看我們的山羊維基!",
|
||||
"walletCreate": "創建錢包",
|
||||
"walletCreateSubtitle": "創建於一個錢包來開始使用源點。",
|
||||
"walletCreatePassword": "在下方設置你的付款密碼",
|
||||
"walletCurrencyShort": "源點",
|
||||
"walletCurrency": {
|
||||
"one": "{} 源點",
|
||||
"other": "{} 源點"
|
||||
},
|
||||
"aiThinkingProcess": "AI 思考過程",
|
||||
"accountSettingsApplied": "帳號設置已應用。"
|
||||
}
|
||||
|
@@ -15,6 +15,10 @@
|
||||
"screenAccountProfileEdit": "編輯資料",
|
||||
"screenAbuseReport": "濫用檢舉",
|
||||
"screenSettings": "設置",
|
||||
"screenAccountSettings": "賬號設置",
|
||||
"screenFactorSettings": "驗證因子",
|
||||
"screenAccountWallet": "錢包",
|
||||
"screenNews": "新聞",
|
||||
"screenAlbum": "相冊",
|
||||
"screenChat": "聊天",
|
||||
"screenChatManage": "編輯聊天頻道",
|
||||
@@ -87,8 +91,18 @@
|
||||
},
|
||||
"loginEnterPassword": "驗證代碼",
|
||||
"loginSuccess": "登錄為 {}",
|
||||
"authFactorDelete": "刪除驗證因子",
|
||||
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||
"authFactorPassword": "密碼",
|
||||
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||
"authFactorEmail": "電郵一次性驗證碼",
|
||||
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||
"authFactorTOTP": "時序驗證碼",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||
"authFactorAdd": "添加新驗證因子",
|
||||
"authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。",
|
||||
"accountIntroTitle": "喜歡您來!",
|
||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||
"accountLogout": "退出登錄",
|
||||
@@ -97,8 +111,14 @@
|
||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||
"accountPublishers": "你的發佈者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帳號設置",
|
||||
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||
"accountProfileEdit": "編輯資料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
|
||||
"accountWallet": "錢包",
|
||||
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||
"factorSettings": "驗證因子",
|
||||
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||
"publishersNew": "新發布者",
|
||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||
@@ -177,6 +197,9 @@
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsDisplayLanguage": "顯示語言",
|
||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||
@@ -185,10 +208,19 @@
|
||||
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||
"settingsAppBarTransparent": "透明頂欄",
|
||||
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
|
||||
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
|
||||
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
|
||||
"settingsColorScheme": "主題色",
|
||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||
"settingsColorSeed": "預設色彩主題",
|
||||
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||
"settingsFeatures": "功能",
|
||||
"settingsNotifyWithHaptic": "新通知時振動",
|
||||
"settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
|
||||
"settingsExpandPostLink": "展開帖子鏈接",
|
||||
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
|
||||
"settingsExpandChatLink": "展開聊天鏈接",
|
||||
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
|
||||
"settingsNetwork": "網絡",
|
||||
"settingsNetworkServer": "HyperNet 服務器",
|
||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||
@@ -207,12 +239,15 @@
|
||||
"settingsMisc": "雜項",
|
||||
"settingsMiscAbout": "關於",
|
||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||
"settingsAccountLanguage": "帳號偏好語言",
|
||||
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
|
||||
"sensitiveContent": "敏感內容",
|
||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||
"sensitiveContentReveal": "顯示內容",
|
||||
"serverConnecting": "正在連接服務器…",
|
||||
"serverDisconnected": "已與服務器斷開連接",
|
||||
"serverConnecting": "正在連接…",
|
||||
"serverDisconnected": "已斷開連接",
|
||||
"serverConnected": "已連接",
|
||||
"fieldChatAlias": "頻道別名",
|
||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||
"fieldChatName": "名稱",
|
||||
@@ -279,23 +314,50 @@
|
||||
"one": "{} 個附件",
|
||||
"other": "{} 個附件"
|
||||
},
|
||||
"messageTyping": {
|
||||
"one": "{} 正在輸入",
|
||||
"other": "{} 正在輸入"
|
||||
},
|
||||
"fieldAttachmentRandomId": "訪問 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||
"addAttachmentFromClipboard": "粘貼附件",
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||
"attachmentDetailInfo": "附件詳細信息",
|
||||
"attachmentPastedImage": "粘貼的圖片",
|
||||
"attachmentInsertLink": "插入連接",
|
||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||
"attachmentCompressVideo": "重新編碼視頻",
|
||||
"attachmentSetThumbnail": "設置縮略圖",
|
||||
"attachmentSetAlt": "設置概述文字",
|
||||
"attachmentCopyRandomId": "複製訪問 ID",
|
||||
"attachmentUpload": "上傳",
|
||||
"attachmentInputDialog": "上傳附件",
|
||||
"attachmentInputUseRandomId": "使用訪問 ID",
|
||||
"attachmentInputNew": "新上傳附件",
|
||||
"waitingForUpload": "等待上傳",
|
||||
"attachmentVideoCompressHint": "壓縮一份視頻的副本",
|
||||
"attachmentVideoCompressHintDescription": "你想上傳壓縮視頻 {} 的副本嗎?它將幫助你的觀眾快速預覽視頻,並且他們仍然可以觀看原始視頻。這將會在在你的設備上處理視頻,所以需要一些時間,所以請耐心等待。",
|
||||
"attachmentCompressQuality": "壓縮質量",
|
||||
"attachmentCompressQualityHighest": "最高",
|
||||
"attachmentCompressQualityDefault": "默認",
|
||||
"attachmentCompressQualityMedium": "中等",
|
||||
"attachmentCompressQualityLow": "低",
|
||||
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。",
|
||||
"attachmentUploaded": "已上傳",
|
||||
"attachmentPending": "未上傳",
|
||||
"attachmentCopyCompressed": "有壓縮副本",
|
||||
"attachmentGotBoosted": "有加速傳遞",
|
||||
"attachmentBoost": "加速包",
|
||||
"attachmentCreateBoost": "加速傳遞",
|
||||
"attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。",
|
||||
"attachmentDestinationRegion": "目標節點",
|
||||
"attachmentDestinationRegionAPAC": "亞太地區",
|
||||
"attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波",
|
||||
"attachmentDestinationRegionHKG": "香港",
|
||||
"notification": "通知",
|
||||
"notificationUnreadCount": {
|
||||
"zero": "無未讀通知",
|
||||
@@ -384,6 +446,9 @@
|
||||
"dailyCheckNegativeHint6": "出門",
|
||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||
"celebrateBirthday": "生日快樂,{}!",
|
||||
"celebrateLunarNewYear": "春節快樂,{}!",
|
||||
"celebrateMidAutumn": "中秋節快樂,{}!",
|
||||
"celebrateDragonBoat": "端午節快樂,{}!",
|
||||
"celebrateMerryXmas": "聖誕快樂,{}!",
|
||||
"celebrateNewYear": "新年快樂,{}!",
|
||||
"celebrateValentineDay": "今天是情人節,{}!",
|
||||
@@ -393,6 +458,9 @@
|
||||
"celebrateFatherDay": "今天是父親節,{}。",
|
||||
"celebrateHalloween": "快樂在聖誕節,{}!",
|
||||
"celebrateThanksgiving": "今天是感恩節,{}!",
|
||||
"pendingLunarNewYear": "{} 過春節",
|
||||
"pendingMidAutumn": "{} 過中秋節",
|
||||
"pendingDragonBoat": "{} 過端午節",
|
||||
"pendingBirthday": "{} 過生日",
|
||||
"pendingMerryXmas": "{} 過聖誕節",
|
||||
"pendingNewYear": "{} 跨年",
|
||||
@@ -436,6 +504,7 @@
|
||||
"accountJoinedAt": "加入於 {}",
|
||||
"accountBirthday": "出生於 {}",
|
||||
"accountBadge": "徽章",
|
||||
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||
"badgeSiteMigration": "Solar Network 原住民",
|
||||
"accountStatus": "狀態",
|
||||
@@ -444,6 +513,7 @@
|
||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||
"postArticle": "Solar Network 上的文章",
|
||||
"postStory": "Solar Network 上的故事",
|
||||
"postLocalDraftRestored": "從本地恢復草稿",
|
||||
"articleWrittenAt": "發表於 {}",
|
||||
"articleEditedAt": "編輯於 {}",
|
||||
"attachmentSaved": "已保存到相冊",
|
||||
@@ -484,11 +554,15 @@
|
||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖圖",
|
||||
"postGetInsight": "獲取見解",
|
||||
"postGetInsightTitle": "AI 見解",
|
||||
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||
"shareIntentPostStory": "發佈動態",
|
||||
"shareIntentSendChannel": "分享到聊天頻道",
|
||||
"updateAvailable": "檢測到更新可用",
|
||||
"updateOngoing": "正在更新,請稍後……",
|
||||
"custom": "自定義",
|
||||
@@ -501,6 +575,7 @@
|
||||
"colorSchemeWhite": "白色",
|
||||
"colorSchemeBlack": "黑色",
|
||||
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
||||
"postFeaturedComment": "精選評論",
|
||||
"postCategoryTechnology": "技術",
|
||||
"postCategoryGaming": "遊戲",
|
||||
"postCategoryLife": "生活",
|
||||
@@ -512,11 +587,25 @@
|
||||
"postCategoryLiterature": "文學",
|
||||
"postCategoryFunny": "搞笑",
|
||||
"postCategoryUncategorized": "未分類",
|
||||
"waitingForUpload": "等待上傳",
|
||||
"attachmentCompressQuality": "壓縮質量",
|
||||
"attachmentCompressQualityHighest": "最高",
|
||||
"attachmentCompressQualityDefault": "默認",
|
||||
"attachmentCompressQualityMedium": "中等",
|
||||
"attachmentCompressQualityLow": "低",
|
||||
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。"
|
||||
"newsAllSources": "所有新聞",
|
||||
"newsReadingProviderSwap": "切換",
|
||||
"newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章",
|
||||
"newsReadingFromOriginal": "你正在閱讀原始文章",
|
||||
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
|
||||
"newsToday": "快訊",
|
||||
"totpPostSetup": "還有一件事",
|
||||
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。",
|
||||
"totpNeverShare": "永遠不要分享這個 QR Code",
|
||||
"needHelp": "需要幫助?",
|
||||
"needHelpLaunch": "查看我們的山羊維基!",
|
||||
"walletCreate": "創建錢包",
|
||||
"walletCreateSubtitle": "創建於一個錢包來開始使用源點。",
|
||||
"walletCreatePassword": "在下方設置你的付款密碼",
|
||||
"walletCurrencyShort": "源點",
|
||||
"walletCurrency": {
|
||||
"one": "{} 源點",
|
||||
"other": "{} 源點"
|
||||
},
|
||||
"aiThinkingProcess": "AI 思考過程",
|
||||
"accountSettingsApplied": "帳號設置已應用。"
|
||||
}
|
||||
|
104
ios/Podfile.lock
104
ios/Podfile.lock
@@ -43,58 +43,58 @@ PODS:
|
||||
- Flutter
|
||||
- file_saver (0.0.1):
|
||||
- Flutter
|
||||
- Firebase/Analytics (11.4.0):
|
||||
- Firebase/Analytics (11.6.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (11.4.0):
|
||||
- Firebase/Core (11.6.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 11.4.0)
|
||||
- Firebase/CoreOnly (11.4.0):
|
||||
- FirebaseCore (= 11.4.0)
|
||||
- Firebase/Messaging (11.4.0):
|
||||
- FirebaseAnalytics (~> 11.6.0)
|
||||
- Firebase/CoreOnly (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- Firebase/Messaging (11.6.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.4.0)
|
||||
- firebase_analytics (11.3.6):
|
||||
- Firebase/Analytics (= 11.4.0)
|
||||
- FirebaseMessaging (~> 11.6.0)
|
||||
- firebase_analytics (11.4.1):
|
||||
- Firebase/Analytics (= 11.6.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_core (3.9.0):
|
||||
- Firebase/CoreOnly (= 11.4.0)
|
||||
- firebase_core (3.10.1):
|
||||
- Firebase/CoreOnly (= 11.6.0)
|
||||
- Flutter
|
||||
- firebase_messaging (15.1.6):
|
||||
- Firebase/Messaging (= 11.4.0)
|
||||
- firebase_messaging (15.2.1):
|
||||
- Firebase/Messaging (= 11.6.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseAnalytics (11.4.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.4.0)
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseAnalytics (11.6.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.4.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleAppMeasurement (= 11.4.0)
|
||||
- GoogleAppMeasurement (= 11.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (11.4.0):
|
||||
- FirebaseCoreInternal (~> 11.0)
|
||||
- FirebaseCore (11.6.0):
|
||||
- FirebaseCoreInternal (~> 11.6.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- FirebaseCoreInternal (11.6.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseInstallations (11.4.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseInstallations (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (11.4.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseMessaging (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@@ -105,32 +105,39 @@ PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_app_update (0.0.1):
|
||||
- Flutter
|
||||
- flutter_inappwebview_ios (0.0.1):
|
||||
- Flutter
|
||||
- flutter_inappwebview_ios/Core (= 0.0.1)
|
||||
- OrderedSet (~> 6.0.3)
|
||||
- flutter_inappwebview_ios/Core (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (~> 6.0.3)
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- flutter_webrtc (0.12.2):
|
||||
- flutter_webrtc (0.12.6):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleAppMeasurement (11.4.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.4.0)
|
||||
- GoogleAppMeasurement (11.6.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.4.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.6.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
@@ -173,7 +180,7 @@ PODS:
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- Kingfisher (8.1.3)
|
||||
- livekit_client (2.3.3):
|
||||
- livekit_client (2.3.5):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@@ -188,6 +195,7 @@ PODS:
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- OrderedSet (6.0.3)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- pasteboard (0.0.1):
|
||||
@@ -239,6 +247,7 @@ DEPENDENCIES:
|
||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
|
||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||
@@ -282,6 +291,7 @@ SPEC REPOS:
|
||||
- GoogleUtilities
|
||||
- Kingfisher
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- SAMKeychain
|
||||
- SDWebImage
|
||||
@@ -309,6 +319,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
flutter_app_update:
|
||||
:path: ".symlinks/plugins/flutter_app_update/ios"
|
||||
flutter_inappwebview_ios:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_udid:
|
||||
@@ -367,35 +379,37 @@ SPEC CHECKSUMS:
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
|
||||
firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75
|
||||
firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10
|
||||
firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9
|
||||
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
|
||||
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
|
||||
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
|
||||
firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e
|
||||
firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b
|
||||
firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e
|
||||
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
|
||||
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
|
||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
||||
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
|
||||
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
|
||||
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
|
||||
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
|
||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||
flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563
|
||||
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
|
||||
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
|
||||
livekit_client: 02cf2cc4357a655af12ccee70ff5596ae4e6feef
|
||||
livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
@@ -11,6 +12,7 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/websocket.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChatMessageController extends ChangeNotifier {
|
||||
@@ -36,8 +38,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
|
||||
int? messageTotal;
|
||||
|
||||
bool get isAllLoaded =>
|
||||
messageTotal != null && messages.length >= messageTotal!;
|
||||
bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!;
|
||||
|
||||
String? _boxKey;
|
||||
SnChannel? channel;
|
||||
@@ -50,8 +51,10 @@ class ChatMessageController extends ChangeNotifier {
|
||||
/// Stored as a list of nonce to provide the loading state
|
||||
final List<String> unconfirmedMessages = List.empty(growable: true);
|
||||
|
||||
Box<SnChatMessage>? get _box =>
|
||||
(_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
|
||||
Box<SnChatMessage>? get _box => (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
|
||||
|
||||
final List<SnChannelMember> typingMembers = List.empty(growable: true);
|
||||
final Map<int, Timer> typingInactiveTimer = {};
|
||||
|
||||
Future<void> initialize(SnChannel chan) async {
|
||||
channel = chan;
|
||||
@@ -71,6 +74,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
_wsSubscription = _ws.stream.stream.listen((event) {
|
||||
switch (event.method) {
|
||||
case 'events.new':
|
||||
if (event.payload?['channel_id'] != channel?.id) break;
|
||||
final payload = SnChatMessage.fromJson(event.payload!);
|
||||
_addMessage(payload);
|
||||
break;
|
||||
@@ -78,22 +82,16 @@ class ChatMessageController extends ChangeNotifier {
|
||||
if (event.payload?['channel_id'] != channel?.id) break;
|
||||
final member = SnChannelMember.fromJson(event.payload!['member']);
|
||||
if (member.id == profile?.id) break;
|
||||
// TODO impl typing users
|
||||
// if (!_typingUsers.any((x) => x.id == member.id)) {
|
||||
// setState(() {
|
||||
// _typingUsers.add(member);
|
||||
// });
|
||||
// }
|
||||
// _typingInactiveTimer[member.id]?.cancel();
|
||||
// _typingInactiveTimer[member.id] = Timer(
|
||||
// const Duration(seconds: 3),
|
||||
// () {
|
||||
// setState(() {
|
||||
// _typingUsers.removeWhere((x) => x.id == member.id);
|
||||
// _typingInactiveTimer.remove(member.id);
|
||||
// });
|
||||
// },
|
||||
// );
|
||||
if (!typingMembers.any((x) => x.id == member.id)) {
|
||||
typingMembers.add(member);
|
||||
notifyListeners();
|
||||
}
|
||||
typingInactiveTimer[member.id]?.cancel();
|
||||
typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () {
|
||||
typingMembers.removeWhere((x) => x.id == member.id);
|
||||
typingInactiveTimer.remove(member.id);
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -101,6 +99,35 @@ class ChatMessageController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Timer? _typingNotifyTimer;
|
||||
bool _typingStatus = false;
|
||||
|
||||
Future<void> _sendTypingStatusPackage() async {
|
||||
_ws.conn?.sink.add(jsonEncode(
|
||||
WebSocketPackage(
|
||||
method: 'status.typing',
|
||||
endpoint: 'im',
|
||||
payload: {
|
||||
'channel_id': channel!.id,
|
||||
},
|
||||
).toJson(),
|
||||
));
|
||||
}
|
||||
|
||||
void pingTypingStatus() {
|
||||
if (!_typingStatus) {
|
||||
_sendTypingStatusPackage();
|
||||
_typingStatus = true;
|
||||
}
|
||||
|
||||
if (_typingNotifyTimer == null || !_typingNotifyTimer!.isActive) {
|
||||
_typingNotifyTimer?.cancel();
|
||||
_typingNotifyTimer = Timer(const Duration(milliseconds: 1850), () {
|
||||
_typingStatus = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
|
||||
if (_box == null) return;
|
||||
await _box!.putAll({
|
||||
@@ -167,8 +194,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
switch (message.type) {
|
||||
case 'messages.edit':
|
||||
if (message.relatedEventId != null) {
|
||||
final idx =
|
||||
messages.indexWhere((x) => x.id == message.relatedEventId);
|
||||
final idx = messages.indexWhere((x) => x.id == message.relatedEventId);
|
||||
if (idx != -1) {
|
||||
final newBody = message.body;
|
||||
newBody.remove('related_event');
|
||||
@@ -207,8 +233,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
'algorithm': 'plain',
|
||||
if (quoteId != null) 'quote_event': quoteId,
|
||||
if (relatedId != null) 'related_event': relatedId,
|
||||
if (attachments != null && attachments.isNotEmpty)
|
||||
'attachments': attachments,
|
||||
if (attachments != null && attachments.isNotEmpty) 'attachments': attachments,
|
||||
};
|
||||
|
||||
// Mock the message locally
|
||||
@@ -305,8 +330,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
|
||||
if (out == null) {
|
||||
try {
|
||||
final resp = await _sn.client
|
||||
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
||||
final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
||||
out = SnChatMessage.fromJson(resp.data);
|
||||
_saveMessageToLocal([out]);
|
||||
} catch (_) {
|
||||
@@ -341,9 +365,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
bool forceRemote = false,
|
||||
}) async {
|
||||
late List<SnChatMessage> out;
|
||||
if (_box != null &&
|
||||
(_box!.length >= take + offset || forceLocal) &&
|
||||
!forceRemote) {
|
||||
if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) {
|
||||
out = _box!.keys
|
||||
.toList()
|
||||
.cast<int>()
|
||||
@@ -386,8 +408,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
quoteEvent: quoteEvent,
|
||||
attachments: attachments
|
||||
.where(
|
||||
(ele) =>
|
||||
out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
||||
(ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
@@ -395,10 +416,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Preload sender accounts
|
||||
final accountId = out
|
||||
.where((ele) => ele.sender.accountId >= 0)
|
||||
.map((ele) => ele.sender.accountId)
|
||||
.toSet();
|
||||
final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet();
|
||||
await _ud.listAccount(accountId);
|
||||
|
||||
return out;
|
||||
|
@@ -1,12 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
@@ -14,6 +19,7 @@ import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
class PostWriteMedia {
|
||||
late String name;
|
||||
@@ -98,7 +104,7 @@ class PostWriteMedia {
|
||||
if (attachment != null) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
||||
if (width != null && height != null) {
|
||||
if (width != null && height != null && !kIsWeb) {
|
||||
return ResizeImage(
|
||||
provider,
|
||||
width: width,
|
||||
@@ -148,9 +154,22 @@ class PostWriteController extends ChangeNotifier {
|
||||
final TextEditingController descriptionController = TextEditingController();
|
||||
final TextEditingController aliasController = TextEditingController();
|
||||
|
||||
PostWriteController() {
|
||||
titleController.addListener(() => notifyListeners());
|
||||
descriptionController.addListener(() => notifyListeners());
|
||||
bool _temporarySaveActive = false;
|
||||
|
||||
PostWriteController({bool doLoadFromTemporary = true}) {
|
||||
_temporarySaveActive = doLoadFromTemporary;
|
||||
titleController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
});
|
||||
descriptionController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
});
|
||||
contentController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
});
|
||||
if (doLoadFromTemporary) _temporaryLoad();
|
||||
}
|
||||
|
||||
String mode = kTitleMap.keys.first;
|
||||
@@ -197,11 +216,11 @@ class PostWriteController extends ChangeNotifier {
|
||||
aliasController.text = post.alias ?? '';
|
||||
publishedAt = post.publishedAt;
|
||||
publishedUntil = post.publishedUntil;
|
||||
visibleUsers = List.from(post.visibleUsersList ?? []);
|
||||
invisibleUsers = List.from(post.invisibleUsersList ?? []);
|
||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
|
||||
visibility = post.visibility;
|
||||
tags = List.from(post.tags.map((ele) => ele.alias));
|
||||
categories = List.from(post.categories.map((ele) => ele.alias));
|
||||
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
||||
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||
|
||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||
@@ -229,7 +248,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media) async {
|
||||
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media,
|
||||
{bool isCompressed = false}) async {
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
|
||||
final place = await attach.chunkedUploadInitialize(
|
||||
@@ -240,19 +260,138 @@ class PostWriteController extends ChangeNotifier {
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||
);
|
||||
|
||||
final item = await attach.chunkedUploadParts(
|
||||
var item = await attach.chunkedUploadParts(
|
||||
media.toFile()!,
|
||||
place.$1,
|
||||
place.$2,
|
||||
onProgress: (progress) {
|
||||
progress = progress;
|
||||
analyzeNow: media.type == SnMediaType.image,
|
||||
onProgress: (value) {
|
||||
progress = value;
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
|
||||
try {
|
||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||
if (compressedAttachment != null) {
|
||||
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||
}
|
||||
} catch (err) {
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
Future<SnAttachment?> _tryCompressVideoCopy(BuildContext context, PostWriteMedia media) async {
|
||||
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) return null;
|
||||
if (media.type != SnMediaType.video) return null;
|
||||
if (media.file == null) return null;
|
||||
if (VideoCompress.isCompressing) return null;
|
||||
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'attachmentVideoCompressHint'.tr(),
|
||||
'attachmentVideoCompressHintDescription'.tr(args: [media.file!.name]),
|
||||
);
|
||||
if (!confirm) return null;
|
||||
|
||||
progress = null;
|
||||
notifyListeners();
|
||||
|
||||
final mediaInfo = await VideoCompress.compressVideo(
|
||||
media.file!.path,
|
||||
quality: VideoQuality.LowQuality,
|
||||
frameRate: 30,
|
||||
deleteOrigin: false,
|
||||
);
|
||||
if (mediaInfo == null) return null;
|
||||
if (!context.mounted) return null;
|
||||
|
||||
final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!));
|
||||
final compressedAttachment = await _uploadAttachment(context, compressedMedia, isCompressed: true);
|
||||
|
||||
return compressedAttachment;
|
||||
}
|
||||
|
||||
static const kTemporaryStorageKey = 'int_draft_post';
|
||||
|
||||
Timer? _temporarySaveTimer;
|
||||
|
||||
void _temporaryPlanSave() {
|
||||
if (!_temporarySaveActive) return;
|
||||
_temporarySaveTimer?.cancel();
|
||||
_temporarySaveTimer = Timer(const Duration(seconds: 1), () {
|
||||
_temporarySave();
|
||||
log("[PostWriter] Temporary save saved.");
|
||||
});
|
||||
}
|
||||
|
||||
void _temporarySave() {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
if (titleController.text.isEmpty &&
|
||||
descriptionController.text.isEmpty &&
|
||||
contentController.text.isEmpty &&
|
||||
thumbnail == null &&
|
||||
attachments.isEmpty) {
|
||||
prefs.remove(kTemporaryStorageKey);
|
||||
return;
|
||||
}
|
||||
|
||||
prefs.setString(
|
||||
kTemporaryStorageKey,
|
||||
jsonEncode({
|
||||
'publisher': publisher,
|
||||
'content': contentController.text,
|
||||
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
||||
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
|
||||
'attachments':
|
||||
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
|
||||
'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
|
||||
'categories': categories.map((ele) => {'alias': ele}).toList(growable: true),
|
||||
'visibility': visibility,
|
||||
'visible_users_list': visibleUsers,
|
||||
'invisible_users_list': invisibleUsers,
|
||||
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
bool temporaryRestored = false;
|
||||
|
||||
void _temporaryLoad() {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
final raw = prefs.getString(kTemporaryStorageKey);
|
||||
if (raw == null) return;
|
||||
final data = jsonDecode(raw);
|
||||
contentController.text = data['content'];
|
||||
aliasController.text = data['alias'] ?? '';
|
||||
titleController.text = data['title'] ?? '';
|
||||
descriptionController.text = data['description'] ?? '';
|
||||
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||
attachments
|
||||
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
|
||||
tags = List.from(data['tags'].map((ele) => ele['alias']));
|
||||
categories = List.from(data['categories'].map((ele) => ele['alias']));
|
||||
visibility = data['visibility'];
|
||||
visibleUsers = List.from(data['visible_users_list'] ?? []);
|
||||
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
|
||||
if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
||||
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
||||
temporaryRestored = true;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
|
||||
if (isBusy) return;
|
||||
|
||||
@@ -297,17 +436,28 @@ class PostWriteController extends ChangeNotifier {
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||
);
|
||||
|
||||
final item = await attach.chunkedUploadParts(
|
||||
var item = await attach.chunkedUploadParts(
|
||||
media.toFile()!,
|
||||
place.$1,
|
||||
place.$2,
|
||||
onProgress: (progress) {
|
||||
onProgress: (value) {
|
||||
// Calculate overall progress for attachments
|
||||
progress = math.max(((i + progress) / attachments.length) * kAttachmentProgressWeight, progress);
|
||||
progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
if (context.mounted) {
|
||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||
if (compressedAttachment != null) {
|
||||
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
}
|
||||
|
||||
progress = (i + 1) / attachments.length * kAttachmentProgressWeight;
|
||||
attachments[i] = PostWriteMedia(item);
|
||||
notifyListeners();
|
||||
@@ -361,6 +511,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
method: editingPost != null ? 'PUT' : 'POST',
|
||||
),
|
||||
);
|
||||
reset();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@@ -409,73 +560,88 @@ class PostWriteController extends ChangeNotifier {
|
||||
|
||||
void setPublisher(SnPublisher? item) {
|
||||
publisher = item;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPublishedAt(DateTime? value) {
|
||||
publishedAt = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPublishedUntil(DateTime? value) {
|
||||
publishedUntil = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setTags(List<String> value) {
|
||||
tags = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setCategories(List<String> value) {
|
||||
categories = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVisibility(int value) {
|
||||
visibility = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVisibleUsers(List<int> value) {
|
||||
visibleUsers = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setInvisibleUsers(List<int> value) {
|
||||
invisibleUsers = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setProgress(double? value) {
|
||||
progress = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setIsBusy(bool value) {
|
||||
isBusy = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setMode(String value) {
|
||||
mode = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
publishedAt = null;
|
||||
publishedUntil = null;
|
||||
thumbnail = null;
|
||||
visibility = 0;
|
||||
titleController.clear();
|
||||
descriptionController.clear();
|
||||
contentController.clear();
|
||||
aliasController.clear();
|
||||
tags.clear();
|
||||
categories.clear();
|
||||
attachments.clear();
|
||||
tags = List.empty(growable: true);
|
||||
categories = List.empty(growable: true);
|
||||
attachments = List.empty(growable: true);
|
||||
editingPost = null;
|
||||
replyingPost = null;
|
||||
repostingPost = null;
|
||||
mode = kTitleMap.keys.first;
|
||||
temporaryRestored = false;
|
||||
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
136
lib/main.dart
136
lib/main.dart
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:croppy/croppy.dart';
|
||||
@@ -10,7 +11,7 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -18,7 +19,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/firebase_options.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
@@ -30,6 +30,7 @@ import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/relationship.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_sticker.dart';
|
||||
import 'package:surface/providers/special_day.dart';
|
||||
import 'package:surface/providers/theme.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
@@ -41,7 +42,7 @@ import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/version_label.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
@@ -144,6 +145,7 @@ class SolianApp extends StatelessWidget {
|
||||
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
||||
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
||||
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
|
||||
Provider(create: (ctx) => SnStickerProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
||||
@@ -207,9 +209,7 @@ class _AppSplashScreen extends StatefulWidget {
|
||||
State<_AppSplashScreen> createState() => _AppSplashScreenState();
|
||||
}
|
||||
|
||||
class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
bool _isReady = false;
|
||||
|
||||
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
void _tryRequestRating() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.containsKey('first_boot_time')) {
|
||||
@@ -261,6 +261,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
|
||||
Future<void> _initialize() async {
|
||||
try {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
cfg.calcDrawerSize(context);
|
||||
});
|
||||
final home = context.read<HomeWidgetProvider>();
|
||||
await home.initialize();
|
||||
if (!mounted) return;
|
||||
@@ -278,12 +282,14 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
await ws.tryConnect();
|
||||
if (!mounted) return;
|
||||
final notify = context.read<NotificationProvider>();
|
||||
notify.listen();
|
||||
await notify.registerPushNotifications();
|
||||
if (!mounted) return;
|
||||
final sticker = context.read<SnStickerProvider>();
|
||||
await sticker.listStickerEagerly();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
await context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isReady = true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,9 +297,45 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
await widgetUpdateRandomPost();
|
||||
}
|
||||
|
||||
Future<void> _trayInitialization() async {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||
|
||||
final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png';
|
||||
final appVersion = await PackageInfo.fromPlatform();
|
||||
|
||||
trayManager.addListener(this);
|
||||
await trayManager.setIcon(icon);
|
||||
|
||||
Menu menu = Menu(
|
||||
items: [
|
||||
MenuItem(
|
||||
key: 'version_label',
|
||||
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
|
||||
disabled: true,
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
key: 'exit',
|
||||
label: 'trayMenuExit'.tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
await trayManager.setContextMenu(menu);
|
||||
}
|
||||
|
||||
AppLifecycleListener? _appLifecycleListener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
||||
_appLifecycleListener = AppLifecycleListener(
|
||||
onExitRequested: _onExitRequested,
|
||||
);
|
||||
}
|
||||
|
||||
_trayInitialization();
|
||||
_initialize().then((_) {
|
||||
_postInitialization();
|
||||
_tryRequestRating();
|
||||
@@ -301,34 +343,58 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_isReady) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 180),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (MediaQuery.of(context).platformBrightness == Brightness.dark)
|
||||
Image.asset("assets/icon/icon-dark.png", width: 64, height: 64)
|
||||
else
|
||||
Image.asset("assets/icon/icon.png", width: 64, height: 64),
|
||||
const Gap(6),
|
||||
LinearProgressIndicator(
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
const Gap(20),
|
||||
Text('appInitializing'.tr(), textAlign: TextAlign.center),
|
||||
AppVersionLabel(),
|
||||
],
|
||||
),
|
||||
).center(),
|
||||
);
|
||||
Future<AppExitResponse> _onExitRequested() async {
|
||||
appWindow.hide();
|
||||
return AppExitResponse.cancel;
|
||||
}
|
||||
|
||||
return widget.child;
|
||||
@override
|
||||
void onTrayIconMouseDown() {
|
||||
if (Platform.isWindows) {
|
||||
appWindow.show();
|
||||
} else {
|
||||
trayManager.popUpContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconRightMouseDown() {
|
||||
if (Platform.isWindows) {
|
||||
trayManager.popUpContextMenu();
|
||||
} else {
|
||||
appWindow.show();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayMenuItemClick(MenuItem menuItem) {
|
||||
switch (menuItem.key) {
|
||||
case 'exit':
|
||||
_appLifecycleListener?.dispose();
|
||||
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) trayManager.removeListener(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
return NotificationListener<SizeChangedLayoutNotification>(
|
||||
onNotification: (notification) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
cfg.calcDrawerSize(context);
|
||||
});
|
||||
return false;
|
||||
},
|
||||
child: SizeChangedLayoutNotifier(
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/providers/widget.dart';
|
||||
|
||||
@@ -12,6 +13,10 @@ const kNetworkServerStoreKey = 'app_server_url';
|
||||
const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
||||
const kAppBackgroundStoreKey = 'app_has_background';
|
||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
||||
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
|
||||
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
|
||||
const kAppExpandPostLink = 'app_expand_post_link';
|
||||
const kAppExpandChatLink = 'app_expand_chat_link';
|
||||
|
||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||
'settingsImageQualityLowest': FilterQuality.none,
|
||||
@@ -33,6 +38,32 @@ class ConfigProvider extends ChangeNotifier {
|
||||
prefs = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
bool drawerIsCollapsed = false;
|
||||
bool drawerIsExpanded = false;
|
||||
|
||||
void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
|
||||
bool newDrawerIsCollapsed = false;
|
||||
bool newDrawerIsExpanded = false;
|
||||
if (withMediaQuery) {
|
||||
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450;
|
||||
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451;
|
||||
} else {
|
||||
final rpb = ResponsiveBreakpoints.of(context);
|
||||
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
|
||||
newDrawerIsExpanded = rpb.largerThan(TABLET)
|
||||
? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
|
||||
? false
|
||||
: true
|
||||
: false;
|
||||
}
|
||||
|
||||
if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
|
||||
drawerIsExpanded = newDrawerIsExpanded;
|
||||
drawerIsCollapsed = newDrawerIsCollapsed;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
FilterQuality get imageQuality {
|
||||
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
|
||||
}
|
||||
|
@@ -58,6 +58,11 @@ class NavigationProvider extends ChangeNotifier {
|
||||
screen: 'realm',
|
||||
label: 'screenRealm',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
|
||||
screen: 'news',
|
||||
label: 'screenNews',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
||||
screen: 'album',
|
||||
@@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier {
|
||||
|
||||
List<AppNavDestination> destinations = [];
|
||||
|
||||
int get pinnedDestinationCount =>
|
||||
destinations.where((ele) => ele.isPinned).length;
|
||||
int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
|
||||
|
||||
NavigationProvider() {
|
||||
buildDestinations(kDefaultPinnedDestination);
|
||||
@@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
bool isIndexInRange(int min, int max) {
|
||||
return _currentIndex != null &&
|
||||
_currentIndex! >= min &&
|
||||
_currentIndex! < max;
|
||||
return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
|
||||
}
|
||||
|
||||
void autoDetectIndex(GoRouter? state) {
|
||||
if (state == null) return;
|
||||
final idx = destinations.indexWhere(
|
||||
(ele) =>
|
||||
ele.screen ==
|
||||
state.routerDelegate.currentConfiguration.last.route.name,
|
||||
(ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
|
||||
);
|
||||
_currentIndex = idx == -1 ? null : idx;
|
||||
notifyListeners();
|
||||
|
@@ -4,18 +4,27 @@ import 'dart:io';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/notification.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
|
||||
class NotificationProvider extends ChangeNotifier {
|
||||
late final SnNetworkProvider _sn;
|
||||
late final UserProvider _ua;
|
||||
late final WebSocketProvider _ws;
|
||||
late final ConfigProvider _cfg;
|
||||
|
||||
NotificationProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
_ua = context.read<UserProvider>();
|
||||
_ws = context.read<WebSocketProvider>();
|
||||
_cfg = context.read<ConfigProvider>();
|
||||
}
|
||||
|
||||
Future<void> registerPushNotifications() async {
|
||||
@@ -62,4 +71,42 @@ class NotificationProvider extends ChangeNotifier {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
int showingCount = 0;
|
||||
List<SnNotification> notifications = List.empty(growable: true);
|
||||
|
||||
void listen() {
|
||||
_ws.stream.stream.listen((event) {
|
||||
if (event.method == 'notifications.new') {
|
||||
final notification = SnNotification.fromJson(event.payload!);
|
||||
if (showingCount < 0) showingCount = 0;
|
||||
showingCount++;
|
||||
notifications.add(notification);
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (showingCount >= 0) showingCount--;
|
||||
notifyListeners();
|
||||
});
|
||||
notifyListeners();
|
||||
updateTray();
|
||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||
if (doHaptic) HapticFeedback.mediumImpact();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void updateTray() {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||
if (notifications.isEmpty) {
|
||||
trayManager.setTitle('');
|
||||
} else {
|
||||
trayManager.setTitle(' ${notifications.length.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
showingCount = 0;
|
||||
notifications.clear();
|
||||
updateTray();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@@ -86,6 +86,7 @@ class SnAttachmentProvider {
|
||||
Map<String, dynamic>? metadata, {
|
||||
String? mimetype,
|
||||
Function(double progress)? onProgress,
|
||||
bool analyzeNow = false,
|
||||
}) async {
|
||||
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
||||
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
||||
@@ -108,6 +109,7 @@ class SnAttachmentProvider {
|
||||
final resp = await _sn.client.post(
|
||||
'/cgi/uc/attachments',
|
||||
data: formData,
|
||||
queryParameters: {'analyzeNow': analyzeNow},
|
||||
onSendProgress: (count, total) {
|
||||
if (onProgress != null) {
|
||||
onProgress(count / total);
|
||||
@@ -152,10 +154,12 @@ class SnAttachmentProvider {
|
||||
String rid,
|
||||
String cid, {
|
||||
Function(double progress)? onProgress,
|
||||
bool analyzeNow = false,
|
||||
}) async {
|
||||
final resp = await _sn.client.post(
|
||||
'/cgi/uc/fragments/$rid/$cid',
|
||||
data: data,
|
||||
queryParameters: {'analyzeNow': analyzeNow},
|
||||
options: Options(headers: {'Content-Type': 'application/octet-stream'}),
|
||||
onSendProgress: (count, total) {
|
||||
if (onProgress != null) {
|
||||
@@ -176,9 +180,10 @@ class SnAttachmentProvider {
|
||||
SnAttachmentFragment place,
|
||||
int chunkSize, {
|
||||
Function(double progress)? onProgress,
|
||||
bool analyzeNow = false,
|
||||
}) async {
|
||||
final Map<String, dynamic> chunks = place.fileChunks;
|
||||
var currentTask = 0;
|
||||
var completedTasks = 0;
|
||||
|
||||
final queue = Queue<Future<void>>();
|
||||
final activeTasks = <Future<void>>[];
|
||||
@@ -198,14 +203,15 @@ class SnAttachmentProvider {
|
||||
data,
|
||||
place.rid,
|
||||
entry.key,
|
||||
analyzeNow: analyzeNow,
|
||||
onProgress: (progress) {
|
||||
final overallProgress = (currentTask + progress) / chunks.length;
|
||||
final overallProgress = (completedTasks + progress) / chunks.length;
|
||||
onProgress?.call(overallProgress);
|
||||
},
|
||||
);
|
||||
|
||||
currentTask++;
|
||||
final overallProgress = currentTask / chunks.length;
|
||||
completedTasks++;
|
||||
final overallProgress = completedTasks / chunks.length;
|
||||
onProgress?.call(overallProgress);
|
||||
|
||||
if (result is SnAttachmentFragment) {
|
||||
@@ -233,19 +239,19 @@ class SnAttachmentProvider {
|
||||
}
|
||||
|
||||
Future<SnAttachment> updateOne(
|
||||
int id, {
|
||||
SnAttachment item, {
|
||||
String? alt,
|
||||
int? thumbnailId,
|
||||
int? compressedId,
|
||||
Map<String, dynamic>? metadata,
|
||||
bool? isIndexable,
|
||||
}) async {
|
||||
final resp = await _sn.client.put('/cgi/uc/attachments/$id', data: {
|
||||
'alt': alt,
|
||||
'thumbnail': thumbnailId,
|
||||
'compressed': compressedId,
|
||||
'metadata': metadata,
|
||||
'is_indexable': isIndexable,
|
||||
final resp = await _sn.client.put('/cgi/uc/attachments/${item.id}', data: {
|
||||
'alt': alt ?? item.alt,
|
||||
'thumbnail': thumbnailId ?? item.thumbnailId,
|
||||
'compressed': compressedId ?? item.compressedId,
|
||||
'metadata': metadata ?? item.usermeta,
|
||||
'is_indexable': isIndexable ?? item.isIndexable,
|
||||
});
|
||||
return SnAttachment.fromJson(resp.data);
|
||||
}
|
||||
|
74
lib/providers/sn_sticker.dart
Normal file
74
lib/providers/sn_sticker.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
|
||||
class SnStickerProvider {
|
||||
late final SnNetworkProvider _sn;
|
||||
final Map<String, SnSticker?> _cache = {};
|
||||
|
||||
final Map<int, List<SnSticker>> stickersByPack = {};
|
||||
|
||||
List<SnSticker> get stickers => _cache.values.where((ele) => ele != null).cast<SnSticker>().toList();
|
||||
|
||||
SnStickerProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
}
|
||||
|
||||
bool hasNotSticker(String alias) {
|
||||
return _cache.containsKey(alias) && _cache[alias] == null;
|
||||
}
|
||||
|
||||
void _cacheSticker(SnSticker sticker) {
|
||||
_cache['${sticker.pack.prefix}:${sticker.alias}'] = sticker;
|
||||
if (stickersByPack[sticker.pack.id] == null) stickersByPack[sticker.pack.id] = List.empty(growable: true);
|
||||
if (!stickersByPack[sticker.pack.id]!.contains(sticker)) stickersByPack[sticker.pack.id]!.add(sticker);
|
||||
}
|
||||
|
||||
Future<SnSticker?> lookupSticker(String alias) async {
|
||||
if (_cache.containsKey(alias)) {
|
||||
return _cache[alias];
|
||||
}
|
||||
|
||||
try {
|
||||
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
|
||||
final sticker = SnSticker.fromJson(resp.data);
|
||||
_cacheSticker(sticker);
|
||||
|
||||
return sticker;
|
||||
} catch (err) {
|
||||
_cache[alias] = null;
|
||||
log('[Sticker] Failed to lookup sticker $alias: $err');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> listStickerEagerly() async {
|
||||
var count = await listSticker();
|
||||
for (var page = 1; count > 0; count -= 10) {
|
||||
await listSticker(page: page);
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> listSticker({int page = 0}) async {
|
||||
try {
|
||||
final resp = await _sn.client.get('/cgi/uc/stickers', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': page * 10,
|
||||
});
|
||||
final data = resp.data;
|
||||
final stickers = List.from(data['data']).map((ele) => SnSticker.fromJson(ele));
|
||||
for (final sticker in stickers) {
|
||||
_cacheSticker(sticker);
|
||||
}
|
||||
return data['count'] as int;
|
||||
} catch (err) {
|
||||
log('[Sticker] Failed to list stickers: $err');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,9 +3,12 @@ import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
|
||||
// Stored as key: month, day
|
||||
const Map<String, (int, int)> kSpecialDays = {
|
||||
final Map<String, (int, int)> kSpecialDays = {
|
||||
// Birthday is dynamically generated according to the user's profile
|
||||
'NewYear': (1, 1),
|
||||
'LunarNewYear': (lunarToGregorian(null, 1, 1).month, lunarToGregorian(null, 1, 1).day),
|
||||
'MidAutumn': (lunarToGregorian(null, 8, 15).month, lunarToGregorian(null, 8, 15).day),
|
||||
'DragonBoat': (lunarToGregorian(null, 5, 5).month, lunarToGregorian(null, 5, 5).day),
|
||||
'ValentineDay': (2, 14),
|
||||
'LaborDay': (5, 1),
|
||||
'MotherDay': (5, 11),
|
||||
@@ -19,6 +22,9 @@ const Map<String, (int, int)> kSpecialDays = {
|
||||
const Map<String, String> kSpecialDaysSymbol = {
|
||||
'Birthday': '🎂',
|
||||
'NewYear': '🎉',
|
||||
'LunarNewYear': '🎉',
|
||||
'MidAutumn': '🥮',
|
||||
'DragonBoat': '🐲',
|
||||
'MerryXmas': '🎄',
|
||||
'ValentineDay': '💑',
|
||||
'LaborDay': '🏋️',
|
||||
@@ -134,3 +140,45 @@ class SpecialDayProvider {
|
||||
return (elapsedDuration / totalDuration).clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
final Map<int, LunarYear> lunarYearData = {
|
||||
2025: LunarYear(
|
||||
startDate: DateTime(2025, 1, 29),
|
||||
months: [29, 30, 30, 29, 30, 29, 29, 30, 30, 29, 30, 29],
|
||||
leapMonth: 0,
|
||||
),
|
||||
};
|
||||
|
||||
class LunarYear {
|
||||
final DateTime startDate;
|
||||
final List<int> months;
|
||||
final int leapMonth;
|
||||
|
||||
LunarYear({required this.startDate, required this.months, required this.leapMonth});
|
||||
}
|
||||
|
||||
DateTime lunarToGregorian(int? year, int month, int day, {bool isLeapMonth = false}) {
|
||||
year = year ?? DateTime.now().year;
|
||||
final lunarYear = lunarYearData[year];
|
||||
if (lunarYear == null) {
|
||||
throw Exception('Lunar data for year $year not found');
|
||||
}
|
||||
|
||||
int leapMonth = lunarYear.leapMonth;
|
||||
if (isLeapMonth && (leapMonth == 0 || leapMonth != month)) {
|
||||
throw Exception('Invalid leap month for year $year');
|
||||
}
|
||||
|
||||
int daysFromStart = 0;
|
||||
for (int i = 0; i < month - 1; i++) {
|
||||
daysFromStart += lunarYear.months[i];
|
||||
}
|
||||
|
||||
if (isLeapMonth) {
|
||||
daysFromStart += lunarYear.months[month - 1];
|
||||
}
|
||||
|
||||
daysFromStart += day - 1;
|
||||
|
||||
return lunarYear.startDate.add(Duration(days: daysFromStart));
|
||||
}
|
||||
|
@@ -53,4 +53,11 @@ class UserProvider extends ChangeNotifier {
|
||||
user = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setLanguage(String? value) {
|
||||
if (value == null) return;
|
||||
if (user == null) return;
|
||||
user = user!.copyWith(language: value);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@@ -33,9 +33,18 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
await connect();
|
||||
}
|
||||
|
||||
Completer<void>? _connectCompleter;
|
||||
|
||||
Future<void> connect({noRetry = false}) async {
|
||||
if(_connectCompleter != null) {
|
||||
await _connectCompleter!.future;
|
||||
_connectCompleter = null;
|
||||
}
|
||||
|
||||
_connectCompleter = Completer<void>();
|
||||
|
||||
if (!_ua.isAuthorized) return;
|
||||
if (isConnected) {
|
||||
if (isConnected || conn != null) {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
@@ -70,6 +79,7 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
} finally {
|
||||
isBusy = false;
|
||||
notifyListeners();
|
||||
_connectCompleter!.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +107,7 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
onError: (err) {
|
||||
isConnected = false;
|
||||
notifyListeners();
|
||||
Future.delayed(const Duration(seconds: 11), () => connect());
|
||||
Future.delayed(const Duration(seconds: 1), () => connect());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@@ -47,6 +47,7 @@ class HomeWidgetProvider {
|
||||
}
|
||||
|
||||
Future<void> widgetUpdateRandomPost() async {
|
||||
if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) return;
|
||||
final snc = await SnNetworkProvider.createOffContextClient();
|
||||
final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1');
|
||||
final post = SnPost.fromJson(resp.data['data'][0]);
|
||||
|
251
lib/router.dart
251
lib/router.dart
@@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:surface/screens/abuse_report.dart';
|
||||
import 'package:surface/screens/account.dart';
|
||||
import 'package:surface/screens/account/pfp.dart';
|
||||
import 'package:surface/screens/account/account_settings.dart';
|
||||
import 'package:surface/screens/account/factor_settings.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
import 'package:surface/screens/account/profile_edit.dart';
|
||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||
import 'package:surface/screens/account/publishers/publisher_new.dart';
|
||||
@@ -19,6 +21,8 @@ import 'package:surface/screens/chat/room.dart';
|
||||
import 'package:surface/screens/explore.dart';
|
||||
import 'package:surface/screens/friend.dart';
|
||||
import 'package:surface/screens/home.dart';
|
||||
import 'package:surface/screens/news/news_detail.dart';
|
||||
import 'package:surface/screens/news/news_list.dart';
|
||||
import 'package:surface/screens/notification.dart';
|
||||
import 'package:surface/screens/post/post_detail.dart';
|
||||
import 'package:surface/screens/post/post_editor.dart';
|
||||
@@ -29,37 +33,36 @@ import 'package:surface/screens/realm/manage.dart';
|
||||
import 'package:surface/screens/realm/realm_detail.dart';
|
||||
import 'package:surface/screens/settings.dart';
|
||||
import 'package:surface/screens/sharing.dart';
|
||||
import 'package:surface/screens/wallet.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/about.dart';
|
||||
import 'package:surface/widgets/navigation/app_background.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
Widget _fadeThroughTransition(
|
||||
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
final _appRoutes = [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(
|
||||
body: child,
|
||||
showAppBar: false,
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
name: 'home',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const HomeScreen(),
|
||||
),
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/posts',
|
||||
name: 'explore',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const ExploreScreen(),
|
||||
),
|
||||
builder: (context, state) => const ExploreScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/write/:mode',
|
||||
name: 'postEditor',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: PostEditorScreen(
|
||||
builder: (context, state) => PostEditorScreen(
|
||||
mode: state.pathParameters['mode']!,
|
||||
postEditId: int.tryParse(
|
||||
state.uri.queryParameters['editing'] ?? '',
|
||||
@@ -70,249 +73,187 @@ final _appRoutes = [
|
||||
postRepostId: int.tryParse(
|
||||
state.uri.queryParameters['reposting'] ?? '',
|
||||
),
|
||||
extraProps: state.extra as PostEditorExtraProps?,
|
||||
),
|
||||
extraProps: state.extra as PostEditorExtra?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/search',
|
||||
name: 'postSearch',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: PostSearchScreen(
|
||||
builder: (context, state) => PostSearchScreen(
|
||||
initialTags: state.uri.queryParameters['tags']?.split(','),
|
||||
initialCategories: state.uri.queryParameters['categories']?.split(','),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/:name',
|
||||
name: 'postPublisher',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: PostPublisherScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:slug',
|
||||
name: 'postDetail',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: PostDetailScreen(
|
||||
builder: (context, state) => PostDetailScreen(
|
||||
slug: state.pathParameters['slug']!,
|
||||
preload: state.extra as SnPost?,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
|
||||
GoRoute(
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
path: '/wallet',
|
||||
name: 'accountWallet',
|
||||
builder: (context, state) => const WalletScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'accountSettings',
|
||||
builder: (context, state) => AccountSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings/factors',
|
||||
name: 'factorSettings',
|
||||
builder: (context, state) => FactorSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => ProfileEditScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers',
|
||||
name: 'accountPublishers',
|
||||
builder: (context, state) => PublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/new',
|
||||
name: 'accountPublisherNew',
|
||||
builder: (context, state) => AccountPublisherNewScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/edit/:name',
|
||||
name: 'accountPublisherEdit',
|
||||
builder: (context, state) => AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const AccountScreen(),
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
routes: [],
|
||||
),
|
||||
]),
|
||||
GoRoute(
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const ChatScreen(),
|
||||
),
|
||||
builder: (context, state) => const ChatScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:scope/:alias',
|
||||
name: 'chatRoom',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: ChatRoomScreen(
|
||||
builder: (context, state) => ChatRoomScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
extra: state.extra as ChatRoomScreenExtra?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:scope/:alias/call',
|
||||
name: 'chatCallRoom',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: CallRoomScreen(
|
||||
builder: (context, state) => CallRoomScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:scope/:alias/detail',
|
||||
name: 'channelDetail',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: ChannelDetailScreen(
|
||||
builder: (context, state) => ChannelDetailScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manage',
|
||||
name: 'chatManage',
|
||||
pageBuilder: (context, state) => CustomTransitionPage(
|
||||
child: ChatManageScreen(
|
||||
builder: (context, state) => ChatManageScreen(
|
||||
editingChannelAlias: state.uri.queryParameters['editing'],
|
||||
),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Colors.transparent,
|
||||
child: AppBackground(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:alias',
|
||||
name: 'realmDetail',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/realm',
|
||||
name: 'realm',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
pageBuilder: (context, state) => CustomTransitionPage(
|
||||
transitionsBuilder: _fadeThroughTransition,
|
||||
child: const RealmScreen(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:alias',
|
||||
name: 'realmDetail',
|
||||
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manage',
|
||||
name: 'realmManage',
|
||||
pageBuilder: (context, state) => CustomTransitionPage(
|
||||
child: RealmManageScreen(
|
||||
builder: (context, state) => RealmManageScreen(
|
||||
editingRealmAlias: state.uri.queryParameters['editing'],
|
||||
),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Colors.transparent,
|
||||
child: AppBackground(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
|
||||
GoRoute(
|
||||
path: '/:hash',
|
||||
name: 'newsDetail',
|
||||
builder: (context, state) => NewsDetailScreen(
|
||||
hash: state.pathParameters['hash']!,
|
||||
),
|
||||
),
|
||||
]),
|
||||
GoRoute(
|
||||
path: '/album',
|
||||
name: 'album',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const AlbumScreen(),
|
||||
),
|
||||
builder: (context, state) => const AlbumScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/friend',
|
||||
name: 'friend',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const FriendScreen(),
|
||||
),
|
||||
builder: (context, state) => const FriendScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notification',
|
||||
name: 'notification',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const NotificationScreen(),
|
||||
builder: (context, state) => const NotificationScreen(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/auth/login',
|
||||
name: 'authLogin',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: LoginScreen(),
|
||||
),
|
||||
builder: (context, state) => LoginScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/register',
|
||||
name: 'authRegister',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: RegisterScreen(),
|
||||
),
|
||||
builder: (context, state) => RegisterScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/reports',
|
||||
name: 'abuseReport',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: AbuseReportScreen(),
|
||||
builder: (context, state) => AbuseReportScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: ProfileEditScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers',
|
||||
name: 'accountPublishers',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: PublisherScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers/new',
|
||||
name: 'accountPublisherNew',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: AccountPublisherNewScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers/edit/:name',
|
||||
name: 'accountPublisherEdit',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: SettingsScreen(),
|
||||
builder: (context, state) => SettingsScreen(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: AboutScreen(),
|
||||
),
|
||||
),
|
||||
],
|
||||
builder: (context, state) => AboutScreen(),
|
||||
),
|
||||
];
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
import '../types/account.dart';
|
||||
|
||||
@@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAbuseReport').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
@@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
itemCount: _reports.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return ListTile(
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
@@ -12,6 +14,8 @@ import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class AccountScreen extends StatelessWidget {
|
||||
const AccountScreen({super.key});
|
||||
@@ -19,11 +23,51 @@ class AccountScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text("screenAccount").tr(),
|
||||
title: Text(
|
||||
"screenAccount",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 5.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
).tr(),
|
||||
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
||||
? Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 56 + MediaQuery.of(context).padding.top,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 10,
|
||||
sigmaY: 10,
|
||||
),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(
|
||||
clampDouble(10 * 0.1, 0, 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.settings, fill: 1),
|
||||
@@ -82,16 +126,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
);
|
||||
}).padding(all: 20),
|
||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||
ListTile(
|
||||
title: Text('accountProfileEdit').tr(),
|
||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.contact_page),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountProfileEdit');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountPublishers').tr(),
|
||||
subtitle: Text('accountPublishersSubtitle').tr(),
|
||||
@@ -112,6 +146,36 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
GoRouter.of(context).pushNamed('abuseReport');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('factorSettings').tr(),
|
||||
subtitle: Text('factorSettingsSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.lock),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('factorSettings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountWallet').tr(),
|
||||
subtitle: Text('accountWalletSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.wallet),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountWallet');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountSettings').tr(),
|
||||
subtitle: Text('accountSettingsSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.manage_accounts),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountSettings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountLogout').tr(),
|
||||
subtitle: Text('accountLogoutSubtitle').tr(),
|
||||
@@ -133,33 +197,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
await Hive.initFlutter();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountDeletion'.tr()),
|
||||
subtitle: Text('accountDeletionActionDescription'.tr()),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.person_cancel),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'accountDeletion'.tr(),
|
||||
'accountDeletionDescription'.tr(),
|
||||
)
|
||||
.then((value) {
|
||||
if (!value || !context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
sn.client.post('/cgi/id/users/me/deletion').then((value) {
|
||||
if (context.mounted) {
|
||||
context.showSnackbar('accountDeletionSubmitted'.tr());
|
||||
}
|
||||
}).catchError((err) {
|
||||
if (context.mounted) {
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
126
lib/screens/account/account_settings.dart
Normal file
126
lib/screens/account/account_settings.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:intl/locale.dart';
|
||||
|
||||
class AccountSettingsScreen extends StatelessWidget {
|
||||
const AccountSettingsScreen({super.key});
|
||||
|
||||
Future<void> _setAccountLanguage(BuildContext context, Locale? value) async {
|
||||
if (value == null) return;
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client.put('/cgi/id/users/me/language', data: {
|
||||
'language': value.toString(),
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
context.showSnackbar('accountSettingsApplied'.tr());
|
||||
await ua.refreshUser();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountSettings').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text('settingsAccountLanguage').tr(),
|
||||
subtitle: Text('settingsAccountLanguageDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.translate),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<Locale?>(
|
||||
isExpanded: true,
|
||||
items: [
|
||||
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
|
||||
return DropdownMenuItem<Locale?>(
|
||||
value: Locale.parse(ele.toString()),
|
||||
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
|
||||
);
|
||||
}),
|
||||
],
|
||||
value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'),
|
||||
onChanged: (Locale? value) {
|
||||
if (value == null) return;
|
||||
_setAccountLanguage(context, value);
|
||||
ua.setLanguage(value.toString());
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 5,
|
||||
),
|
||||
height: 40,
|
||||
width: 160,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountProfileEdit').tr(),
|
||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.contact_page),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountProfileEdit');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountDeletion'.tr()),
|
||||
subtitle: Text('accountDeletionActionDescription'.tr()),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.person_cancel),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'accountDeletion'.tr(),
|
||||
'accountDeletionDescription'.tr(),
|
||||
)
|
||||
.then((value) {
|
||||
if (!value || !context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
sn.client.post('/cgi/id/users/me/deletion').then((value) {
|
||||
if (context.mounted) {
|
||||
context.showSnackbar('accountDeletionSubmitted'.tr());
|
||||
}
|
||||
}).catchError((err) {
|
||||
if (context.mounted) {
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
294
lib/screens/account/factor_settings.dart
Normal file
294
lib/screens/account/factor_settings.dart
Normal file
@@ -0,0 +1,294 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/auth.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
final Map<int, (String, String, IconData)> kFactorTypes = {
|
||||
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
|
||||
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
|
||||
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
|
||||
3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active),
|
||||
};
|
||||
|
||||
class FactorSettingsScreen extends StatefulWidget {
|
||||
const FactorSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FactorSettingsScreen> createState() => _FactorSettingsScreenState();
|
||||
}
|
||||
|
||||
class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
||||
bool _isBusy = false;
|
||||
List<SnAuthFactor>? _factors;
|
||||
|
||||
Future<void> _fetchFactors() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/me/factors');
|
||||
_factors = List<SnAuthFactor>.from(
|
||||
resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchFactors();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenFactorSettings').tr(),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(
|
||||
isActive: _isBusy,
|
||||
),
|
||||
ListTile(
|
||||
title: Text('authFactorAdd').tr(),
|
||||
subtitle: Text('authFactorAddSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.add),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _FactorNewDialog(
|
||||
currentlyHave: _factors!,
|
||||
),
|
||||
).then((val) {
|
||||
if (val == true) _fetchFactors();
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchFactors,
|
||||
child: ListView.builder(
|
||||
itemCount: _factors?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final ele = _factors![idx];
|
||||
return ListTile(
|
||||
title: Text(kFactorTypes[ele.type]!.$1).tr(),
|
||||
subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 12),
|
||||
leading: Icon(kFactorTypes[ele.type]!.$3),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: ele.type > 0
|
||||
? () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'authFactorDelete'.tr(),
|
||||
'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]),
|
||||
)
|
||||
.then((val) async {
|
||||
if (!val) return;
|
||||
try {
|
||||
if (!context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/id/users/me/factors/${ele.id}');
|
||||
_fetchFactors();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FactorNewDialog extends StatefulWidget {
|
||||
final List<SnAuthFactor> currentlyHave;
|
||||
|
||||
const _FactorNewDialog({required this.currentlyHave});
|
||||
|
||||
@override
|
||||
State<_FactorNewDialog> createState() => _FactorNewDialogState();
|
||||
}
|
||||
|
||||
class _FactorNewDialogState extends State<_FactorNewDialog> {
|
||||
int? _factorType;
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _submit() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.post('/cgi/id/users/me/factors', data: {
|
||||
'type': _factorType,
|
||||
});
|
||||
final factor = SnAuthFactor.fromJson(resp.data);
|
||||
if (!mounted) return;
|
||||
if (factor.type == 2) {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _FactorTotpFactorDialog(factor: factor),
|
||||
);
|
||||
}
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(true);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('authFactorAdd').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<int>(
|
||||
hint: Text(
|
||||
'Select Item',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
value: _factorType,
|
||||
items: kFactorTypes.entries.map(
|
||||
(ele) {
|
||||
final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key);
|
||||
return DropdownMenuItem<int>(
|
||||
enabled: !contains,
|
||||
value: ele.key,
|
||||
child: Text(
|
||||
ele.value.$1.tr(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
).opacity(contains ? 0.75 : 1),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
onChanged: (val) => setState(() {
|
||||
_factorType = val;
|
||||
}),
|
||||
buttonStyleData: ButtonStyleData(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => Navigator.of(context).pop(),
|
||||
child: Text('dialogCancel').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _submit(),
|
||||
child: Text('dialogConfirm').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FactorTotpFactorDialog extends StatelessWidget {
|
||||
final SnAuthFactor factor;
|
||||
|
||||
const _FactorTotpFactorDialog({required this.factor});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
'totpPostSetup',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr().width(280),
|
||||
),
|
||||
const Gap(4),
|
||||
Center(
|
||||
child: Text(
|
||||
'totpPostSetupDescription',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
).tr().width(280),
|
||||
),
|
||||
const Gap(16),
|
||||
QrImageView(
|
||||
padding: EdgeInsets.zero,
|
||||
data: factor.config!['url'],
|
||||
errorCorrectionLevel: QrErrorCorrectLevel.H,
|
||||
version: QrVersions.auto,
|
||||
size: 160,
|
||||
gapless: true,
|
||||
eyeStyle: QrEyeStyle(
|
||||
eyeShape: QrEyeShape.circle,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
dataModuleStyle: QrDataModuleStyle(
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Center(
|
||||
child: Text(
|
||||
'totpNeverShare',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
).tr().bold().width(280),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -18,6 +18,7 @@ import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class ProfileEditScreen extends StatefulWidget {
|
||||
@@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
onDateTimeChanged: (DateTime newDate) {
|
||||
setState(() {
|
||||
_birthday = newDate;
|
||||
_birthdayController.text =
|
||||
DateFormat(_kDateFormat).format(_birthday!);
|
||||
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
if (image == null) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final ImageProvider imageProvider =
|
||||
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios = place == 'banner'
|
||||
? [CropAspectRatio(width: 16, height: 7)]
|
||||
: [CropAspectRatio(width: 1, height: 1)];
|
||||
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios =
|
||||
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
|
||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||
? await showCupertinoImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
@@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final rawBytes =
|
||||
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List();
|
||||
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
||||
|
||||
try {
|
||||
final attachment = await attach.directUploadOne(
|
||||
@@ -212,7 +207,12 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return SingleChildScrollView(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountProfileEdit').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -229,8 +229,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(_banner!),
|
||||
@@ -343,6 +342,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
).padding(horizontal: padding),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -241,6 +241,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
@@ -517,6 +518,12 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
future: _getCheckInRecords(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const SizedBox.shrink();
|
||||
if (snapshot.data!.length <= 1) {
|
||||
return Text(
|
||||
'accountCheckInNoRecords',
|
||||
textAlign: TextAlign.center,
|
||||
).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8);
|
||||
}
|
||||
final records = snapshot.data!;
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -588,7 +595,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
subtitle: Text('@${ele.name}'),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
GoRouter.of(context).goNamed(
|
||||
'postPublisher',
|
||||
pathParameters: {'name': ele.name},
|
||||
);
|
@@ -18,6 +18,7 @@ import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class AccountPublisherEditScreen extends StatefulWidget {
|
||||
@@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
|
@@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class AccountPublisherNewScreen extends StatefulWidget {
|
||||
const AccountPublisherNewScreen({super.key});
|
||||
@@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountPublisherNew').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
|
@@ -10,6 +10,7 @@ import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class PublisherScreen extends StatefulWidget {
|
||||
const PublisherScreen({super.key});
|
||||
@@ -32,8 +33,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
|
||||
try {
|
||||
final resp = await sn.client.get('/cgi/co/publishers/me');
|
||||
final List<SnPublisher> out = List<SnPublisher>.from(
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
||||
final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountPublishers').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
@@ -62,9 +66,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.add_circle),
|
||||
onTap: () {
|
||||
GoRouter.of(context)
|
||||
.pushNamed('accountPublisherNew')
|
||||
.then((value) {
|
||||
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
|
||||
if (value == true) {
|
||||
_publishers.clear();
|
||||
_fetchPublishers();
|
||||
@@ -75,6 +77,9 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
const Divider(height: 1),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_publishers.clear();
|
||||
@@ -120,6 +125,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@@ -11,6 +11,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AlbumScreen extends StatefulWidget {
|
||||
@@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
|
@@ -7,17 +7,14 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/account/factor_settings.dart';
|
||||
import 'package:surface/types/auth.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import '../../providers/websocket.dart';
|
||||
|
||||
final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
|
||||
0: ('authFactorPassword'.tr(), Symbols.password, false),
|
||||
1: ('authFactorEmail'.tr(), Symbols.email, true),
|
||||
};
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@@ -35,7 +32,12 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAuthLogin').tr(),
|
||||
),
|
||||
body: Theme(
|
||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||
child: SingleChildScrollView(
|
||||
child: PageTransitionSwitcher(
|
||||
@@ -96,6 +98,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
},
|
||||
).padding(all: 24),
|
||||
).center(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -205,7 +208,9 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
autofillHints: [
|
||||
(_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode
|
||||
widget.factor!.type == 0
|
||||
? AutofillHints.password
|
||||
: AutofillHints.oneTimeCode
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
@@ -260,7 +265,8 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
||||
bool _isBusy = false;
|
||||
int? _factorPicked;
|
||||
|
||||
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
||||
|
||||
void _performGetFactorCode() async {
|
||||
if (_factorPicked == null) return;
|
||||
@@ -321,11 +327,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
||||
),
|
||||
),
|
||||
secondary: Icon(
|
||||
_factorLabelMap[x.type]?.$2 ?? Symbols.question_mark,
|
||||
kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
|
||||
),
|
||||
title: Text(
|
||||
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
|
||||
),
|
||||
kFactorTypes[x.type]?.$1 ?? 'unknown',
|
||||
).tr(),
|
||||
enabled: !widget.ticket!.factorTrail.contains(x.id),
|
||||
value: _factorPicked == x.id,
|
||||
onChanged: (value) {
|
||||
@@ -401,11 +407,14 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final lookupResp = await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
||||
final lookupResp =
|
||||
await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
||||
await sn.client.post('/cgi/id/users/me/password-reset', data: {
|
||||
'user_id': lookupResp.data['id'],
|
||||
});
|
||||
if (mounted) context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||
if (mounted) {
|
||||
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
@@ -430,7 +439,8 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
widget.onTicket(result.ticket);
|
||||
|
||||
// Pull factors
|
||||
final factorResp = await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
||||
final factorResp =
|
||||
await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
||||
'ticketId': result.ticket!.id.toString(),
|
||||
});
|
||||
widget.onFactor(
|
||||
@@ -524,7 +534,10 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
'termAcceptNextWithAgree'.tr(),
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha((255 * 0.75).round()),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
|
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class RegisterScreen extends StatefulWidget {
|
||||
@@ -43,6 +44,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
'nick': nickname,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'language': EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
@@ -54,7 +56,12 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StyledWidget(Container(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAuthRegister').tr(),
|
||||
),
|
||||
body: StyledWidget(Container(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
@@ -180,10 +187,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
'termAcceptNextWithAgree'.tr(),
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha((255 * 0.75).round()),
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
@@ -223,6 +227,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
)).padding(all: 24).center();
|
||||
)).padding(all: 24).center(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ import 'package:surface/widgets/account/account_select.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenChat').tr(),
|
||||
@@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenChat').tr(),
|
||||
@@ -195,6 +196,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||
child: ListView.builder(
|
||||
@@ -236,7 +240,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
'alias': channel.alias,
|
||||
},
|
||||
).then((value) {
|
||||
if (value == true) _refreshChannels();
|
||||
if (mounted) _refreshChannels();
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -276,6 +280,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@@ -9,10 +9,12 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/widgets/chat/call/call_controls.dart';
|
||||
import 'package:surface/widgets/chat/call/call_participant.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class CallRoomScreen extends StatefulWidget {
|
||||
final String scope;
|
||||
final String alias;
|
||||
|
||||
const CallRoomScreen({super.key, required this.scope, required this.alias});
|
||||
|
||||
@override
|
||||
@@ -35,8 +37,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
||||
child: call.focusTrack != null
|
||||
? InteractiveParticipantWidget(
|
||||
isFixedAvatar: false,
|
||||
@@ -71,8 +72,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
color: Theme.of(context).cardColor,
|
||||
participant: track,
|
||||
onTap: () {
|
||||
if (track.participant.sid !=
|
||||
call.focusTrack?.participant.sid) {
|
||||
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
||||
call.setFocusTrack(track);
|
||||
}
|
||||
},
|
||||
@@ -114,14 +114,10 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: InteractiveParticipantWidget(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh
|
||||
.withOpacity(0.75),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
|
||||
participant: track,
|
||||
onTap: () {
|
||||
if (track.participant.sid !=
|
||||
call.focusTrack?.participant.sid) {
|
||||
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
||||
call.setFocusTrack(track);
|
||||
}
|
||||
},
|
||||
@@ -152,31 +148,24 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
return ListenableBuilder(
|
||||
listenable: call,
|
||||
builder: (context, _) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: 'call'.tr(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge!
|
||||
.copyWith(color: Colors.white),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: call.lastDuration.toString(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: Colors.white),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: GestureDetector(
|
||||
body: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -190,8 +179,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
Builder(builder: (context) {
|
||||
final call = context.read<ChatCallProvider>();
|
||||
final connectionQuality =
|
||||
call.room.localParticipant?.connectionQuality ??
|
||||
livekit.ConnectionQuality.unknown;
|
||||
call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown;
|
||||
return Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -213,35 +201,24 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
children: [
|
||||
Text(
|
||||
{
|
||||
livekit.ConnectionState.disconnected:
|
||||
'callStatusDisconnected'.tr(),
|
||||
livekit.ConnectionState.connected:
|
||||
'callStatusConnected'.tr(),
|
||||
livekit.ConnectionState.connecting:
|
||||
'callStatusConnecting'.tr(),
|
||||
livekit.ConnectionState.reconnecting:
|
||||
'callStatusReconnecting'.tr(),
|
||||
livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(),
|
||||
livekit.ConnectionState.connected: 'callStatusConnected'.tr(),
|
||||
livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(),
|
||||
livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(),
|
||||
}[call.room.connectionState]!,
|
||||
),
|
||||
const Gap(6),
|
||||
if (connectionQuality !=
|
||||
livekit.ConnectionQuality.unknown)
|
||||
if (connectionQuality != livekit.ConnectionQuality.unknown)
|
||||
Icon(
|
||||
{
|
||||
livekit.ConnectionQuality.excellent:
|
||||
Icons.signal_cellular_alt,
|
||||
livekit.ConnectionQuality.good:
|
||||
Icons.signal_cellular_alt_2_bar,
|
||||
livekit.ConnectionQuality.poor:
|
||||
Icons.signal_cellular_alt_1_bar,
|
||||
livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt,
|
||||
livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
|
||||
livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
|
||||
}[connectionQuality],
|
||||
color: {
|
||||
livekit.ConnectionQuality.excellent:
|
||||
Colors.green,
|
||||
livekit.ConnectionQuality.good:
|
||||
Colors.orange,
|
||||
livekit.ConnectionQuality.poor:
|
||||
Colors.red,
|
||||
livekit.ConnectionQuality.excellent: Colors.green,
|
||||
livekit.ConnectionQuality.good: Colors.orange,
|
||||
livekit.ConnectionQuality.poor: Colors.red,
|
||||
}[connectionQuality],
|
||||
size: 16,
|
||||
)
|
||||
@@ -263,9 +240,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: _layoutMode == 0
|
||||
? const Icon(Icons.view_list)
|
||||
: const Icon(Icons.grid_view),
|
||||
icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view),
|
||||
onPressed: () {
|
||||
_switchLayout();
|
||||
},
|
||||
@@ -277,8 +252,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
switch (_layoutMode) {
|
||||
@@ -303,7 +277,6 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class ChannelDetailScreen extends StatefulWidget {
|
||||
@@ -189,7 +190,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
|
||||
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
|
||||
),
|
||||
|
@@ -12,6 +12,7 @@ import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChatManageScreen extends StatefulWidget {
|
||||
@@ -87,7 +88,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
try {
|
||||
final resp = await sn.client.request(
|
||||
widget.editingChannelAlias != null
|
||||
? '/cgi/im/channels/$scope/${widget.editingChannelAlias}'
|
||||
? '/cgi/im/channels/$scope/${_editingChannel!.id}'
|
||||
: '/cgi/im/channels/$scope',
|
||||
data: payload,
|
||||
options: Options(
|
||||
@@ -121,7 +122,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.editingChannelAlias != null
|
||||
? Text('screenChatManage').tr()
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -9,26 +10,36 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/chat_message_controller.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
||||
import 'package:surface/widgets/chat/chat_message.dart';
|
||||
import 'package:surface/widgets/chat/chat_message_input.dart';
|
||||
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
import '../../providers/user_directory.dart';
|
||||
import '../../providers/userinfo.dart';
|
||||
class ChatRoomScreenExtra {
|
||||
final String? initialText;
|
||||
final List<PostWriteMedia>? initialAttachments;
|
||||
|
||||
ChatRoomScreenExtra({this.initialText, this.initialAttachments});
|
||||
}
|
||||
|
||||
class ChatRoomScreen extends StatefulWidget {
|
||||
final String scope;
|
||||
final String alias;
|
||||
final ChatRoomScreenExtra? extra;
|
||||
|
||||
const ChatRoomScreen({super.key, required this.scope, required this.alias});
|
||||
const ChatRoomScreen({super.key, required this.scope, required this.alias, this.extra});
|
||||
|
||||
@override
|
||||
State<ChatRoomScreen> createState() => _ChatRoomScreenState();
|
||||
@@ -175,8 +186,23 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
_messageController = ChatMessageController(context);
|
||||
_fetchChannel().then((_) async {
|
||||
await _messageController.initialize(_channel!);
|
||||
await _messageController.checkUpdate();
|
||||
await _fetchOngoingCall();
|
||||
|
||||
if (widget.extra != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
log('[ChatInput] Setting initial text and attachments...');
|
||||
if (widget.extra!.initialText != null) {
|
||||
_inputGlobalKey.currentState?.setInitialText(widget.extra!.initialText!);
|
||||
}
|
||||
if (widget.extra!.initialAttachments != null) {
|
||||
_inputGlobalKey.currentState?.setInitialAttachments(widget.extra!.initialAttachments!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
_messageController.checkUpdate(),
|
||||
_fetchOngoingCall(),
|
||||
]);
|
||||
});
|
||||
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
@@ -210,7 +236,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
final call = context.watch<ChatCallProvider>();
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_channel?.type == 1
|
||||
@@ -280,11 +306,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
Expanded(
|
||||
child: InfiniteList(
|
||||
reverse: true,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 12,
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
hasReachedMax: _messageController.isAllLoaded,
|
||||
itemCount: _messageController.messages.length,
|
||||
isLoading: _messageController.isLoading,
|
||||
@@ -310,8 +332,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 480),
|
||||
child: ChatMessage(
|
||||
data: message,
|
||||
isMerged: canMerge,
|
||||
@@ -327,7 +347,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
_inputGlobalKey.currentState?.deleteMessage(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -335,11 +354,17 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
if (!_messageController.isPending)
|
||||
Material(
|
||||
elevation: 2,
|
||||
child: ChatMessageInput(
|
||||
child: Column(
|
||||
children: [
|
||||
ChatTypingIndicator(controller: _messageController),
|
||||
ChatMessageInput(
|
||||
key: _inputGlobalKey,
|
||||
otherMember: _otherMember,
|
||||
controller: _messageController,
|
||||
).padding(bottom: MediaQuery.of(context).padding.bottom),
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||
@@ -6,11 +7,14 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/post/post_detail.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
@@ -93,7 +97,9 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: ExpandableFab(
|
||||
key: _fabKey,
|
||||
@@ -210,6 +216,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverGap(12),
|
||||
SliverInfiniteList(
|
||||
itemCount: _posts.length,
|
||||
isLoading: _isBusy,
|
||||
@@ -217,7 +224,10 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
||||
onFetchData: _fetchPosts,
|
||||
itemBuilder: (context, idx) {
|
||||
return GestureDetector(
|
||||
return Center(
|
||||
child: OpenContainer(
|
||||
closedBuilder: (_, __) => Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: PostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: 640,
|
||||
@@ -228,16 +238,25 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
_refreshPosts();
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {'slug': _posts[idx].id.toString()},
|
||||
extra: _posts[idx],
|
||||
),
|
||||
openBuilder: (_, close) => PostDetailScreen(
|
||||
slug: _posts[idx].id.toString(),
|
||||
preload: _posts[idx],
|
||||
onBack: close,
|
||||
),
|
||||
openColor: Colors.transparent,
|
||||
openElevation: 0,
|
||||
transitionType: ContainerTransitionType.fade,
|
||||
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
||||
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
||||
),
|
||||
closedShape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@@ -11,6 +11,7 @@ import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
import '../providers/userinfo.dart';
|
||||
import '../widgets/unauthorized_hint.dart';
|
||||
@@ -180,7 +181,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenFriend').tr(),
|
||||
@@ -191,7 +192,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenFriend').tr(),
|
||||
@@ -233,6 +234,9 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
if (_requests.isNotEmpty || _blocks.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.wait([
|
||||
_fetchRelations(),
|
||||
@@ -282,6 +286,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@@ -9,6 +9,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
@@ -22,9 +23,11 @@ import 'package:surface/providers/special_day.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/widget.dart';
|
||||
import 'package:surface/types/check_in.dart';
|
||||
import 'package:surface/types/news.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
|
||||
class HomeScreenDashEntry {
|
||||
@@ -48,12 +51,12 @@ class HomeScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
static const List<HomeScreenDashEntry> kCards = [
|
||||
late final List<HomeScreenDashEntry> kCards = [
|
||||
HomeScreenDashEntry(
|
||||
name: 'dashEntryRecommendation',
|
||||
cols: 2,
|
||||
rows: 2,
|
||||
child: _HomeDashRecommendationPostWidget(),
|
||||
rows: 2,
|
||||
cols: 2,
|
||||
),
|
||||
HomeScreenDashEntry(
|
||||
name: 'dashEntryCheckIn',
|
||||
@@ -63,11 +66,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
name: 'dashEntryNotification',
|
||||
child: _HomeDashNotificationWidget(),
|
||||
),
|
||||
HomeScreenDashEntry(
|
||||
name: 'dashEntryTodayNews',
|
||||
child: _HomeDashTodayNews(),
|
||||
cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text("screenHome").tr(),
|
||||
@@ -153,9 +161,14 @@ class _HomeDashUpdateWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeDashSpecialDayWidget extends StatelessWidget {
|
||||
class _HomeDashSpecialDayWidget extends StatefulWidget {
|
||||
const _HomeDashSpecialDayWidget();
|
||||
|
||||
@override
|
||||
State<_HomeDashSpecialDayWidget> createState() => _HomeDashSpecialDayWidgetState();
|
||||
}
|
||||
|
||||
class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
@@ -165,7 +178,6 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
||||
|
||||
if (days.isNotEmpty) {
|
||||
return Column(
|
||||
spacing: 8,
|
||||
children: days.map((ele) {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
@@ -173,8 +185,8 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
||||
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
||||
subtitle: Text(
|
||||
DateFormat('y/M/d').format(DateTime.now().copyWith(
|
||||
month: kSpecialDays[ele]!.$1,
|
||||
day: kSpecialDays[ele]!.$2,
|
||||
month: kSpecialDays[ele]?.$1,
|
||||
day: kSpecialDays[ele]?.$2,
|
||||
)),
|
||||
),
|
||||
),
|
||||
@@ -193,7 +205,7 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
||||
title: Text('pending$name').tr(args: [RelativeTime(context).format(date)]),
|
||||
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
|
||||
subtitle: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@@ -204,6 +216,9 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
||||
separatorType: SeparatorType.symbol,
|
||||
decoration: BoxDecoration(),
|
||||
padding: EdgeInsets.zero,
|
||||
onDone: () {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
@@ -222,6 +237,106 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeDashTodayNews extends StatefulWidget {
|
||||
const _HomeDashTodayNews();
|
||||
|
||||
@override
|
||||
State<_HomeDashTodayNews> createState() => _HomeDashTodayNewsState();
|
||||
}
|
||||
|
||||
class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
||||
SnNewsArticle? _article;
|
||||
|
||||
Future<void> _fetchArticle() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/re/news/today');
|
||||
_article = SnNewsArticle.fromJson(resp.data['data']);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
rethrow;
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_fetchArticle();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Symbols.newspaper),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'newsToday',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr()
|
||||
],
|
||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||
if (_article != null)
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
_article!.title,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18),
|
||||
maxLines: MediaQuery.of(context).size.width >= 640 ? 2 : 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
parse(_article!.description).children.map((e) => e.text.trim()).join(),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75);
|
||||
}),
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'newsDetail',
|
||||
pathParameters: {'hash': _article!.hash},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeDashCheckInWidget extends StatefulWidget {
|
||||
const _HomeDashCheckInWidget();
|
||||
|
||||
@@ -380,6 +495,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
Text(
|
||||
'dailyCheckInNone',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
],
|
||||
)
|
||||
@@ -397,6 +514,11 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
'+${_todayRecord!.resultExperience} EXP',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (_todayRecord!.resultCoin >= 0)
|
||||
Text(
|
||||
'+${_todayRecord!.resultCoin} ${'walletCurrencyShort'.tr()}',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
241
lib/screens/news/news_detail.dart
Normal file
241
lib/screens/news/news_detail.dart
Normal file
@@ -0,0 +1,241 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/news.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class NewsDetailScreen extends StatefulWidget {
|
||||
final String hash;
|
||||
|
||||
const NewsDetailScreen({super.key, required this.hash});
|
||||
|
||||
@override
|
||||
State<NewsDetailScreen> createState() => _NewsDetailScreenState();
|
||||
}
|
||||
|
||||
class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
SnNewsArticle? _article;
|
||||
dom.Document? _articleFragment;
|
||||
|
||||
Future<void> _fetchArticle() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
|
||||
_article = SnNewsArticle.fromJson(resp.data);
|
||||
_articleFragment = parse(_article!.content);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err).then((_) {
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context);
|
||||
});
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _parseHtmlToWidgets(Iterable<dom.Element>? elements) {
|
||||
if (elements == null) return [];
|
||||
|
||||
final List<Widget> widgets = [];
|
||||
|
||||
for (final node in elements) {
|
||||
switch (node.localName) {
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
widgets.add(Text(node.text.trim(), style: Theme.of(context).textTheme.titleMedium));
|
||||
break;
|
||||
case 'p':
|
||||
if (node.text.trim().isEmpty) continue;
|
||||
widgets.add(
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: node.text.trim(),
|
||||
children: [
|
||||
for (final child in node.children)
|
||||
switch (child.localName) {
|
||||
'a' => TextSpan(
|
||||
text: child.text.trim(),
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlString(child.attributes['href']!);
|
||||
},
|
||||
),
|
||||
_ => TextSpan(text: child.text.trim()),
|
||||
},
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'a':
|
||||
// drop single link
|
||||
break;
|
||||
case 'div':
|
||||
// ignore div text, normally it is not meaningful
|
||||
widgets.addAll(_parseHtmlToWidgets(node.children));
|
||||
break;
|
||||
case 'hr':
|
||||
widgets.add(const Divider());
|
||||
break;
|
||||
case 'img':
|
||||
var src = node.attributes['src'];
|
||||
if (src == null) break;
|
||||
final width = double.tryParse(node.attributes['width'] ?? 'null');
|
||||
final height = double.tryParse(node.attributes['height'] ?? 'null');
|
||||
final ratio = width != null && height != null ? width / height : 1.0;
|
||||
if (src.startsWith('//')) {
|
||||
src = 'https:$src';
|
||||
} else if (!src.startsWith('http')) {
|
||||
final baseUri = Uri.parse(_article!.url);
|
||||
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||
src = '$baseUrl/$src';
|
||||
}
|
||||
widgets.add(
|
||||
AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
height: height ?? double.infinity,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AutoResizeUniversalImage(
|
||||
src,
|
||||
fit: width != null && height != null ? BoxFit.cover : BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
widgets.addAll(_parseHtmlToWidgets(node.children));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchArticle();
|
||||
}
|
||||
|
||||
bool _isReadingFromReader = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text(_article?.title ?? 'loading'.tr()),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
MaterialBanner(
|
||||
dividerColor: Colors.transparent,
|
||||
leading: const Icon(Icons.info),
|
||||
content: Text(_isReadingFromReader ? 'newsReadingFromReader'.tr() : 'newsReadingFromOriginal'.tr()),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('newsReadingProviderSwap').tr(),
|
||||
onPressed: () {
|
||||
setState(() => _isReadingFromReader = !_isReadingFromReader);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_articleFragment != null && _isReadingFromReader)
|
||||
Expanded(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
|
||||
Builder(builder: (context) {
|
||||
final htmlDescription = parse(_article!.description);
|
||||
return Text(
|
||||
htmlDescription.children.map((ele) => ele.text.trim()).join(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
);
|
||||
}),
|
||||
Builder(builder: (context) {
|
||||
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||
return Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75);
|
||||
}),
|
||||
Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75),
|
||||
const Divider(),
|
||||
..._parseHtmlToWidgets(_articleFragment!.children),
|
||||
const Divider(),
|
||||
InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Reference from original website',
|
||||
style: TextStyle(decoration: TextDecoration.underline),
|
||||
),
|
||||
const Gap(4),
|
||||
Icon(Icons.launch, size: 16),
|
||||
],
|
||||
).opacity(0.85),
|
||||
onTap: () {
|
||||
launchUrlString(_article!.url);
|
||||
},
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
).padding(horizontal: 12, vertical: 16),
|
||||
),
|
||||
).center(),
|
||||
)
|
||||
else if (_article != null)
|
||||
Expanded(
|
||||
child: InAppWebView(
|
||||
key: GlobalKey(),
|
||||
initialUrlRequest: URLRequest(url: WebUri(_article!.url)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
239
lib/screens/news/news_list.dart
Normal file
239
lib/screens/news/news_list.dart
Normal file
@@ -0,0 +1,239 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/news.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class NewsScreen extends StatefulWidget {
|
||||
const NewsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<NewsScreen> createState() => _NewsScreenState();
|
||||
}
|
||||
|
||||
class _NewsScreenState extends State<NewsScreen> {
|
||||
List<SnNewsSource>? _sources;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_fetchSources();
|
||||
}
|
||||
|
||||
Future<void> _fetchSources() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/re/well-known/sources');
|
||||
_sources = List<SnNewsSource>.from(
|
||||
resp.data?.map((e) => SnNewsSource.fromJson(e)) ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_sources == null) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNews').tr(),
|
||||
),
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return DefaultTabController(
|
||||
length: _sources!.length + 1,
|
||||
child: AppScaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverOverlapAbsorber(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: SliverAppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNews').tr(),
|
||||
floating: true,
|
||||
snap: true,
|
||||
bottom: TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(child: Text('newsAllSources'.tr()).textColor(Theme.of(context).appBarTheme.foregroundColor)),
|
||||
for (final source in _sources!)
|
||||
Tab(
|
||||
child: Text(source.label).textColor(Theme.of(context).appBarTheme.foregroundColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
body: TabBarView(
|
||||
children: [
|
||||
_NewsArticleListWidget(allSources: _sources!),
|
||||
for (final source in _sources!)
|
||||
_NewsArticleListWidget(
|
||||
source: source.id,
|
||||
allSources: _sources!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NewsArticleListWidget extends StatefulWidget {
|
||||
final String? source;
|
||||
final List<SnNewsSource> allSources;
|
||||
|
||||
const _NewsArticleListWidget({this.source, required this.allSources});
|
||||
|
||||
@override
|
||||
State<_NewsArticleListWidget> createState() => _NewsArticleListWidgetState();
|
||||
}
|
||||
|
||||
class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
|
||||
bool _isBusy = false;
|
||||
|
||||
int? _totalCount;
|
||||
final List<SnNewsArticle> _articles = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchArticles() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/re/news', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _articles.length,
|
||||
if (widget.source != null) 'source': widget.source,
|
||||
});
|
||||
_totalCount = resp.data['count'];
|
||||
_articles.addAll(List<SnNewsArticle>.from(
|
||||
resp.data['data']?.map((e) => SnNewsArticle.fromJson(e)) ?? [],
|
||||
));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchArticles();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchArticles,
|
||||
child: InfiniteList(
|
||||
isLoading: _isBusy,
|
||||
itemCount: _articles.length,
|
||||
hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
|
||||
onFetchData: () {
|
||||
_fetchArticles();
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final article = _articles[index];
|
||||
|
||||
final baseUri = Uri.parse(article.url);
|
||||
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||
|
||||
final htmlDescription = parse(article.description);
|
||||
final date = article.publishedAt ?? article.createdAt;
|
||||
|
||||
return Card(
|
||||
child: InkWell(
|
||||
radius: 8,
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'newsDetail',
|
||||
pathParameters: {'hash': article.hash},
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(8),
|
||||
topLeft: Radius.circular(8),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AutoResizeUniversalImage(
|
||||
article.thumbnail.startsWith('http')
|
||||
? article.thumbnail
|
||||
: '$baseUrl/${article.thumbnail}',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
|
||||
.textStyle(Theme.of(context).textTheme.bodyMedium!)
|
||||
.padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(widget.allSources.where((x) => x.id == article.source).first.label)
|
||||
.textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75).padding(horizontal: 16),
|
||||
Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75).padding(horizontal: 16),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/notification.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/notification.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
@@ -14,12 +15,23 @@ import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
import '../providers/userinfo.dart';
|
||||
import '../widgets/unauthorized_hint.dart';
|
||||
|
||||
const Map<String, IconData> kNotificationTopicIcons = {
|
||||
'general': Symbols.notifications,
|
||||
'passport.security.alert': Symbols.gpp_maybe,
|
||||
'passport.security.otp': Symbols.password,
|
||||
'interactive.subscription': Symbols.subscriptions,
|
||||
'interactive.feedback': Symbols.add_reaction,
|
||||
'messaging.callStart': Symbols.call_received,
|
||||
'wallet.transaction.new': Symbols.receipt,
|
||||
};
|
||||
|
||||
class NotificationScreen extends StatefulWidget {
|
||||
const NotificationScreen({super.key});
|
||||
|
||||
@@ -35,13 +47,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
final List<SnNotification> _notifications = List.empty(growable: true);
|
||||
int? _totalCount;
|
||||
|
||||
static const Map<String, IconData> kNotificationTopicIcons = {
|
||||
'passport.security.alert': Symbols.gpp_maybe,
|
||||
'interactive.subscription': Symbols.subscriptions,
|
||||
'interactive.feedback': Symbols.add_reaction,
|
||||
'messaging.callStart': Symbols.call_received,
|
||||
};
|
||||
|
||||
Future<void> _fetchNotifications() async {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) return;
|
||||
@@ -50,6 +55,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final nty = context.read<NotificationProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/notifications?take=10');
|
||||
_totalCount = resp.data['count'];
|
||||
_notifications.addAll(
|
||||
@@ -58,6 +64,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
.cast<SnNotification>() ??
|
||||
[],
|
||||
);
|
||||
nty.updateTray();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@@ -82,24 +89,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
if (!mounted) return;
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
List<int> markList = List.empty(growable: true);
|
||||
for (final element in _notifications) {
|
||||
if (element.id <= 0) continue;
|
||||
if (element.readAt != null) continue;
|
||||
markList.add(element.id);
|
||||
}
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.put('/cgi/id/notifications/read', data: {
|
||||
'messages': markList,
|
||||
});
|
||||
final nty = context.read<NotificationProvider>();
|
||||
final resp = await sn.client.put('/cgi/id/notifications/read/all');
|
||||
_notifications.clear();
|
||||
_fetchNotifications();
|
||||
nty.clear();
|
||||
|
||||
if (!mounted) return;
|
||||
context.showSnackbar(
|
||||
'notificationMarkAllReadPrompt'.plural(markList.length),
|
||||
'notificationMarkAllReadPrompt'.plural(resp.data['count']),
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
@@ -146,7 +146,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNotification').tr(),
|
||||
@@ -157,7 +157,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNotification').tr(),
|
||||
@@ -215,10 +215,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
if (nty.subtitle != null) const Gap(4),
|
||||
MarkdownTextContent(
|
||||
SelectionArea(
|
||||
child: MarkdownTextContent(
|
||||
content: nty.body,
|
||||
isAutoWarp: true,
|
||||
isSelectable: true,
|
||||
),
|
||||
),
|
||||
if ([
|
||||
'interactive.feedback',
|
||||
|
@@ -13,6 +13,8 @@ import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_background.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
@@ -20,12 +22,9 @@ import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
class PostDetailScreen extends StatefulWidget {
|
||||
final String slug;
|
||||
final SnPost? preload;
|
||||
final Function? onBack;
|
||||
|
||||
const PostDetailScreen({
|
||||
super.key,
|
||||
required this.slug,
|
||||
this.preload,
|
||||
});
|
||||
const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
|
||||
|
||||
@override
|
||||
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
||||
@@ -67,10 +66,15 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
final ua = context.watch<UserProvider>();
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
return Scaffold(
|
||||
return AppBackground(
|
||||
isRoot: widget.onBack != null,
|
||||
child: AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (widget.onBack != null) {
|
||||
widget.onBack!.call();
|
||||
}
|
||||
if (GoRouter.of(context).canPop()) {
|
||||
GoRouter.of(context).pop(context);
|
||||
return;
|
||||
@@ -96,6 +100,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
),
|
||||
),
|
||||
]),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text('postDetail').tr(),
|
||||
),
|
||||
@@ -183,6 +189,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -13,19 +13,20 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
||||
import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class PostEditorExtraProps {
|
||||
class PostEditorExtra {
|
||||
final String? text;
|
||||
final String? title;
|
||||
final String? description;
|
||||
final List<PostWriteMedia>? attachments;
|
||||
|
||||
const PostEditorExtraProps({
|
||||
const PostEditorExtra({
|
||||
this.text,
|
||||
this.title,
|
||||
this.description,
|
||||
@@ -38,7 +39,7 @@ class PostEditorScreen extends StatefulWidget {
|
||||
final int? postEditId;
|
||||
final int? postReplyId;
|
||||
final int? postRepostId;
|
||||
final PostEditorExtraProps? extraProps;
|
||||
final PostEditorExtra? extraProps;
|
||||
|
||||
const PostEditorScreen({
|
||||
super.key,
|
||||
@@ -54,7 +55,9 @@ class PostEditorScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
final PostWriteController _writeController = PostWriteController();
|
||||
late final PostWriteController _writeController = PostWriteController(
|
||||
doLoadFromTemporary: widget.postEditId == null,
|
||||
);
|
||||
|
||||
bool _isFetching = false;
|
||||
|
||||
@@ -126,7 +129,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
return ListenableBuilder(
|
||||
listenable: _writeController,
|
||||
builder: (context, _) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
@@ -150,6 +153,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
),
|
||||
]),
|
||||
maxLines: 2,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
@@ -247,8 +251,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 160),
|
||||
child: Column(
|
||||
children: [
|
||||
// Replying Notice
|
||||
@@ -301,7 +307,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
],
|
||||
),
|
||||
// Content Input Area
|
||||
TextField(
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: TextField(
|
||||
controller: _writeController.contentController,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
@@ -315,6 +323,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
]
|
||||
.expandIndexed(
|
||||
(idx, ele) => [
|
||||
@@ -325,9 +334,12 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
|
||||
PostMediaPendingList(
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: PostMediaPendingList(
|
||||
thumbnail: _writeController.thumbnail,
|
||||
attachments: _writeController.attachments,
|
||||
isBusy: _writeController.isBusy,
|
||||
@@ -359,6 +371,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
},
|
||||
onUpdateBusy: (state) => _writeController.setIsBusy(state),
|
||||
).padding(bottom: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Material(
|
||||
elevation: 2,
|
||||
child: Column(
|
||||
@@ -373,6 +389,36 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
)
|
||||
else if (_writeController.isBusy)
|
||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||
Container(
|
||||
child: _writeController.temporaryRestored
|
||||
? Container(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.restore, size: 20),
|
||||
const Gap(8),
|
||||
Expanded(child: Text('postLocalDraftRestored').tr()),
|
||||
InkWell(
|
||||
child: Text('dialogDismiss').tr(),
|
||||
onTap: () {
|
||||
_writeController.reset();
|
||||
},
|
||||
),
|
||||
],
|
||||
))
|
||||
: const SizedBox.shrink(),
|
||||
)
|
||||
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
@@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_tags_field.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
@@ -119,7 +120,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('screenPostSearch').tr(),
|
||||
actions: [
|
||||
|
@@ -17,6 +17,7 @@ import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
@@ -274,7 +275,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
body: NestedScrollView(
|
||||
controller: _scrollController,
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
|
@@ -12,6 +12,7 @@ import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
@@ -83,7 +84,7 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenRealm').tr(),
|
||||
@@ -94,7 +95,7 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenRealm').tr(),
|
||||
@@ -118,6 +119,9 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchRealms,
|
||||
child: ListView.builder(
|
||||
@@ -196,7 +200,9 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: (realm.banner?.isEmpty ?? true)
|
||||
? const SizedBox.shrink()
|
||||
@@ -205,6 +211,7 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -30,
|
||||
left: 18,
|
||||
@@ -240,6 +247,7 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@@ -18,6 +18,7 @@ import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@@ -179,7 +180,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.editingRealmAlias != null
|
||||
? Text('screenRealmManage').tr()
|
||||
|
@@ -8,13 +8,13 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
import '../../types/post.dart';
|
||||
|
||||
class RealmDetailScreen extends StatefulWidget {
|
||||
final String alias;
|
||||
|
||||
@@ -70,27 +70,19 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
child: AppScaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
// These are the slivers that show up in the "outer" scroll view.
|
||||
return <Widget>[
|
||||
SliverOverlapAbsorber(
|
||||
// This widget takes the overlapping behavior of the SliverAppBar,
|
||||
// and redirects it to the SliverOverlapInjector below. If it is
|
||||
// missing, then it is possible for the nested "inner" scroll view
|
||||
// below to end up under the SliverAppBar even when the inner
|
||||
// scroll view thinks it has not been scrolled.
|
||||
// This is not necessary if the "headerSliverBuilder" only builds
|
||||
// widgets that do not overlap the next sliver.
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: SliverAppBar(
|
||||
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||
bottom: TabBar(
|
||||
tabs: [
|
||||
Tab(icon: const Icon(Symbols.home)),
|
||||
Tab(icon: const Icon(Symbols.group)),
|
||||
Tab(icon: const Icon(Symbols.settings)),
|
||||
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -428,7 +420,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const Gap(16),
|
||||
const Gap(8),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.edit),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
|
@@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/theme.dart';
|
||||
import 'package:surface/theme.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
const Map<String, Color> kColorSchemes = {
|
||||
'colorSchemeIndigo': Colors.indigo,
|
||||
@@ -67,7 +68,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenSettings').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
@@ -77,6 +82,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('settingsAppearance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
ListTile(
|
||||
title: Text('settingsDisplayLanguage').tr(),
|
||||
subtitle: Text('settingsDisplayLanguageDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.translate),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<Locale?>(
|
||||
isExpanded: true,
|
||||
items: [
|
||||
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
|
||||
return DropdownMenuItem<Locale?>(
|
||||
value: ele,
|
||||
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
|
||||
);
|
||||
}),
|
||||
DropdownMenuItem<Locale?>(
|
||||
value: null,
|
||||
child: Text('settingsDisplayLanguageSystem').tr().fontSize(14),
|
||||
),
|
||||
],
|
||||
value: EasyLocalization.of(context)!.currentLocale,
|
||||
onChanged: (Locale? value) {
|
||||
if (value != null) {
|
||||
EasyLocalization.of(context)!.setLocale(value);
|
||||
} else {
|
||||
EasyLocalization.of(context)!.resetLocale();
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 5,
|
||||
),
|
||||
height: 40,
|
||||
width: 160,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb)
|
||||
ListTile(
|
||||
title: Text('settingsBackgroundImage').tr(),
|
||||
@@ -120,7 +167,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
subtitle: Text('settingsThemeMaterial3Description').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
secondary: const Icon(Symbols.new_releases),
|
||||
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false,
|
||||
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_prefs.setBool(
|
||||
@@ -142,7 +189,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value);
|
||||
final color = await showDialog<Color?>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
builder: (context) =>
|
||||
AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
pickerColor: pickerColor,
|
||||
@@ -205,7 +253,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
|
||||
onChanged: (int? value) {
|
||||
if (value != null && value != -1) {
|
||||
_prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values.elementAt(value).value);
|
||||
_prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values
|
||||
.elementAt(value)
|
||||
.value);
|
||||
final th = context.read<ThemeProvider>();
|
||||
th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value));
|
||||
setState(() {});
|
||||
@@ -240,6 +290,61 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.left_panel_close),
|
||||
title: Text('settingsDrawerPreferCollapse').tr(),
|
||||
subtitle: Text('settingsDrawerPreferCollapseDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
|
||||
onChanged: (value) {
|
||||
_prefs.setBool(kAppDrawerPreferCollapse, value ?? false);
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
cfg.calcDrawerSize(context);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('settingsFeatures').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.vibration),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
title: Text('settingsNotifyWithHaptic').tr(),
|
||||
subtitle: Text('settingsNotifyWithHapticDescription').tr(),
|
||||
value: _prefs.getBool(kAppNotifyWithHaptic) ?? true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_prefs.setBool(kAppNotifyWithHaptic, value ?? false);
|
||||
});
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.link),
|
||||
title: Text('settingsExpandPostLink').tr(),
|
||||
subtitle: Text('settingsExpandPostLinkDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
value: _prefs.getBool(kAppExpandPostLink) ?? true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_prefs.setBool(kAppExpandPostLink, value ?? false);
|
||||
});
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.chat),
|
||||
title: Text('settingsExpandChatLink').tr(),
|
||||
subtitle: Text('settingsExpandChatLinkDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
value: _prefs.getBool(kAppExpandChatLink) ?? true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_prefs.setBool(kAppExpandChatLink, value ?? false);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
@@ -282,7 +387,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
('Custom', _serverUrlController.text),
|
||||
]
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
(item) =>
|
||||
DropdownMenuItem<String>(
|
||||
value: item.$2,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
@@ -349,7 +455,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
isExpanded: true,
|
||||
items: kImageQualityLevel.entries
|
||||
.map(
|
||||
(item) => DropdownMenuItem<FilterQuality>(
|
||||
(item) =>
|
||||
DropdownMenuItem<FilterQuality>(
|
||||
value: item.value,
|
||||
child: Text(item.key).tr().fontSize(14),
|
||||
),
|
||||
|
@@ -8,9 +8,20 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/chat/room.dart';
|
||||
import 'package:surface/screens/post/post_editor.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
|
||||
class AppSharingListener extends StatefulWidget {
|
||||
final Widget child;
|
||||
@@ -51,20 +62,39 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
pathParameters: {
|
||||
'mode': 'stories',
|
||||
},
|
||||
extra: PostEditorExtraProps(
|
||||
extra: PostEditorExtra(
|
||||
text: value
|
||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||
.map((e) => e.path).join('\n'),
|
||||
.map((e) => e.path)
|
||||
.join('\n'),
|
||||
attachments: value
|
||||
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(),
|
||||
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
|
||||
.contains(e.type))
|
||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
leading: Icon(Icons.chat_outlined),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('shareIntentSendChannel').tr(),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _ShareIntentChannelSelect(value: value),
|
||||
).then((val) {
|
||||
if (!context.mounted) return;
|
||||
if (val == true) Navigator.pop(context);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
).width(280),
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -120,3 +150,193 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
class _ShareIntentChannelSelect extends StatefulWidget {
|
||||
final Iterable<SharedMediaFile> value;
|
||||
|
||||
const _ShareIntentChannelSelect({required this.value});
|
||||
|
||||
@override
|
||||
State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState();
|
||||
}
|
||||
|
||||
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||
bool _isBusy = true;
|
||||
|
||||
List<SnChannel>? _channels;
|
||||
Map<int, SnChatMessage>? _lastMessages;
|
||||
|
||||
void _refreshChannels() {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) {
|
||||
setState(() => _isBusy = false);
|
||||
return;
|
||||
}
|
||||
|
||||
final chan = context.read<ChatChannelProvider>();
|
||||
chan.fetchChannels().listen((channels) async {
|
||||
final lastMessages = await chan.getLastMessages(channels);
|
||||
_lastMessages = {for (final val in lastMessages) val.channelId: val};
|
||||
channels.sort((a, b) {
|
||||
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) {
|
||||
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt);
|
||||
}
|
||||
if (_lastMessages!.containsKey(a.id)) return -1;
|
||||
if (_lastMessages!.containsKey(b.id)) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (!mounted) return;
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
for (final channel in channels) {
|
||||
if (channel.type == 1) {
|
||||
await ud.listAccount(
|
||||
channel.members
|
||||
?.cast<SnChannelMember?>()
|
||||
.map((ele) => ele?.accountId)
|
||||
.where((ele) => ele != null)
|
||||
.toSet() ??
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) setState(() => _channels = channels);
|
||||
})
|
||||
..onError((err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
setState(() => _isBusy = false);
|
||||
})
|
||||
..onDone(() {
|
||||
if (!mounted) return;
|
||||
setState(() => _isBusy = false);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refreshChannels();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.read<UserProvider>();
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.chat, size: 24),
|
||||
const Gap(16),
|
||||
Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||
child: ListView.builder(
|
||||
itemCount: _channels?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final channel = _channels![idx];
|
||||
final lastMessage = _lastMessages?[channel.id];
|
||||
|
||||
if (channel.type == 1) {
|
||||
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||
(ele) => ele?.accountId != ua.user?.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text(
|
||||
'channelDirectMessageDescription'.tr(args: [
|
||||
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
||||
]),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {
|
||||
'scope': channel.realm?.alias ?? 'global',
|
||||
'alias': channel.alias,
|
||||
},
|
||||
).then((value) {
|
||||
if (mounted) _refreshChannels();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
title: Text(channel.name),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text(
|
||||
channel.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: null,
|
||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, true);
|
||||
GoRouter.of(context)
|
||||
.pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {
|
||||
'scope': channel.realm?.alias ?? 'global',
|
||||
'alias': channel.alias,
|
||||
},
|
||||
extra: ChatRoomScreenExtra(
|
||||
initialText: widget.value
|
||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||
.map((e) => e.path)
|
||||
.join('\n'),
|
||||
initialAttachments: widget.value
|
||||
.where((e) =>
|
||||
[SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
.then((value) {
|
||||
if (value == true) _refreshChannels();
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
279
lib/screens/wallet.dart
Normal file
279
lib/screens/wallet.dart
Normal file
@@ -0,0 +1,279 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/wallet.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class WalletScreen extends StatefulWidget {
|
||||
const WalletScreen({super.key});
|
||||
|
||||
@override
|
||||
State<WalletScreen> createState() => _WalletScreenState();
|
||||
}
|
||||
|
||||
class _WalletScreenState extends State<WalletScreen> {
|
||||
bool _isBusy = false;
|
||||
SnWallet? _wallet;
|
||||
|
||||
Future<void> _fetchWallet() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/wa/wallets/me');
|
||||
_wallet = SnWallet.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchWallet();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountWallet').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (_wallet == null)
|
||||
Expanded(
|
||||
child: _CreateWalletWidget(
|
||||
onCreate: () {
|
||||
_fetchWallet();
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
child: Icon(Symbols.wallet, size: 28),
|
||||
),
|
||||
const Gap(12),
|
||||
SizedBox(width: double.infinity),
|
||||
Text(
|
||||
NumberFormat.compactCurrency(
|
||||
locale: EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
symbol: '${'walletCurrencyShort'.tr()} ',
|
||||
decimalDigits: 2,
|
||||
).format(double.parse(_wallet!.balance)),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text('walletCurrency'.plural(double.parse(_wallet!.balance))),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 24),
|
||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||
if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletTransactionList extends StatefulWidget {
|
||||
final SnWallet myself;
|
||||
|
||||
const _WalletTransactionList({required this.myself});
|
||||
|
||||
@override
|
||||
State<_WalletTransactionList> createState() => _WalletTransactionListState();
|
||||
}
|
||||
|
||||
class _WalletTransactionListState extends State<_WalletTransactionList> {
|
||||
bool _isBusy = false;
|
||||
int? _totalCount;
|
||||
final List<SnTransaction> _transactions = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchTransactions() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/wa/transactions/me', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _transactions.length,
|
||||
});
|
||||
_totalCount = resp.data['count'];
|
||||
_transactions.addAll(
|
||||
resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchTransactions();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchTransactions,
|
||||
child: InfiniteList(
|
||||
itemCount: _transactions.length,
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!,
|
||||
onFetchData: () {
|
||||
_fetchTransactions();
|
||||
},
|
||||
itemBuilder: (context, idx) {
|
||||
final ele = _transactions[idx];
|
||||
final isIncoming = ele.payeeId == widget.myself.id;
|
||||
return ListTile(
|
||||
leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made),
|
||||
title: Text(
|
||||
'${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
|
||||
style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(ele.remark),
|
||||
const Gap(2),
|
||||
Text(
|
||||
DateFormat(
|
||||
null,
|
||||
EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
).format(ele.createdAt),
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateWalletWidget extends StatefulWidget {
|
||||
final Function()? onCreate;
|
||||
|
||||
const _CreateWalletWidget({required this.onCreate});
|
||||
|
||||
@override
|
||||
State<_CreateWalletWidget> createState() => _CreateWalletWidgetState();
|
||||
}
|
||||
|
||||
class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _createWallet() async {
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
final password = await showDialog<String?>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('walletCreate').tr(),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('walletCreatePassword').tr(),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
controller: passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPassword'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text('cancel').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop(passwordController.text);
|
||||
},
|
||||
child: Text('next').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
passwordController.dispose();
|
||||
});
|
||||
if (password == null || password.isEmpty) return;
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/wa/wallets/me', data: {
|
||||
'password': password,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
child: Icon(Symbols.add, size: 28),
|
||||
),
|
||||
const Gap(12),
|
||||
Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(),
|
||||
const Gap(8),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: _isBusy ? null : () => _createWallet(),
|
||||
child: Text('next').tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -34,9 +34,10 @@ Future<ThemeData> createAppTheme(
|
||||
);
|
||||
|
||||
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false),
|
||||
useMaterial3: useM3,
|
||||
colorScheme: colorScheme,
|
||||
brightness: brightness,
|
||||
iconTheme: IconThemeData(
|
||||
@@ -45,12 +46,24 @@ Future<ThemeData> createAppTheme(
|
||||
opticalSize: 20,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: hasAppBarBlurry ? 0 : null,
|
||||
backgroundColor: hasAppBarBlurry ? colorScheme.surfaceContainer.withAlpha(200) : colorScheme.primary,
|
||||
backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
|
||||
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.transparent,
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
|
||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -15,12 +15,13 @@ class SnAccount with _$SnAccount {
|
||||
required DateTime? deletedAt,
|
||||
required DateTime? confirmedAt,
|
||||
required List<SnAccountContact>? contacts,
|
||||
required String avatar,
|
||||
required String banner,
|
||||
@Default("") String avatar,
|
||||
@Default("") String banner,
|
||||
required String description,
|
||||
required String name,
|
||||
required String nick,
|
||||
required Map<String, dynamic> permNodes,
|
||||
required String language,
|
||||
required SnAccountProfile? profile,
|
||||
@Default([]) List<SnAccountBadge> badges,
|
||||
required DateTime? suspendedAt,
|
||||
|
@@ -33,6 +33,7 @@ mixin _$SnAccount {
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
String get nick => throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> get permNodes => throw _privateConstructorUsedError;
|
||||
String get language => throw _privateConstructorUsedError;
|
||||
SnAccountProfile? get profile => throw _privateConstructorUsedError;
|
||||
List<SnAccountBadge> get badges => throw _privateConstructorUsedError;
|
||||
DateTime? get suspendedAt => throw _privateConstructorUsedError;
|
||||
@@ -69,6 +70,7 @@ abstract class $SnAccountCopyWith<$Res> {
|
||||
String name,
|
||||
String nick,
|
||||
Map<String, dynamic> permNodes,
|
||||
String language,
|
||||
SnAccountProfile? profile,
|
||||
List<SnAccountBadge> badges,
|
||||
DateTime? suspendedAt,
|
||||
@@ -107,6 +109,7 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
|
||||
Object? name = null,
|
||||
Object? nick = null,
|
||||
Object? permNodes = null,
|
||||
Object? language = null,
|
||||
Object? profile = freezed,
|
||||
Object? badges = null,
|
||||
Object? suspendedAt = freezed,
|
||||
@@ -164,6 +167,10 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
|
||||
? _value.permNodes
|
||||
: permNodes // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
language: null == language
|
||||
? _value.language
|
||||
: language // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
profile: freezed == profile
|
||||
? _value.profile
|
||||
: profile // ignore: cast_nullable_to_non_nullable
|
||||
@@ -231,6 +238,7 @@ abstract class _$$SnAccountImplCopyWith<$Res>
|
||||
String name,
|
||||
String nick,
|
||||
Map<String, dynamic> permNodes,
|
||||
String language,
|
||||
SnAccountProfile? profile,
|
||||
List<SnAccountBadge> badges,
|
||||
DateTime? suspendedAt,
|
||||
@@ -268,6 +276,7 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
||||
Object? name = null,
|
||||
Object? nick = null,
|
||||
Object? permNodes = null,
|
||||
Object? language = null,
|
||||
Object? profile = freezed,
|
||||
Object? badges = null,
|
||||
Object? suspendedAt = freezed,
|
||||
@@ -325,6 +334,10 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
||||
? _value._permNodes
|
||||
: permNodes // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
language: null == language
|
||||
? _value.language
|
||||
: language // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
profile: freezed == profile
|
||||
? _value.profile
|
||||
: profile // ignore: cast_nullable_to_non_nullable
|
||||
@@ -367,12 +380,13 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
required this.deletedAt,
|
||||
required this.confirmedAt,
|
||||
required final List<SnAccountContact>? contacts,
|
||||
required this.avatar,
|
||||
required this.banner,
|
||||
this.avatar = "",
|
||||
this.banner = "",
|
||||
required this.description,
|
||||
required this.name,
|
||||
required this.nick,
|
||||
required final Map<String, dynamic> permNodes,
|
||||
required this.language,
|
||||
required this.profile,
|
||||
final List<SnAccountBadge> badges = const [],
|
||||
required this.suspendedAt,
|
||||
@@ -410,8 +424,10 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final String avatar;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String banner;
|
||||
@override
|
||||
final String description;
|
||||
@@ -427,6 +443,8 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
return EqualUnmodifiableMapView(_permNodes);
|
||||
}
|
||||
|
||||
@override
|
||||
final String language;
|
||||
@override
|
||||
final SnAccountProfile? profile;
|
||||
final List<SnAccountBadge> _badges;
|
||||
@@ -451,7 +469,7 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -477,6 +495,8 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
(identical(other.nick, nick) || other.nick == nick) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._permNodes, _permNodes) &&
|
||||
(identical(other.language, language) ||
|
||||
other.language == language) &&
|
||||
(identical(other.profile, profile) || other.profile == profile) &&
|
||||
const DeepCollectionEquality().equals(other._badges, _badges) &&
|
||||
(identical(other.suspendedAt, suspendedAt) ||
|
||||
@@ -507,6 +527,7 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
name,
|
||||
nick,
|
||||
const DeepCollectionEquality().hash(_permNodes),
|
||||
language,
|
||||
profile,
|
||||
const DeepCollectionEquality().hash(_badges),
|
||||
suspendedAt,
|
||||
@@ -540,12 +561,13 @@ abstract class _SnAccount extends SnAccount {
|
||||
required final DateTime? deletedAt,
|
||||
required final DateTime? confirmedAt,
|
||||
required final List<SnAccountContact>? contacts,
|
||||
required final String avatar,
|
||||
required final String banner,
|
||||
final String avatar,
|
||||
final String banner,
|
||||
required final String description,
|
||||
required final String name,
|
||||
required final String nick,
|
||||
required final Map<String, dynamic> permNodes,
|
||||
required final String language,
|
||||
required final SnAccountProfile? profile,
|
||||
final List<SnAccountBadge> badges,
|
||||
required final DateTime? suspendedAt,
|
||||
@@ -584,6 +606,8 @@ abstract class _SnAccount extends SnAccount {
|
||||
@override
|
||||
Map<String, dynamic> get permNodes;
|
||||
@override
|
||||
String get language;
|
||||
@override
|
||||
SnAccountProfile? get profile;
|
||||
@override
|
||||
List<SnAccountBadge> get badges;
|
||||
|
@@ -20,12 +20,13 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
|
||||
contacts: (json['contacts'] as List<dynamic>?)
|
||||
?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
avatar: json['avatar'] as String,
|
||||
banner: json['banner'] as String,
|
||||
avatar: json['avatar'] as String? ?? "",
|
||||
banner: json['banner'] as String? ?? "",
|
||||
description: json['description'] as String,
|
||||
name: json['name'] as String,
|
||||
nick: json['nick'] as String,
|
||||
permNodes: json['perm_nodes'] as Map<String, dynamic>,
|
||||
language: json['language'] as String,
|
||||
profile: json['profile'] == null
|
||||
? null
|
||||
: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
|
||||
@@ -56,6 +57,7 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
|
||||
'name': instance.name,
|
||||
'nick': instance.nick,
|
||||
'perm_nodes': instance.permNodes,
|
||||
'language': instance.language,
|
||||
'profile': instance.profile?.toJson(),
|
||||
'badges': instance.badges.map((e) => e.toJson()).toList(),
|
||||
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
||||
|
@@ -34,15 +34,17 @@ class SnAttachment with _$SnAttachment {
|
||||
required DateTime? cleanedAt,
|
||||
required bool isAnalyzed,
|
||||
required bool isSelfRef,
|
||||
required bool isIndexable,
|
||||
required SnAttachment? ref,
|
||||
required int? refId,
|
||||
required SnAttachmentPool? pool,
|
||||
required int poolId,
|
||||
required int? poolId,
|
||||
required int accountId,
|
||||
int? thumbnailId,
|
||||
SnAttachment? thumbnail,
|
||||
int? compressedId,
|
||||
SnAttachment? compressed,
|
||||
@Default([]) List<SnAttachmentBoost> boosts,
|
||||
@Default({}) Map<String, dynamic> usermeta,
|
||||
@Default({}) Map<String, dynamic> metadata,
|
||||
}) = _SnAttachment;
|
||||
@@ -109,3 +111,69 @@ class SnAttachmentPool with _$SnAttachmentPool {
|
||||
|
||||
factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAttachmentDestination with _$SnAttachmentDestination {
|
||||
const factory SnAttachmentDestination({
|
||||
@Default(0) int id,
|
||||
required String type,
|
||||
required String label,
|
||||
required String region,
|
||||
required bool isBoost,
|
||||
}) = _SnAttachmentDestination;
|
||||
|
||||
factory SnAttachmentDestination.fromJson(Map<String, Object?> json) => _$SnAttachmentDestinationFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnAttachmentBoost with _$SnAttachmentBoost {
|
||||
const factory SnAttachmentBoost({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required int status,
|
||||
required int destination,
|
||||
required int attachmentId,
|
||||
required SnAttachment attachment,
|
||||
required int account,
|
||||
}) = _SnAttachmentBoost;
|
||||
|
||||
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnSticker with _$SnSticker {
|
||||
const factory SnSticker({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required String alias,
|
||||
required String name,
|
||||
required int attachmentId,
|
||||
required SnAttachment attachment,
|
||||
required int packId,
|
||||
required SnStickerPack pack,
|
||||
required int accountId,
|
||||
}) = _SnSticker;
|
||||
|
||||
factory SnSticker.fromJson(Map<String, Object?> json) => _$SnStickerFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnStickerPack with _$SnStickerPack {
|
||||
const factory SnStickerPack({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required String prefix,
|
||||
required String name,
|
||||
required String description,
|
||||
required List<SnSticker>? stickers,
|
||||
required int accountId,
|
||||
}) = _SnStickerPack;
|
||||
|
||||
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
||||
: DateTime.parse(json['cleaned_at'] as String),
|
||||
isAnalyzed: json['is_analyzed'] as bool,
|
||||
isSelfRef: json['is_self_ref'] as bool,
|
||||
isIndexable: json['is_indexable'] as bool,
|
||||
ref: json['ref'] == null
|
||||
? null
|
||||
: SnAttachment.fromJson(json['ref'] as Map<String, dynamic>),
|
||||
@@ -37,7 +38,7 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
||||
pool: json['pool'] == null
|
||||
? null
|
||||
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
|
||||
poolId: (json['pool_id'] as num).toInt(),
|
||||
poolId: (json['pool_id'] as num?)?.toInt(),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
|
||||
thumbnail: json['thumbnail'] == null
|
||||
@@ -47,6 +48,11 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
||||
compressed: json['compressed'] == null
|
||||
? null
|
||||
: SnAttachment.fromJson(json['compressed'] as Map<String, dynamic>),
|
||||
boosts: (json['boosts'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) => SnAttachmentBoost.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
usermeta: json['usermeta'] as Map<String, dynamic>? ?? const {},
|
||||
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
|
||||
);
|
||||
@@ -71,6 +77,7 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
|
||||
'cleaned_at': instance.cleanedAt?.toIso8601String(),
|
||||
'is_analyzed': instance.isAnalyzed,
|
||||
'is_self_ref': instance.isSelfRef,
|
||||
'is_indexable': instance.isIndexable,
|
||||
'ref': instance.ref?.toJson(),
|
||||
'ref_id': instance.refId,
|
||||
'pool': instance.pool?.toJson(),
|
||||
@@ -80,6 +87,7 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
|
||||
'thumbnail': instance.thumbnail?.toJson(),
|
||||
'compressed_id': instance.compressedId,
|
||||
'compressed': instance.compressed?.toJson(),
|
||||
'boosts': instance.boosts.map((e) => e.toJson()).toList(),
|
||||
'usermeta': instance.usermeta,
|
||||
'metadata': instance.metadata,
|
||||
};
|
||||
@@ -159,3 +167,117 @@ Map<String, dynamic> _$$SnAttachmentPoolImplToJson(
|
||||
'config': instance.config,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
_$SnAttachmentDestinationImpl _$$SnAttachmentDestinationImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnAttachmentDestinationImpl(
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
type: json['type'] as String,
|
||||
label: json['label'] as String,
|
||||
region: json['region'] as String,
|
||||
isBoost: json['is_boost'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAttachmentDestinationImplToJson(
|
||||
_$SnAttachmentDestinationImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'type': instance.type,
|
||||
'label': instance.label,
|
||||
'region': instance.region,
|
||||
'is_boost': instance.isBoost,
|
||||
};
|
||||
|
||||
_$SnAttachmentBoostImpl _$$SnAttachmentBoostImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SnAttachmentBoostImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
status: (json['status'] as num).toInt(),
|
||||
destination: (json['destination'] as num).toInt(),
|
||||
attachmentId: (json['attachment_id'] as num).toInt(),
|
||||
attachment:
|
||||
SnAttachment.fromJson(json['attachment'] as Map<String, dynamic>),
|
||||
account: (json['account'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnAttachmentBoostImplToJson(
|
||||
_$SnAttachmentBoostImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'status': instance.status,
|
||||
'destination': instance.destination,
|
||||
'attachment_id': instance.attachmentId,
|
||||
'attachment': instance.attachment.toJson(),
|
||||
'account': instance.account,
|
||||
};
|
||||
|
||||
_$SnStickerImpl _$$SnStickerImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnStickerImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
alias: json['alias'] as String,
|
||||
name: json['name'] as String,
|
||||
attachmentId: (json['attachment_id'] as num).toInt(),
|
||||
attachment:
|
||||
SnAttachment.fromJson(json['attachment'] as Map<String, dynamic>),
|
||||
packId: (json['pack_id'] as num).toInt(),
|
||||
pack: SnStickerPack.fromJson(json['pack'] as Map<String, dynamic>),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnStickerImplToJson(_$SnStickerImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'alias': instance.alias,
|
||||
'name': instance.name,
|
||||
'attachment_id': instance.attachmentId,
|
||||
'attachment': instance.attachment.toJson(),
|
||||
'pack_id': instance.packId,
|
||||
'pack': instance.pack.toJson(),
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
_$SnStickerPackImpl _$$SnStickerPackImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnStickerPackImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
prefix: json['prefix'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
stickers: (json['stickers'] as List<dynamic>?)
|
||||
?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'prefix': instance.prefix,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'stickers': instance.stickers?.map((e) => e.toJson()).toList(),
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
@@ -16,6 +16,7 @@ class SnCheckInRecord with _$SnCheckInRecord {
|
||||
required DateTime? deletedAt,
|
||||
required int resultTier,
|
||||
required int resultExperience,
|
||||
required double resultCoin,
|
||||
required List<int> resultModifiers,
|
||||
required int accountId,
|
||||
}) = _SnCheckInRecord;
|
||||
|
@@ -26,6 +26,7 @@ mixin _$SnCheckInRecord {
|
||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||
int get resultTier => throw _privateConstructorUsedError;
|
||||
int get resultExperience => throw _privateConstructorUsedError;
|
||||
double get resultCoin => throw _privateConstructorUsedError;
|
||||
List<int> get resultModifiers => throw _privateConstructorUsedError;
|
||||
int get accountId => throw _privateConstructorUsedError;
|
||||
|
||||
@@ -52,6 +53,7 @@ abstract class $SnCheckInRecordCopyWith<$Res> {
|
||||
DateTime? deletedAt,
|
||||
int resultTier,
|
||||
int resultExperience,
|
||||
double resultCoin,
|
||||
List<int> resultModifiers,
|
||||
int accountId});
|
||||
}
|
||||
@@ -77,6 +79,7 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
||||
Object? deletedAt = freezed,
|
||||
Object? resultTier = null,
|
||||
Object? resultExperience = null,
|
||||
Object? resultCoin = null,
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
@@ -105,6 +108,10 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
||||
? _value.resultExperience
|
||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultCoin: null == resultCoin
|
||||
? _value.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _value.resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
@@ -132,6 +139,7 @@ abstract class _$$SnCheckInRecordImplCopyWith<$Res>
|
||||
DateTime? deletedAt,
|
||||
int resultTier,
|
||||
int resultExperience,
|
||||
double resultCoin,
|
||||
List<int> resultModifiers,
|
||||
int accountId});
|
||||
}
|
||||
@@ -155,6 +163,7 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
||||
Object? deletedAt = freezed,
|
||||
Object? resultTier = null,
|
||||
Object? resultExperience = null,
|
||||
Object? resultCoin = null,
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
@@ -183,6 +192,10 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
||||
? _value.resultExperience
|
||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultCoin: null == resultCoin
|
||||
? _value.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _value._resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
@@ -205,6 +218,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
required this.deletedAt,
|
||||
required this.resultTier,
|
||||
required this.resultExperience,
|
||||
required this.resultCoin,
|
||||
required final List<int> resultModifiers,
|
||||
required this.accountId})
|
||||
: _resultModifiers = resultModifiers,
|
||||
@@ -225,6 +239,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
final int resultTier;
|
||||
@override
|
||||
final int resultExperience;
|
||||
@override
|
||||
final double resultCoin;
|
||||
final List<int> _resultModifiers;
|
||||
@override
|
||||
List<int> get resultModifiers {
|
||||
@@ -238,7 +254,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -257,6 +273,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
other.resultTier == resultTier) &&
|
||||
(identical(other.resultExperience, resultExperience) ||
|
||||
other.resultExperience == resultExperience) &&
|
||||
(identical(other.resultCoin, resultCoin) ||
|
||||
other.resultCoin == resultCoin) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._resultModifiers, _resultModifiers) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
@@ -273,6 +291,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
deletedAt,
|
||||
resultTier,
|
||||
resultExperience,
|
||||
resultCoin,
|
||||
const DeepCollectionEquality().hash(_resultModifiers),
|
||||
accountId);
|
||||
|
||||
@@ -301,6 +320,7 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
|
||||
required final DateTime? deletedAt,
|
||||
required final int resultTier,
|
||||
required final int resultExperience,
|
||||
required final double resultCoin,
|
||||
required final List<int> resultModifiers,
|
||||
required final int accountId}) = _$SnCheckInRecordImpl;
|
||||
const _SnCheckInRecord._() : super._();
|
||||
@@ -321,6 +341,8 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
|
||||
@override
|
||||
int get resultExperience;
|
||||
@override
|
||||
double get resultCoin;
|
||||
@override
|
||||
List<int> get resultModifiers;
|
||||
@override
|
||||
int get accountId;
|
||||
|
@@ -17,6 +17,7 @@ _$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson(
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
resultTier: (json['result_tier'] as num).toInt(),
|
||||
resultExperience: (json['result_experience'] as num).toInt(),
|
||||
resultCoin: (json['result_coin'] as num).toDouble(),
|
||||
resultModifiers: (json['result_modifiers'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
@@ -32,6 +33,7 @@ Map<String, dynamic> _$$SnCheckInRecordImplToJson(
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'result_tier': instance.resultTier,
|
||||
'result_experience': instance.resultExperience,
|
||||
'result_coin': instance.resultCoin,
|
||||
'result_modifiers': instance.resultModifiers,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
38
lib/types/news.dart
Normal file
38
lib/types/news.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'news.freezed.dart';
|
||||
part 'news.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnNewsSource with _$SnNewsSource {
|
||||
const factory SnNewsSource({
|
||||
required String id,
|
||||
required String label,
|
||||
required String type,
|
||||
required String source,
|
||||
required int depth,
|
||||
required bool enabled,
|
||||
}) = _SnNewsSource;
|
||||
|
||||
factory SnNewsSource.fromJson(Map<String, dynamic> json) => _$SnNewsSourceFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnNewsArticle with _$SnNewsArticle {
|
||||
const factory SnNewsArticle({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required dynamic deletedAt,
|
||||
required String thumbnail,
|
||||
required String title,
|
||||
required String description,
|
||||
required String content,
|
||||
required String url,
|
||||
required String hash,
|
||||
required String source,
|
||||
required DateTime? publishedAt,
|
||||
}) = _SnNewsArticle;
|
||||
|
||||
factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json);
|
||||
}
|
660
lib/types/news.freezed.dart
Normal file
660
lib/types/news.freezed.dart
Normal file
@@ -0,0 +1,660 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'news.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) {
|
||||
return _SnNewsSource.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnNewsSource {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
String get label => throw _privateConstructorUsedError;
|
||||
String get type => throw _privateConstructorUsedError;
|
||||
String get source => throw _privateConstructorUsedError;
|
||||
int get depth => throw _privateConstructorUsedError;
|
||||
bool get enabled => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnNewsSource to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnNewsSourceCopyWith<SnNewsSource> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnNewsSourceCopyWith<$Res> {
|
||||
factory $SnNewsSourceCopyWith(
|
||||
SnNewsSource value, $Res Function(SnNewsSource) then) =
|
||||
_$SnNewsSourceCopyWithImpl<$Res, SnNewsSource>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String id,
|
||||
String label,
|
||||
String type,
|
||||
String source,
|
||||
int depth,
|
||||
bool enabled});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnNewsSourceCopyWithImpl<$Res, $Val extends SnNewsSource>
|
||||
implements $SnNewsSourceCopyWith<$Res> {
|
||||
_$SnNewsSourceCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? label = null,
|
||||
Object? type = null,
|
||||
Object? source = null,
|
||||
Object? depth = null,
|
||||
Object? enabled = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
label: null == label
|
||||
? _value.label
|
||||
: label // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
type: null == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
source: null == source
|
||||
? _value.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
depth: null == depth
|
||||
? _value.depth
|
||||
: depth // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
enabled: null == enabled
|
||||
? _value.enabled
|
||||
: enabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnNewsSourceImplCopyWith<$Res>
|
||||
implements $SnNewsSourceCopyWith<$Res> {
|
||||
factory _$$SnNewsSourceImplCopyWith(
|
||||
_$SnNewsSourceImpl value, $Res Function(_$SnNewsSourceImpl) then) =
|
||||
__$$SnNewsSourceImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String id,
|
||||
String label,
|
||||
String type,
|
||||
String source,
|
||||
int depth,
|
||||
bool enabled});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnNewsSourceImplCopyWithImpl<$Res>
|
||||
extends _$SnNewsSourceCopyWithImpl<$Res, _$SnNewsSourceImpl>
|
||||
implements _$$SnNewsSourceImplCopyWith<$Res> {
|
||||
__$$SnNewsSourceImplCopyWithImpl(
|
||||
_$SnNewsSourceImpl _value, $Res Function(_$SnNewsSourceImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? label = null,
|
||||
Object? type = null,
|
||||
Object? source = null,
|
||||
Object? depth = null,
|
||||
Object? enabled = null,
|
||||
}) {
|
||||
return _then(_$SnNewsSourceImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
label: null == label
|
||||
? _value.label
|
||||
: label // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
type: null == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
source: null == source
|
||||
? _value.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
depth: null == depth
|
||||
? _value.depth
|
||||
: depth // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
enabled: null == enabled
|
||||
? _value.enabled
|
||||
: enabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnNewsSourceImpl implements _SnNewsSource {
|
||||
const _$SnNewsSourceImpl(
|
||||
{required this.id,
|
||||
required this.label,
|
||||
required this.type,
|
||||
required this.source,
|
||||
required this.depth,
|
||||
required this.enabled});
|
||||
|
||||
factory _$SnNewsSourceImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnNewsSourceImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final String label;
|
||||
@override
|
||||
final String type;
|
||||
@override
|
||||
final String source;
|
||||
@override
|
||||
final int depth;
|
||||
@override
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnNewsSourceImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.label, label) || other.label == label) &&
|
||||
(identical(other.type, type) || other.type == type) &&
|
||||
(identical(other.source, source) || other.source == source) &&
|
||||
(identical(other.depth, depth) || other.depth == depth) &&
|
||||
(identical(other.enabled, enabled) || other.enabled == enabled));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, id, label, type, source, depth, enabled);
|
||||
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith =>
|
||||
__$$SnNewsSourceImplCopyWithImpl<_$SnNewsSourceImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnNewsSourceImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnNewsSource implements SnNewsSource {
|
||||
const factory _SnNewsSource(
|
||||
{required final String id,
|
||||
required final String label,
|
||||
required final String type,
|
||||
required final String source,
|
||||
required final int depth,
|
||||
required final bool enabled}) = _$SnNewsSourceImpl;
|
||||
|
||||
factory _SnNewsSource.fromJson(Map<String, dynamic> json) =
|
||||
_$SnNewsSourceImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get id;
|
||||
@override
|
||||
String get label;
|
||||
@override
|
||||
String get type;
|
||||
@override
|
||||
String get source;
|
||||
@override
|
||||
int get depth;
|
||||
@override
|
||||
bool get enabled;
|
||||
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) {
|
||||
return _SnNewsArticle.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnNewsArticle {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
dynamic get deletedAt => throw _privateConstructorUsedError;
|
||||
String get thumbnail => throw _privateConstructorUsedError;
|
||||
String get title => throw _privateConstructorUsedError;
|
||||
String get description => throw _privateConstructorUsedError;
|
||||
String get content => throw _privateConstructorUsedError;
|
||||
String get url => throw _privateConstructorUsedError;
|
||||
String get hash => throw _privateConstructorUsedError;
|
||||
String get source => throw _privateConstructorUsedError;
|
||||
DateTime? get publishedAt => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnNewsArticle to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnNewsArticleCopyWith<SnNewsArticle> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnNewsArticleCopyWith<$Res> {
|
||||
factory $SnNewsArticleCopyWith(
|
||||
SnNewsArticle value, $Res Function(SnNewsArticle) then) =
|
||||
_$SnNewsArticleCopyWithImpl<$Res, SnNewsArticle>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
String thumbnail,
|
||||
String title,
|
||||
String description,
|
||||
String content,
|
||||
String url,
|
||||
String hash,
|
||||
String source,
|
||||
DateTime? publishedAt});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnNewsArticleCopyWithImpl<$Res, $Val extends SnNewsArticle>
|
||||
implements $SnNewsArticleCopyWith<$Res> {
|
||||
_$SnNewsArticleCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? thumbnail = null,
|
||||
Object? title = null,
|
||||
Object? description = null,
|
||||
Object? content = null,
|
||||
Object? url = null,
|
||||
Object? hash = null,
|
||||
Object? source = null,
|
||||
Object? publishedAt = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
thumbnail: null == thumbnail
|
||||
? _value.thumbnail
|
||||
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: null == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
content: null == content
|
||||
? _value.content
|
||||
: content // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
url: null == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
hash: null == hash
|
||||
? _value.hash
|
||||
: hash // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
source: null == source
|
||||
? _value.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
publishedAt: freezed == publishedAt
|
||||
? _value.publishedAt
|
||||
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnNewsArticleImplCopyWith<$Res>
|
||||
implements $SnNewsArticleCopyWith<$Res> {
|
||||
factory _$$SnNewsArticleImplCopyWith(
|
||||
_$SnNewsArticleImpl value, $Res Function(_$SnNewsArticleImpl) then) =
|
||||
__$$SnNewsArticleImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
String thumbnail,
|
||||
String title,
|
||||
String description,
|
||||
String content,
|
||||
String url,
|
||||
String hash,
|
||||
String source,
|
||||
DateTime? publishedAt});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnNewsArticleImplCopyWithImpl<$Res>
|
||||
extends _$SnNewsArticleCopyWithImpl<$Res, _$SnNewsArticleImpl>
|
||||
implements _$$SnNewsArticleImplCopyWith<$Res> {
|
||||
__$$SnNewsArticleImplCopyWithImpl(
|
||||
_$SnNewsArticleImpl _value, $Res Function(_$SnNewsArticleImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? thumbnail = null,
|
||||
Object? title = null,
|
||||
Object? description = null,
|
||||
Object? content = null,
|
||||
Object? url = null,
|
||||
Object? hash = null,
|
||||
Object? source = null,
|
||||
Object? publishedAt = freezed,
|
||||
}) {
|
||||
return _then(_$SnNewsArticleImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
thumbnail: null == thumbnail
|
||||
? _value.thumbnail
|
||||
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: null == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
content: null == content
|
||||
? _value.content
|
||||
: content // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
url: null == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
hash: null == hash
|
||||
? _value.hash
|
||||
: hash // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
source: null == source
|
||||
? _value.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
publishedAt: freezed == publishedAt
|
||||
? _value.publishedAt
|
||||
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnNewsArticleImpl implements _SnNewsArticle {
|
||||
const _$SnNewsArticleImpl(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.thumbnail,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.content,
|
||||
required this.url,
|
||||
required this.hash,
|
||||
required this.source,
|
||||
required this.publishedAt});
|
||||
|
||||
factory _$SnNewsArticleImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnNewsArticleImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final dynamic deletedAt;
|
||||
@override
|
||||
final String thumbnail;
|
||||
@override
|
||||
final String title;
|
||||
@override
|
||||
final String description;
|
||||
@override
|
||||
final String content;
|
||||
@override
|
||||
final String url;
|
||||
@override
|
||||
final String hash;
|
||||
@override
|
||||
final String source;
|
||||
@override
|
||||
final DateTime? publishedAt;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnNewsArticle(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, source: $source, publishedAt: $publishedAt)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnNewsArticleImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
||||
(identical(other.thumbnail, thumbnail) ||
|
||||
other.thumbnail == thumbnail) &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
(identical(other.description, description) ||
|
||||
other.description == description) &&
|
||||
(identical(other.content, content) || other.content == content) &&
|
||||
(identical(other.url, url) || other.url == url) &&
|
||||
(identical(other.hash, hash) || other.hash == hash) &&
|
||||
(identical(other.source, source) || other.source == source) &&
|
||||
(identical(other.publishedAt, publishedAt) ||
|
||||
other.publishedAt == publishedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
const DeepCollectionEquality().hash(deletedAt),
|
||||
thumbnail,
|
||||
title,
|
||||
description,
|
||||
content,
|
||||
url,
|
||||
hash,
|
||||
source,
|
||||
publishedAt);
|
||||
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith =>
|
||||
__$$SnNewsArticleImplCopyWithImpl<_$SnNewsArticleImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnNewsArticleImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnNewsArticle implements SnNewsArticle {
|
||||
const factory _SnNewsArticle(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final dynamic deletedAt,
|
||||
required final String thumbnail,
|
||||
required final String title,
|
||||
required final String description,
|
||||
required final String content,
|
||||
required final String url,
|
||||
required final String hash,
|
||||
required final String source,
|
||||
required final DateTime? publishedAt}) = _$SnNewsArticleImpl;
|
||||
|
||||
factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =
|
||||
_$SnNewsArticleImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
dynamic get deletedAt;
|
||||
@override
|
||||
String get thumbnail;
|
||||
@override
|
||||
String get title;
|
||||
@override
|
||||
String get description;
|
||||
@override
|
||||
String get content;
|
||||
@override
|
||||
String get url;
|
||||
@override
|
||||
String get hash;
|
||||
@override
|
||||
String get source;
|
||||
@override
|
||||
DateTime? get publishedAt;
|
||||
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
61
lib/types/news.g.dart
Normal file
61
lib/types/news.g.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'news.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnNewsSourceImpl _$$SnNewsSourceImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnNewsSourceImpl(
|
||||
id: json['id'] as String,
|
||||
label: json['label'] as String,
|
||||
type: json['type'] as String,
|
||||
source: json['source'] as String,
|
||||
depth: (json['depth'] as num).toInt(),
|
||||
enabled: json['enabled'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnNewsSourceImplToJson(_$SnNewsSourceImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'label': instance.label,
|
||||
'type': instance.type,
|
||||
'source': instance.source,
|
||||
'depth': instance.depth,
|
||||
'enabled': instance.enabled,
|
||||
};
|
||||
|
||||
_$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnNewsArticleImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'],
|
||||
thumbnail: json['thumbnail'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
content: json['content'] as String,
|
||||
url: json['url'] as String,
|
||||
hash: json['hash'] as String,
|
||||
source: json['source'] as String,
|
||||
publishedAt: json['published_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['published_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt,
|
||||
'thumbnail': instance.thumbnail,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'content': instance.content,
|
||||
'url': instance.url,
|
||||
'hash': instance.hash,
|
||||
'source': instance.source,
|
||||
'published_at': instance.publishedAt?.toIso8601String(),
|
||||
};
|
37
lib/types/wallet.dart
Normal file
37
lib/types/wallet.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'wallet.freezed.dart';
|
||||
part 'wallet.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnWallet with _$SnWallet {
|
||||
const factory SnWallet({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required String balance,
|
||||
required String password,
|
||||
required int accountId,
|
||||
}) = _SnWallet;
|
||||
|
||||
factory SnWallet.fromJson(Map<String, dynamic> json) => _$SnWalletFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnTransaction with _$SnTransaction {
|
||||
const factory SnTransaction({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required String remark,
|
||||
required String amount,
|
||||
required SnWallet? payer,
|
||||
required SnWallet? payee,
|
||||
required int? payerId,
|
||||
required int? payeeId,
|
||||
}) = _SnTransaction;
|
||||
|
||||
factory SnTransaction.fromJson(Map<String, dynamic> json) => _$SnTransactionFromJson(json);
|
||||
}
|
666
lib/types/wallet.freezed.dart
Normal file
666
lib/types/wallet.freezed.dart
Normal file
@@ -0,0 +1,666 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'wallet.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
SnWallet _$SnWalletFromJson(Map<String, dynamic> json) {
|
||||
return _SnWallet.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnWallet {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||
String get balance => throw _privateConstructorUsedError;
|
||||
String get password => throw _privateConstructorUsedError;
|
||||
int get accountId => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnWallet to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnWallet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnWalletCopyWith<SnWallet> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnWalletCopyWith<$Res> {
|
||||
factory $SnWalletCopyWith(SnWallet value, $Res Function(SnWallet) then) =
|
||||
_$SnWalletCopyWithImpl<$Res, SnWallet>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String balance,
|
||||
String password,
|
||||
int accountId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnWalletCopyWithImpl<$Res, $Val extends SnWallet>
|
||||
implements $SnWalletCopyWith<$Res> {
|
||||
_$SnWalletCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnWallet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? balance = null,
|
||||
Object? password = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
balance: null == balance
|
||||
? _value.balance
|
||||
: balance // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
password: null == password
|
||||
? _value.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnWalletImplCopyWith<$Res>
|
||||
implements $SnWalletCopyWith<$Res> {
|
||||
factory _$$SnWalletImplCopyWith(
|
||||
_$SnWalletImpl value, $Res Function(_$SnWalletImpl) then) =
|
||||
__$$SnWalletImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String balance,
|
||||
String password,
|
||||
int accountId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnWalletImplCopyWithImpl<$Res>
|
||||
extends _$SnWalletCopyWithImpl<$Res, _$SnWalletImpl>
|
||||
implements _$$SnWalletImplCopyWith<$Res> {
|
||||
__$$SnWalletImplCopyWithImpl(
|
||||
_$SnWalletImpl _value, $Res Function(_$SnWalletImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnWallet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? balance = null,
|
||||
Object? password = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
return _then(_$SnWalletImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
balance: null == balance
|
||||
? _value.balance
|
||||
: balance // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
password: null == password
|
||||
? _value.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnWalletImpl implements _SnWallet {
|
||||
const _$SnWalletImpl(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.balance,
|
||||
required this.password,
|
||||
required this.accountId});
|
||||
|
||||
factory _$SnWalletImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnWalletImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final String balance;
|
||||
@override
|
||||
final String password;
|
||||
@override
|
||||
final int accountId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, password: $password, accountId: $accountId)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnWalletImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.balance, balance) || other.balance == balance) &&
|
||||
(identical(other.password, password) ||
|
||||
other.password == password) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||
deletedAt, balance, password, accountId);
|
||||
|
||||
/// Create a copy of SnWallet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnWalletImplCopyWith<_$SnWalletImpl> get copyWith =>
|
||||
__$$SnWalletImplCopyWithImpl<_$SnWalletImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnWalletImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnWallet implements SnWallet {
|
||||
const factory _SnWallet(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final DateTime? deletedAt,
|
||||
required final String balance,
|
||||
required final String password,
|
||||
required final int accountId}) = _$SnWalletImpl;
|
||||
|
||||
factory _SnWallet.fromJson(Map<String, dynamic> json) =
|
||||
_$SnWalletImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
DateTime? get deletedAt;
|
||||
@override
|
||||
String get balance;
|
||||
@override
|
||||
String get password;
|
||||
@override
|
||||
int get accountId;
|
||||
|
||||
/// Create a copy of SnWallet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnWalletImplCopyWith<_$SnWalletImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnTransaction _$SnTransactionFromJson(Map<String, dynamic> json) {
|
||||
return _SnTransaction.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnTransaction {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||
String get remark => throw _privateConstructorUsedError;
|
||||
String get amount => throw _privateConstructorUsedError;
|
||||
SnWallet? get payer => throw _privateConstructorUsedError;
|
||||
SnWallet? get payee => throw _privateConstructorUsedError;
|
||||
int? get payerId => throw _privateConstructorUsedError;
|
||||
int? get payeeId => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnTransaction to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnTransactionCopyWith<SnTransaction> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnTransactionCopyWith<$Res> {
|
||||
factory $SnTransactionCopyWith(
|
||||
SnTransaction value, $Res Function(SnTransaction) then) =
|
||||
_$SnTransactionCopyWithImpl<$Res, SnTransaction>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String remark,
|
||||
String amount,
|
||||
SnWallet? payer,
|
||||
SnWallet? payee,
|
||||
int? payerId,
|
||||
int? payeeId});
|
||||
|
||||
$SnWalletCopyWith<$Res>? get payer;
|
||||
$SnWalletCopyWith<$Res>? get payee;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnTransactionCopyWithImpl<$Res, $Val extends SnTransaction>
|
||||
implements $SnTransactionCopyWith<$Res> {
|
||||
_$SnTransactionCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? remark = null,
|
||||
Object? amount = null,
|
||||
Object? payer = freezed,
|
||||
Object? payee = freezed,
|
||||
Object? payerId = freezed,
|
||||
Object? payeeId = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
remark: null == remark
|
||||
? _value.remark
|
||||
: remark // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
amount: null == amount
|
||||
? _value.amount
|
||||
: amount // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
payer: freezed == payer
|
||||
? _value.payer
|
||||
: payer // ignore: cast_nullable_to_non_nullable
|
||||
as SnWallet?,
|
||||
payee: freezed == payee
|
||||
? _value.payee
|
||||
: payee // ignore: cast_nullable_to_non_nullable
|
||||
as SnWallet?,
|
||||
payerId: freezed == payerId
|
||||
? _value.payerId
|
||||
: payerId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
payeeId: freezed == payeeId
|
||||
? _value.payeeId
|
||||
: payeeId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnWalletCopyWith<$Res>? get payer {
|
||||
if (_value.payer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnWalletCopyWith<$Res>(_value.payer!, (value) {
|
||||
return _then(_value.copyWith(payer: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnWalletCopyWith<$Res>? get payee {
|
||||
if (_value.payee == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnWalletCopyWith<$Res>(_value.payee!, (value) {
|
||||
return _then(_value.copyWith(payee: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnTransactionImplCopyWith<$Res>
|
||||
implements $SnTransactionCopyWith<$Res> {
|
||||
factory _$$SnTransactionImplCopyWith(
|
||||
_$SnTransactionImpl value, $Res Function(_$SnTransactionImpl) then) =
|
||||
__$$SnTransactionImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String remark,
|
||||
String amount,
|
||||
SnWallet? payer,
|
||||
SnWallet? payee,
|
||||
int? payerId,
|
||||
int? payeeId});
|
||||
|
||||
@override
|
||||
$SnWalletCopyWith<$Res>? get payer;
|
||||
@override
|
||||
$SnWalletCopyWith<$Res>? get payee;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnTransactionImplCopyWithImpl<$Res>
|
||||
extends _$SnTransactionCopyWithImpl<$Res, _$SnTransactionImpl>
|
||||
implements _$$SnTransactionImplCopyWith<$Res> {
|
||||
__$$SnTransactionImplCopyWithImpl(
|
||||
_$SnTransactionImpl _value, $Res Function(_$SnTransactionImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? remark = null,
|
||||
Object? amount = null,
|
||||
Object? payer = freezed,
|
||||
Object? payee = freezed,
|
||||
Object? payerId = freezed,
|
||||
Object? payeeId = freezed,
|
||||
}) {
|
||||
return _then(_$SnTransactionImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
remark: null == remark
|
||||
? _value.remark
|
||||
: remark // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
amount: null == amount
|
||||
? _value.amount
|
||||
: amount // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
payer: freezed == payer
|
||||
? _value.payer
|
||||
: payer // ignore: cast_nullable_to_non_nullable
|
||||
as SnWallet?,
|
||||
payee: freezed == payee
|
||||
? _value.payee
|
||||
: payee // ignore: cast_nullable_to_non_nullable
|
||||
as SnWallet?,
|
||||
payerId: freezed == payerId
|
||||
? _value.payerId
|
||||
: payerId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
payeeId: freezed == payeeId
|
||||
? _value.payeeId
|
||||
: payeeId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnTransactionImpl implements _SnTransaction {
|
||||
const _$SnTransactionImpl(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.remark,
|
||||
required this.amount,
|
||||
required this.payer,
|
||||
required this.payee,
|
||||
required this.payerId,
|
||||
required this.payeeId});
|
||||
|
||||
factory _$SnTransactionImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnTransactionImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final String remark;
|
||||
@override
|
||||
final String amount;
|
||||
@override
|
||||
final SnWallet? payer;
|
||||
@override
|
||||
final SnWallet? payee;
|
||||
@override
|
||||
final int? payerId;
|
||||
@override
|
||||
final int? payeeId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnTransactionImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.remark, remark) || other.remark == remark) &&
|
||||
(identical(other.amount, amount) || other.amount == amount) &&
|
||||
(identical(other.payer, payer) || other.payer == payer) &&
|
||||
(identical(other.payee, payee) || other.payee == payee) &&
|
||||
(identical(other.payerId, payerId) || other.payerId == payerId) &&
|
||||
(identical(other.payeeId, payeeId) || other.payeeId == payeeId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||
deletedAt, remark, amount, payer, payee, payerId, payeeId);
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnTransactionImplCopyWith<_$SnTransactionImpl> get copyWith =>
|
||||
__$$SnTransactionImplCopyWithImpl<_$SnTransactionImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnTransactionImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnTransaction implements SnTransaction {
|
||||
const factory _SnTransaction(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final DateTime? deletedAt,
|
||||
required final String remark,
|
||||
required final String amount,
|
||||
required final SnWallet? payer,
|
||||
required final SnWallet? payee,
|
||||
required final int? payerId,
|
||||
required final int? payeeId}) = _$SnTransactionImpl;
|
||||
|
||||
factory _SnTransaction.fromJson(Map<String, dynamic> json) =
|
||||
_$SnTransactionImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
DateTime? get deletedAt;
|
||||
@override
|
||||
String get remark;
|
||||
@override
|
||||
String get amount;
|
||||
@override
|
||||
SnWallet? get payer;
|
||||
@override
|
||||
SnWallet? get payee;
|
||||
@override
|
||||
int? get payerId;
|
||||
@override
|
||||
int? get payeeId;
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnTransactionImplCopyWith<_$SnTransactionImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
65
lib/types/wallet.g.dart
Normal file
65
lib/types/wallet.g.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'wallet.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnWalletImpl _$$SnWalletImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnWalletImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
balance: json['balance'] as String,
|
||||
password: json['password'] as String,
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnWalletImplToJson(_$SnWalletImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'balance': instance.balance,
|
||||
'password': instance.password,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
_$SnTransactionImpl _$$SnTransactionImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnTransactionImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
remark: json['remark'] as String,
|
||||
amount: json['amount'] as String,
|
||||
payer: json['payer'] == null
|
||||
? null
|
||||
: SnWallet.fromJson(json['payer'] as Map<String, dynamic>),
|
||||
payee: json['payee'] == null
|
||||
? null
|
||||
: SnWallet.fromJson(json['payee'] as Map<String, dynamic>),
|
||||
payerId: (json['payer_id'] as num?)?.toInt(),
|
||||
payeeId: (json['payee_id'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnTransactionImplToJson(_$SnTransactionImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'remark': instance.remark,
|
||||
'amount': instance.amount,
|
||||
'payer': instance.payer?.toJson(),
|
||||
'payee': instance.payee?.toJson(),
|
||||
'payer_id': instance.payerId,
|
||||
'payee_id': instance.payeeId,
|
||||
};
|
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class AboutScreen extends StatelessWidget {
|
||||
@@ -12,7 +13,12 @@ class AboutScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
||||
|
||||
return SizedBox(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAbout').tr(),
|
||||
),
|
||||
body: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -104,6 +110,7 @@ class AboutScreen extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
164
lib/widgets/account/account_popover.dart
Normal file
164
lib/widgets/account/account_popover.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/experience.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class AccountPopoverCard extends StatelessWidget {
|
||||
final SnAccount data;
|
||||
|
||||
const AccountPopoverCard({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (data.banner.isNotEmpty)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(data.banner),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Top padding
|
||||
Gap(16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountImage(
|
||||
content: data.avatar,
|
||||
radius: 20,
|
||||
),
|
||||
Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(data.nick).bold(),
|
||||
Text('@${data.name}').fontSize(13).opacity(0.75),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
GoRouter.of(context).pushNamed(
|
||||
'accountProfilePage',
|
||||
pathParameters: {'name': data.name},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.chevron_right),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
const Gap(8)
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
const Gap(16),
|
||||
Wrap(
|
||||
children: data.badges
|
||||
.map(
|
||||
(ele) => Tooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
|
||||
if (ele.metadata['title'] != null)
|
||||
TextSpan(
|
||||
text: '\n${ele.metadata['title']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: DateFormat.yMEd().format(ele.createdAt),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
|
||||
color: kBadgesMeta[ele.type]?.$3,
|
||||
fill: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
).padding(horizontal: 24),
|
||||
const Gap(8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.star),
|
||||
const Gap(8),
|
||||
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
|
||||
const Gap(8),
|
||||
Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
|
||||
const Gap(8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: LinearProgressIndicator(
|
||||
value: calcLevelUpProgress(data.profile?.experience ?? 0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
).alignment(Alignment.centerLeft),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24),
|
||||
FutureBuilder(
|
||||
future: sn.client.get('/cgi/id/users/${data.name}/status'),
|
||||
builder: (context, snapshot) {
|
||||
final SnAccountStatusInfo? status =
|
||||
snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null;
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.circle,
|
||||
fill: 1,
|
||||
size: 16,
|
||||
color: (status?.isOnline ?? false) ? Colors.green : Colors.grey,
|
||||
).padding(all: 4),
|
||||
const Gap(8),
|
||||
Text(
|
||||
status != null
|
||||
? status.isOnline
|
||||
? 'accountStatusOnline'.tr()
|
||||
: 'accountStatusOffline'.tr()
|
||||
: 'loading'.tr(),
|
||||
),
|
||||
if (status != null && !status.isOnline && status.lastSeenAt != null)
|
||||
Text(
|
||||
'accountStatusLastSeen'.tr(args: [
|
||||
status.lastSeenAt != null
|
||||
? RelativeTime(context).format(
|
||||
status.lastSeenAt!.toLocal(),
|
||||
)
|
||||
: 'unknown',
|
||||
]),
|
||||
).padding(left: 6).opacity(0.75),
|
||||
],
|
||||
).padding(horizontal: 24);
|
||||
},
|
||||
),
|
||||
// Bottom padding
|
||||
const Gap(16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@@ -10,8 +10,8 @@ import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class AttachmentInputDialog extends StatefulWidget {
|
||||
final String? title;
|
||||
|
||||
const AttachmentInputDialog({super.key, required this.title});
|
||||
final bool? analyzeNow;
|
||||
const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false});
|
||||
|
||||
@override
|
||||
State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
|
||||
@@ -53,6 +53,7 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
_thumbnailFile!.path,
|
||||
'interactive',
|
||||
null,
|
||||
analyzeNow: widget.analyzeNow ?? false,
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, attachment);
|
||||
@@ -77,7 +78,8 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
controller: _randomIdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldAttachmentRandomId'.tr(),
|
||||
border: const OutlineInputBorder(),
|
||||
border: const UnderlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -18,9 +20,11 @@ import 'package:uuid/uuid.dart';
|
||||
class AttachmentItem extends StatelessWidget {
|
||||
final SnAttachment? data;
|
||||
final String? heroTag;
|
||||
final BoxFit fit;
|
||||
|
||||
const AttachmentItem({
|
||||
super.key,
|
||||
this.fit = BoxFit.cover,
|
||||
required this.data,
|
||||
required this.heroTag,
|
||||
});
|
||||
@@ -41,7 +45,7 @@ class AttachmentItem extends StatelessWidget {
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(data!.rid),
|
||||
key: Key('attachment-${data!.rid}-$tag'),
|
||||
fit: BoxFit.cover,
|
||||
fit: fit,
|
||||
),
|
||||
);
|
||||
case 'video':
|
||||
@@ -62,14 +66,12 @@ class AttachmentItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (data!.contentRating > 0) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return _AttachmentItemSensitiveBlur(
|
||||
isCompact: constraints.maxHeight < 360,
|
||||
child: _buildContent(context),
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return _buildContent(context);
|
||||
@@ -176,6 +178,7 @@ class _AttachmentItemContentVideo extends StatefulWidget {
|
||||
|
||||
class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo> {
|
||||
bool _showContent = false;
|
||||
bool _showOriginal = false;
|
||||
|
||||
Player? _videoPlayer;
|
||||
VideoController? _videoController;
|
||||
@@ -184,15 +187,29 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
||||
setState(() => _showContent = true);
|
||||
MediaKit.ensureInitialized();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final url = sn.getAttachmentUrl(widget.data.rid);
|
||||
final url = _showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid);
|
||||
_videoPlayer = Player();
|
||||
_videoController = VideoController(_videoPlayer!);
|
||||
_videoPlayer!.open(Media(url), play: !widget.isAutoload);
|
||||
}
|
||||
|
||||
void _toggleOriginal() {
|
||||
if (!mounted) return;
|
||||
if (widget.data.compressedId == null) return;
|
||||
setState(() => _showOriginal = !_showOriginal);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
_videoPlayer?.open(
|
||||
Media(
|
||||
_showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid),
|
||||
),
|
||||
play: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_showOriginal = widget.data.compressedId == null;
|
||||
if (widget.isAutoload) _startLoad();
|
||||
}
|
||||
|
||||
@@ -297,9 +314,48 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
||||
);
|
||||
}
|
||||
|
||||
return Video(
|
||||
return MaterialDesktopVideoControlsTheme(
|
||||
key: Key('material-desktop-video-controls-theme-$_showOriginal'),
|
||||
normal: MaterialDesktopVideoControlsThemeData(
|
||||
buttonBarButtonSize: 24,
|
||||
buttonBarButtonColor: Colors.white,
|
||||
topButtonBarMargin: EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||
topButtonBar: [
|
||||
const Spacer(),
|
||||
MaterialDesktopCustomButton(
|
||||
iconSize: 24,
|
||||
onPressed: _toggleOriginal,
|
||||
icon: Icon(
|
||||
_showOriginal ? Symbols.high_quality : Symbols.sd,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
fullscreen: const MaterialDesktopVideoControlsThemeData(),
|
||||
child: MaterialVideoControlsTheme(
|
||||
key: Key('material-video-controls-theme-$_showOriginal'),
|
||||
normal: MaterialVideoControlsThemeData(
|
||||
buttonBarButtonSize: 24,
|
||||
buttonBarButtonColor: Colors.white,
|
||||
topButtonBarMargin: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
topButtonBar: [
|
||||
const Spacer(),
|
||||
MaterialDesktopCustomButton(
|
||||
iconSize: 24,
|
||||
onPressed: _toggleOriginal,
|
||||
icon: _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
fullscreen: const MaterialVideoControlsThemeData(),
|
||||
child: Video(
|
||||
controller: _videoController!,
|
||||
aspectRatio: ratio,
|
||||
controls:
|
||||
!kIsWeb && (Platform.isAndroid || Platform.isIOS) ? MaterialVideoControls : MaterialDesktopVideoControls,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -4,8 +4,8 @@ import 'package:collection/collection.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
@@ -14,19 +14,25 @@ import 'package:uuid/uuid.dart';
|
||||
class AttachmentList extends StatefulWidget {
|
||||
final List<SnAttachment?> data;
|
||||
final bool bordered;
|
||||
final bool noGrow;
|
||||
final bool isFlatted;
|
||||
final bool gridded;
|
||||
final bool columned;
|
||||
final BoxFit fit;
|
||||
final double? maxHeight;
|
||||
final EdgeInsets? listPadding;
|
||||
final double? minWidth;
|
||||
final double? maxWidth;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const AttachmentList({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.bordered = false,
|
||||
this.noGrow = false,
|
||||
this.isFlatted = false,
|
||||
this.gridded = false,
|
||||
this.columned = false,
|
||||
this.fit = BoxFit.cover,
|
||||
this.maxHeight,
|
||||
this.listPadding,
|
||||
this.minWidth,
|
||||
this.maxWidth,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
|
||||
@@ -41,8 +47,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
(_) => const Uuid().v4(),
|
||||
);
|
||||
|
||||
static const double kAttachmentMaxWidth = 640;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
@@ -51,9 +55,8 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
|
||||
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||
final constraints = BoxConstraints(
|
||||
minWidth: 80,
|
||||
maxHeight: widget.maxHeight ?? double.infinity,
|
||||
maxWidth: layoutConstraints.maxWidth - 20,
|
||||
minWidth: widget.minWidth ?? 80,
|
||||
maxHeight: widget.maxHeight ?? MediaQuery.of(context).size.height,
|
||||
);
|
||||
|
||||
if (widget.data.isEmpty) return const SizedBox.shrink();
|
||||
@@ -67,27 +70,15 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
.toDouble();
|
||||
|
||||
return Container(
|
||||
constraints: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? constraints.copyWith(
|
||||
maxWidth: math.min(
|
||||
constraints.maxWidth,
|
||||
kAttachmentMaxWidth,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
padding: widget.padding ?? EdgeInsets.zero,
|
||||
constraints: constraints,
|
||||
child: GestureDetector(
|
||||
child: AspectRatio(
|
||||
aspectRatio: singleAspectRatio,
|
||||
child: GestureDetector(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) || widget.noGrow) {
|
||||
return Padding(
|
||||
// Single child list-like displaying
|
||||
padding: widget.listPadding ?? EdgeInsets.zero,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(top: borderSide, bottom: borderSide),
|
||||
border: Border.fromBorderSide(borderSide),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
@@ -95,23 +86,10 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
child: AttachmentItem(
|
||||
data: widget.data[0],
|
||||
heroTag: heroTags[0],
|
||||
fit: widget.fit,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(top: borderSide, bottom: borderSide),
|
||||
),
|
||||
child: AttachmentItem(
|
||||
data: widget.data[0],
|
||||
heroTag: heroTags.first,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
|
||||
@@ -126,19 +104,15 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.isFlatted) {
|
||||
return Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => AspectRatio(
|
||||
aspectRatio: (ele?.data['ratio'] ?? 1).toDouble(),
|
||||
child: Container(
|
||||
final fullOfImage =
|
||||
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
|
||||
|
||||
if (widget.gridded && fullOfImage) {
|
||||
return Container(
|
||||
margin: widget.padding ?? EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
@@ -149,30 +123,92 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: StaggeredGrid.count(
|
||||
crossAxisCount: math.min(widget.data.length, 2),
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisSpacing: 4,
|
||||
children: widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => GestureDetector(
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
child: AttachmentItem(
|
||||
data: ele,
|
||||
heroTag: heroTags[idx],
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data.where((ele) => ele != null).cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ((!fullOfImage && widget.gridded) || widget.columned) {
|
||||
return Container(
|
||||
margin: widget.padding ?? EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: Column(
|
||||
children: widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => GestureDetector(
|
||||
child: AspectRatio(
|
||||
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
child: AttachmentItem(
|
||||
data: ele,
|
||||
heroTag: heroTags[idx],
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
.expand((ele) => [ele, const Divider(height: 1)])
|
||||
.toList()
|
||||
..removeLast(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: (widget.data.firstOrNull?.data['ratio'] ?? 1).toDouble(),
|
||||
aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||
child: ScrollConfiguration(
|
||||
behavior: _AttachmentListScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
padding: widget.padding,
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.data.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
constraints: constraints,
|
||||
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
|
||||
child: AspectRatio(
|
||||
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||
child: GestureDetector(
|
||||
@@ -180,8 +216,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data:
|
||||
widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
||||
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
@@ -223,7 +258,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
padding: widget.listPadding,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
|
@@ -129,6 +129,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
|
||||
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
bool _showDetail = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
@@ -144,10 +146,13 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
onDismissed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
direction: DismissiblePageDismissDirection.down,
|
||||
direction: DismissiblePageDismissDirection.none,
|
||||
backgroundColor: Colors.transparent,
|
||||
isFullScreen: true,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Stack(
|
||||
children: [
|
||||
Builder(builder: (context) {
|
||||
@@ -231,7 +236,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
children: [
|
||||
IgnorePointer(
|
||||
child: AccountImage(
|
||||
content: account!.avatar,
|
||||
content: account?.avatar,
|
||||
radius: 19,
|
||||
),
|
||||
),
|
||||
@@ -246,7 +251,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
account.nick,
|
||||
account?.nick ?? 'unknown'.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
@@ -264,7 +269,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
onTap: _isDownloading
|
||||
? null
|
||||
: () => _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
|
||||
: () =>
|
||||
_saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: !_isDownloading
|
||||
@@ -312,11 +318,6 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
]),
|
||||
style: metaTextStyle,
|
||||
).padding(right: 2),
|
||||
if (item.metadata['exif']?['ShutterSpeed'] != null)
|
||||
Text(
|
||||
item.metadata['exif']?['ShutterSpeed'],
|
||||
style: metaTextStyle,
|
||||
).padding(right: 2),
|
||||
if (item.metadata['exif']?['ISO'] != null)
|
||||
Text(
|
||||
'ISO${item.metadata['exif']?['ISO']}',
|
||||
@@ -327,14 +328,15 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
'f/${item.metadata['exif']?['Aperture']}',
|
||||
style: metaTextStyle,
|
||||
).padding(right: 2),
|
||||
if (item.metadata['exif']?['Megapixels'] != null && item.metadata['exif']?['Model'] != null)
|
||||
if (item.metadata['exif']?['Megapixels'] != null &&
|
||||
item.metadata['exif']?['Model'] != null)
|
||||
Text(
|
||||
'${item.metadata['exif']?['Megapixels']}MP',
|
||||
style: metaTextStyle,
|
||||
)
|
||||
else
|
||||
Text(
|
||||
'${item.size} Bytes',
|
||||
item.size.formatBytes(),
|
||||
style: metaTextStyle,
|
||||
),
|
||||
if (item.metadata['width'] != null && item.metadata['height'] != null)
|
||||
@@ -362,6 +364,134 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
],
|
||||
),
|
||||
),
|
||||
onVerticalDragUpdate: (details) {
|
||||
if (_showDetail) return;
|
||||
if (details.delta.dy <= -40) {
|
||||
_showDetail = true;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _AttachmentZoomDetailPopup(
|
||||
data: widget.data.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
|
||||
),
|
||||
).then((_) {
|
||||
_showDetail = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AttachmentZoomDetailPopup extends StatelessWidget {
|
||||
final SnAttachment data;
|
||||
|
||||
const _AttachmentZoomDetailPopup({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final account = ud.getAccountFromCache(data.accountId);
|
||||
|
||||
const tableGap = TableRow(
|
||||
children: [
|
||||
TableCell(child: SizedBox(height: 16)),
|
||||
TableCell(child: SizedBox(height: 16)),
|
||||
],
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.info, size: 24),
|
||||
const Gap(16),
|
||||
Text('attachmentDetailInfo').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Table(
|
||||
columnWidths: {
|
||||
0: IntrinsicColumnWidth(),
|
||||
1: FlexColumnWidth(),
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
child: Text('attachmentUploadBy').tr().padding(right: 16),
|
||||
),
|
||||
TableCell(
|
||||
child: Row(
|
||||
children: [
|
||||
if (data.accountId > 0)
|
||||
AccountImage(
|
||||
content: account?.avatar,
|
||||
radius: 8,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()),
|
||||
const Gap(8),
|
||||
Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
tableGap,
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(child: Text('Mimetype').padding(right: 16)),
|
||||
TableCell(child: Text(data.mimetype)),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(child: Text('Size').padding(right: 16)),
|
||||
TableCell(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(data.size.formatBytes()),
|
||||
const Gap(12),
|
||||
Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(child: Text('Name').padding(right: 16)),
|
||||
TableCell(child: Text(data.name)),
|
||||
],
|
||||
),
|
||||
if (data.hash.isNotEmpty)
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(child: Text('Hash').padding(right: 16)),
|
||||
TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
|
||||
],
|
||||
),
|
||||
tableGap,
|
||||
...(data.metadata['exif']?.keys.map((k) => TableRow(
|
||||
children: [
|
||||
TableCell(child: Text(k).padding(right: 16)),
|
||||
TableCell(child: Text(data.metadata['exif'][k].toString())),
|
||||
],
|
||||
)) ??
|
||||
[]),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
86
lib/widgets/attachment/pending_attachment_alt.dart
Normal file
86
lib/widgets/attachment/pending_attachment_alt.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class PendingAttachmentAltDialog extends StatefulWidget {
|
||||
final PostWriteMedia media;
|
||||
const PendingAttachmentAltDialog({super.key, required this.media});
|
||||
|
||||
@override
|
||||
State<PendingAttachmentAltDialog> createState() => _PendingAttachmentAltDialogState();
|
||||
}
|
||||
|
||||
class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> {
|
||||
final _contentController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_contentController.text = widget.media.attachment!.alt;
|
||||
}
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _performAction() async {
|
||||
if (_isBusy) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
final result = await attach.updateOne(
|
||||
widget.media.attachment!,
|
||||
alt: _contentController.text,
|
||||
);
|
||||
if (!mounted) return;
|
||||
attach.putCache([result]);
|
||||
Navigator.pop(context, result);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contentController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('attachmentSetAlt').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _contentController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldAttachmentAlt'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('dialogDismiss'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _performAction(),
|
||||
child: Text('dialogConfirm'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
120
lib/widgets/attachment/pending_attachment_boost.dart
Normal file
120
lib/widgets/attachment/pending_attachment_boost.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class PendingAttachmentBoostDialog extends StatefulWidget {
|
||||
final PostWriteMedia media;
|
||||
|
||||
const PendingAttachmentBoostDialog({super.key, required this.media});
|
||||
|
||||
@override
|
||||
State<PendingAttachmentBoostDialog> createState() => _PendingAttachmentBoostDialogState();
|
||||
}
|
||||
|
||||
class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDialog> {
|
||||
List<SnAttachmentDestination>? _regions;
|
||||
SnAttachmentDestination? _selectedRegion;
|
||||
|
||||
Future<void> _fetchRegions() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/uc/destinations');
|
||||
setState(() {
|
||||
_regions = List<SnAttachmentDestination>.from(
|
||||
resp.data?.map((e) => SnAttachmentDestination.fromJson(e)) ?? [],
|
||||
).cast<SnAttachmentDestination>().where((ele) => ele.isBoost).toList();
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _performAction() async {
|
||||
if (_isBusy) return;
|
||||
if (_selectedRegion == null) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.post('/cgi/uc/boosts', data: {
|
||||
'attachment': widget.media.attachment!.id,
|
||||
'destination': _selectedRegion!.id,
|
||||
});
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, SnAttachmentBoost.fromJson(resp.data));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchRegions();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('attachmentBoost').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('attachmentBoostHint').tr(),
|
||||
const Gap(16),
|
||||
Text('attachmentDestinationRegion').tr().fontSize(18),
|
||||
const Gap(8),
|
||||
Card(
|
||||
child: _regions == null
|
||||
? const CircularProgressIndicator().center().padding(all: 16)
|
||||
: Column(
|
||||
children: _regions!.map(
|
||||
(ele) {
|
||||
return RadioListTile(
|
||||
title: Text(ele.label).tr(),
|
||||
subtitle: Text(
|
||||
'attachmentDestinationRegion${ele.region}'.trExists()
|
||||
? 'attachmentDestinationRegion${ele.region}'.tr()
|
||||
: ele.region,
|
||||
),
|
||||
selected: _selectedRegion == ele,
|
||||
value: ele,
|
||||
groupValue: _selectedRegion,
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _selectedRegion = value);
|
||||
},
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('dialogDismiss'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _performAction(),
|
||||
child: Text('dialogConfirm'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,14 +1,19 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:popover/popover.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/account_popover.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/context_menu.dart';
|
||||
import 'package:surface/widgets/link_preview.dart';
|
||||
@@ -24,6 +29,7 @@ class ChatMessage extends StatelessWidget {
|
||||
final Function(SnChatMessage)? onReply;
|
||||
final Function(SnChatMessage)? onEdit;
|
||||
final Function(SnChatMessage)? onDelete;
|
||||
final EdgeInsets padding;
|
||||
|
||||
const ChatMessage({
|
||||
super.key,
|
||||
@@ -35,6 +41,7 @@ class ChatMessage extends StatelessWidget {
|
||||
this.onReply,
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
this.padding = const EdgeInsets.only(left: 12, right: 12),
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -47,13 +54,15 @@ class ChatMessage extends StatelessWidget {
|
||||
|
||||
final dateFormatter = DateFormat('MM/dd HH:mm');
|
||||
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return SwipeTo(
|
||||
key: Key('chat-message-${data.id}'),
|
||||
iconOnLeftSwipe: Symbols.reply,
|
||||
iconOnRightSwipe: Symbols.edit,
|
||||
swipeSensitivity: 20,
|
||||
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
||||
onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null,
|
||||
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
|
||||
child: ContextMenuArea(
|
||||
contextMenu: ContextMenu(
|
||||
entries: [
|
||||
@@ -87,42 +96,68 @@ class ChatMessage extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
Padding(
|
||||
padding: isCompact ? EdgeInsets.zero : padding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isMerged && !isCompact)
|
||||
AccountImage(
|
||||
GestureDetector(
|
||||
child: AccountImage(
|
||||
content: user?.avatar,
|
||||
),
|
||||
onTap: () {
|
||||
if (user == null) return;
|
||||
showPopover(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
transition: PopoverTransition.other,
|
||||
bodyBuilder: (context) => SizedBox(
|
||||
width: math.min(400, MediaQuery.of(context).size.width - 10),
|
||||
child: AccountPopoverCard(
|
||||
data: user,
|
||||
),
|
||||
),
|
||||
direction: PopoverDirection.bottom,
|
||||
arrowHeight: 5,
|
||||
arrowWidth: 15,
|
||||
arrowDxOffset: -190,
|
||||
);
|
||||
},
|
||||
)
|
||||
else if (isMerged)
|
||||
const Gap(40),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 480),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isMerged)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (isCompact)
|
||||
AccountImage(
|
||||
content: user?.avatar,
|
||||
radius: 12,
|
||||
).padding(right: 6),
|
||||
).padding(right: 8),
|
||||
Text(
|
||||
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
|
||||
).bold(),
|
||||
const Gap(6),
|
||||
const Gap(8),
|
||||
Text(
|
||||
dateFormatter.format(data.createdAt.toLocal()),
|
||||
).fontSize(13),
|
||||
],
|
||||
),
|
||||
if (isCompact) const Gap(4),
|
||||
).height(21),
|
||||
if (isCompact) const Gap(8),
|
||||
if (data.preload?.quoteEvent != null)
|
||||
StyledWidget(Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 480,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
@@ -145,25 +180,36 @@ class ChatMessage extends StatelessWidget {
|
||||
),
|
||||
)).padding(bottom: 4, top: 4),
|
||||
switch (data.type) {
|
||||
'messages.new' => _ChatMessageText(data: data),
|
||||
'messages.new' => _ChatMessageText(
|
||||
data: data,
|
||||
onReply: onReply,
|
||||
onEdit: onEdit,
|
||||
onDelete: onDelete,
|
||||
),
|
||||
_ => _ChatMessageSystemNotify(data: data),
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
).opacity(isPending ? 0.5 : 1),
|
||||
if (data.body['text'] != null && (data.body['text']?.isNotEmpty ?? false))
|
||||
),
|
||||
if (data.body['text'] != null &&
|
||||
data.type == 'messages.new' &&
|
||||
(data.body['text']?.isNotEmpty ?? false) &&
|
||||
(cfg.prefs.getBool(kAppExpandChatLink) ?? true))
|
||||
LinkPreviewWidget(text: data.body['text']!),
|
||||
if (data.preload?.attachments?.isNotEmpty ?? false)
|
||||
AttachmentList(
|
||||
data: data.preload!.attachments!,
|
||||
bordered: true,
|
||||
noGrow: true,
|
||||
maxHeight: 520,
|
||||
listPadding: const EdgeInsets.only(top: 8),
|
||||
maxHeight: 560,
|
||||
maxWidth: 480,
|
||||
minWidth: 480,
|
||||
padding: padding.copyWith(top: 8),
|
||||
),
|
||||
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(6),
|
||||
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(8),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -173,20 +219,76 @@ class ChatMessage extends StatelessWidget {
|
||||
|
||||
class _ChatMessageText extends StatelessWidget {
|
||||
final SnChatMessage data;
|
||||
final Function(SnChatMessage)? onReply;
|
||||
final Function(SnChatMessage)? onEdit;
|
||||
final Function(SnChatMessage)? onDelete;
|
||||
|
||||
const _ChatMessageText({required this.data});
|
||||
const _ChatMessageText({required this.data, this.onReply, this.onEdit, this.onDelete});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;
|
||||
|
||||
if (data.body['text'] != null && data.body['text'].isNotEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MarkdownTextContent(
|
||||
SelectionArea(
|
||||
contextMenuBuilder: (context, editableTextState) {
|
||||
final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
|
||||
|
||||
if (onReply != null) {
|
||||
items.insert(
|
||||
0,
|
||||
ContextMenuButtonItem(
|
||||
label: 'reply'.tr(),
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
onReply?.call(data);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isOwner && onEdit != null) {
|
||||
items.insert(
|
||||
1,
|
||||
ContextMenuButtonItem(
|
||||
label: 'edit'.tr(),
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
onEdit?.call(data);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isOwner && onDelete != null) {
|
||||
items.insert(
|
||||
2,
|
||||
ContextMenuButtonItem(
|
||||
label: 'delete'.tr(),
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
onDelete?.call(data);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: editableTextState.contextMenuAnchors,
|
||||
buttonItems: items,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: MarkdownTextContent(
|
||||
content: data.body['text'],
|
||||
isSelectable: true,
|
||||
isAutoWarp: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (data.updatedAt != data.createdAt)
|
||||
Text(
|
||||
'messageEditedHint'.tr(),
|
||||
|
@@ -1,18 +1,23 @@
|
||||
import 'dart:math' show min;
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/chat_message_controller.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_sticker.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class ChatMessageInput extends StatefulWidget {
|
||||
final ChatMessageController controller;
|
||||
@@ -33,12 +38,34 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
final TextEditingController _contentController = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_contentController.addListener(() {
|
||||
if (_contentController.text.isNotEmpty) {
|
||||
widget.controller.pingTypingStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setReply(SnChatMessage? value) {
|
||||
setState(() => _replyingMessage = value);
|
||||
}
|
||||
|
||||
void setInitialText(String? value) {
|
||||
_contentController.text = value ?? '';
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setInitialAttachments(List<PostWriteMedia>? value) {
|
||||
_attachments.addAll(value ?? []);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setEdit(SnChatMessage? value) {
|
||||
_contentController.text = value?.body['text'] ?? '';
|
||||
_attachments.clear();
|
||||
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
||||
setState(() => _editingMessage = value);
|
||||
}
|
||||
|
||||
@@ -83,6 +110,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
media.toFile()!,
|
||||
place.$1,
|
||||
place.$2,
|
||||
analyzeNow: media.type == SnMediaType.image,
|
||||
onProgress: (progress) {
|
||||
// Calculate overall progress for attachments
|
||||
setState(() {
|
||||
@@ -91,7 +119,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
},
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_attachments[i] = PostWriteMedia(item);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
@@ -103,7 +133,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
// Send the message
|
||||
// NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type
|
||||
widget.controller.sendMessage(
|
||||
'messages.new',
|
||||
_editingMessage != null ? 'messages.edit' : 'messages.new',
|
||||
_contentController.text,
|
||||
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
||||
relatedId: _editingMessage?.id,
|
||||
@@ -120,6 +150,29 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
|
||||
final List<PostWriteMedia> _attachments = List.empty(growable: true);
|
||||
|
||||
OverlayEntry? _overlayEntry;
|
||||
|
||||
void _showEmojiPicker(BuildContext context) {
|
||||
final overlay = Overlay.of(context);
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||
right: 16,
|
||||
child: _StickerPicker(
|
||||
originalText: _contentController.text,
|
||||
onDismiss: () => _dismissEmojiPicker(),
|
||||
onInsert: (str) => _contentController.text = str,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
overlay.insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
void _dismissEmojiPicker() {
|
||||
_overlayEntry?.remove();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contentController.dispose();
|
||||
@@ -160,75 +213,84 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: _replyingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
||||
child: _replyingMessage != null
|
||||
? MaterialBanner(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
leading: const Icon(Symbols.reply),
|
||||
backgroundColor: Colors.transparent,
|
||||
content: SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
? Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (_replyingMessage?.body['text'] != null)
|
||||
MarkdownTextContent(
|
||||
content: _replyingMessage?.body['text'],
|
||||
),
|
||||
],
|
||||
const Icon(Symbols.reply, size: 20),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
const Gap(16),
|
||||
InkWell(
|
||||
child: Text('cancel'.tr()),
|
||||
onPressed: () {
|
||||
onTap: () {
|
||||
_attachments.clear();
|
||||
setState(() => _replyingMessage = null);
|
||||
},
|
||||
),
|
||||
],
|
||||
).padding(vertical: 8),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
)
|
||||
.height(_replyingMessage != null ? 54 + 8 : 0, animate: true)
|
||||
.height(_replyingMessage != null ? 38 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: _editingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
||||
child: _editingMessage != null
|
||||
? MaterialBanner(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
leading: const Icon(Symbols.edit),
|
||||
backgroundColor: Colors.transparent,
|
||||
content: SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
? Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (_editingMessage?.body['text'] != null)
|
||||
MarkdownTextContent(
|
||||
content: _editingMessage?.body['text'],
|
||||
),
|
||||
],
|
||||
const Icon(Symbols.edit, size: 20),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
const Gap(16),
|
||||
InkWell(
|
||||
child: Text('cancel'.tr()),
|
||||
onPressed: () {
|
||||
onTap: () {
|
||||
_attachments.clear();
|
||||
_contentController.clear();
|
||||
setState(() => _editingMessage = null);
|
||||
},
|
||||
),
|
||||
],
|
||||
).padding(vertical: 8),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
)
|
||||
.height(_editingMessage != null ? 54 + 8 : 0, animate: true)
|
||||
.height(_editingMessage != null ? 38 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
SizedBox(
|
||||
height: 56,
|
||||
@@ -256,6 +318,19 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Symbols.mood,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
onPressed: () {
|
||||
_showEmojiPicker(context);
|
||||
},
|
||||
),
|
||||
AddPostMediaButton(
|
||||
onAdd: (items) {
|
||||
setState(() {
|
||||
@@ -281,3 +356,105 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StickerPicker extends StatelessWidget {
|
||||
final String originalText;
|
||||
final Function? onDismiss;
|
||||
final Function(String)? onInsert;
|
||||
|
||||
const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sticker = context.read<SnStickerProvider>();
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
onDismiss?.call();
|
||||
},
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: min(360, MediaQuery.of(context).size.width), maxHeight: 240),
|
||||
child: Material(
|
||||
elevation: 8,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: sticker.stickersByPack.entries
|
||||
.map((e) {
|
||||
return <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 8),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(e.value.first.pack.name).bold(),
|
||||
Text(e.value.first.pack.description),
|
||||
],
|
||||
),
|
||||
),
|
||||
GridView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 48,
|
||||
childAspectRatio: 1.0,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemCount: e.value.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final element = e.value[index];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final withSpace = originalText.isNotEmpty;
|
||||
onInsert?.call(
|
||||
'$originalText${withSpace ? ' ' : ''}:${element.pack.prefix}${element.alias}:');
|
||||
onDismiss?.call();
|
||||
},
|
||||
child: Tooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(text: ':${element.pack.prefix}${element.alias}:\n', style: GoogleFonts.robotoMono()),
|
||||
TextSpan(text: element.name).bold(),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: UniversalImage(
|
||||
sn.getAttachmentUrl(element.attachment.rid),
|
||||
width: 48,
|
||||
height: 48,
|
||||
cacheHeight: 48,
|
||||
cacheWidth: 48,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
})
|
||||
.expand((ele) => ele)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
53
lib/widgets/chat/chat_typing_indicator.dart
Normal file
53
lib/widgets/chat/chat_typing_indicator.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/chat_message_controller.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
|
||||
class ChatTypingIndicator extends StatelessWidget {
|
||||
final ChatMessageController controller;
|
||||
|
||||
const ChatTypingIndicator({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
|
||||
return StyledWidget(controller.typingMembers.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.more_horiz, weight: 600, size: 20),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'messageTyping'.plural(controller.typingMembers.length, args: [
|
||||
controller.typingMembers
|
||||
.map((ele) => (ele.nick?.isNotEmpty ?? false)
|
||||
? ele.nick!
|
||||
: ud.getAccountFromCache(ele.accountId)?.name ?? 'unknown')
|
||||
.join(', '),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
.height(controller.typingMembers.isNotEmpty ? 38 : 0, animate: true)
|
||||
.animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.fastLinearToSlowEaseIn,
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,7 +1,10 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
|
||||
@@ -11,42 +14,52 @@ class ConnectionIndicator extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ws = context.watch<WebSocketProvider>();
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
|
||||
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: ws,
|
||||
builder: (context, _) {
|
||||
final ua = context.read<UserProvider>();
|
||||
final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized;
|
||||
|
||||
return GestureDetector(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 8,
|
||||
top: MediaQuery.of(context).padding.top + 8,
|
||||
left: 24,
|
||||
right: 24,
|
||||
),
|
||||
return IgnorePointer(
|
||||
ignoring: !show,
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (ws.isBusy)
|
||||
Text('serverConnecting').tr().textColor(
|
||||
Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else if (!ws.isConnected)
|
||||
Text('serverDisconnected').tr().textColor(
|
||||
Theme.of(context).colorScheme.onSecondaryContainer),
|
||||
Text('serverDisconnected')
|
||||
.tr()
|
||||
.textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else
|
||||
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
||||
const Gap(8),
|
||||
if (ws.isBusy)
|
||||
const CircularProgressIndicator(strokeWidth: 2.5)
|
||||
.width(12)
|
||||
.height(12)
|
||||
.padding(horizontal: 4, right: 4)
|
||||
else if (!ws.isConnected)
|
||||
const Icon(Symbols.power_off, size: 18)
|
||||
else
|
||||
const Icon(Symbols.power, size: 18),
|
||||
],
|
||||
)
|
||||
).padding(horizontal: 8, vertical: 4)
|
||||
: const SizedBox.shrink(),
|
||||
)
|
||||
.height(
|
||||
(ws.isBusy || !ws.isConnected) && ua.isAuthorized
|
||||
? MediaQuery.of(context).padding.top + 36
|
||||
: 0,
|
||||
animate: true)
|
||||
.animate(
|
||||
).opacity(show ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.easeInOut,
|
||||
),
|
||||
@@ -55,6 +68,8 @@ class ConnectionIndicator extends StatelessWidget {
|
||||
ws.connect();
|
||||
}
|
||||
},
|
||||
),
|
||||
).padding(left: marginLeft),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
|
||||
class ContextMenuArea extends StatelessWidget {
|
||||
final ContextMenu contextMenu;
|
||||
@@ -22,13 +23,12 @@ class ContextMenuArea extends StatelessWidget {
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
mousePosition = event.position;
|
||||
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
|
||||
if (!isCollapseDrawer) {
|
||||
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
if (!cfg.drawerIsCollapsed) {
|
||||
// Leave padding for side navigation
|
||||
mousePosition = isExpandDrawer
|
||||
mousePosition = cfg.drawerIsExpanded
|
||||
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
||||
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
|
||||
: mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
|
||||
}
|
||||
},
|
||||
child: GestureDetector(
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user