Compare commits
122 Commits
44f2c5fe0e
...
2.3.2+63
| Author | SHA1 | Date | |
|---|---|---|---|
| 3818328afe | |||
| 11627e2455 | |||
| 3f82c06ff8 | |||
| 2350f59131 | |||
| 9fe7c9530a | |||
| 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 |
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"sync": {
|
"sync": {
|
||||||
"region": "solian-next",
|
"region": "solian",
|
||||||
"configPath": "roadsign.toml"
|
"configPath": "roadsign.toml"
|
||||||
},
|
},
|
||||||
"deployments": [
|
"deployments": [
|
||||||
{
|
{
|
||||||
"region": "solian-next",
|
"region": "solian",
|
||||||
"site": "solian-next-web",
|
"site": "solian-web",
|
||||||
"path": "build/web"
|
"path": "build/web"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -17,11 +17,16 @@
|
|||||||
android:label="Solian"
|
android:label="Solian"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:requestLegacyExternalStorage="true">
|
android:requestLegacyExternalStorage="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleInstance"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
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 {
|
post {
|
||||||
url: {{endpoint}}/cgi/id/dev/notify/all
|
url: {{endpoint}}/cgi/id/dev/notify/all
|
||||||
body: json
|
body: json
|
||||||
auth: bearer
|
auth: inherit
|
||||||
}
|
|
||||||
|
|
||||||
auth:bearer {
|
|
||||||
token: {{atk}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
@@ -19,11 +15,11 @@ body:json {
|
|||||||
"client_id": "{{third_client_id}}",
|
"client_id": "{{third_client_id}}",
|
||||||
"client_secret":"{{third_client_tk}}",
|
"client_secret":"{{third_client_tk}}",
|
||||||
"type": "general",
|
"type": "general",
|
||||||
"subject": "Merry Christmas!",
|
"subject": "新年快乐!",
|
||||||
"subtitle": "一条来自 Solar Network 团队的信息",
|
"subtitle": "一条来自 Solar Network 团队的信息",
|
||||||
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
|
"content": "今天是农历正月初一,小羊祝您新年快乐 🎉",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"image": "6EqsYQwmFRCkbmhR"
|
"image": "D2EDbcrsTugs3xk5"
|
||||||
},
|
},
|
||||||
"priority": 10
|
"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": "alphabot",
|
||||||
|
"client_secret": "_uR0sVnHTh",
|
||||||
|
"remark": "新年红包",
|
||||||
|
"amount": 9705,
|
||||||
|
"payee_id": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
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",
|
"screenAccountProfileEdit": "Edit Profile",
|
||||||
"screenAbuseReport": "Abuse Reports",
|
"screenAbuseReport": "Abuse Reports",
|
||||||
"screenSettings": "Settings",
|
"screenSettings": "Settings",
|
||||||
|
"screenAccountSettings": "Account Settings",
|
||||||
|
"screenFactorSettings": "Auth Factors",
|
||||||
|
"screenAccountWallet": "Wallet",
|
||||||
|
"screenNews": "News",
|
||||||
"screenAlbum": "Album",
|
"screenAlbum": "Album",
|
||||||
"screenChat": "Chat",
|
"screenChat": "Chat",
|
||||||
"screenChatManage": "Edit Channel",
|
"screenChatManage": "Edit Channel",
|
||||||
@@ -103,8 +107,18 @@
|
|||||||
},
|
},
|
||||||
"loginEnterPassword": "Enter the code",
|
"loginEnterPassword": "Enter the code",
|
||||||
"loginSuccess": "Logged in as {}",
|
"loginSuccess": "Logged in as {}",
|
||||||
|
"authFactorDelete": "Delete Auth Factor",
|
||||||
|
"authFactorDeleteDescription": "Are you sure you want delete auth factor {}?",
|
||||||
"authFactorPassword": "Password",
|
"authFactorPassword": "Password",
|
||||||
|
"authFactorPasswordDescription": "The password you set when you registered.",
|
||||||
"authFactorEmail": "Email verification code",
|
"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!",
|
"accountIntroTitle": "Hello there!",
|
||||||
"accountIntroSubtitle": "Pick an option below to get started.",
|
"accountIntroSubtitle": "Pick an option below to get started.",
|
||||||
"accountLogout": "Logout",
|
"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.",
|
"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",
|
"accountPublishers": "Your publishers",
|
||||||
"accountPublishersSubtitle": "Manage your publish identities.",
|
"accountPublishersSubtitle": "Manage your publish identities.",
|
||||||
|
"accountSettings": "Account Settings",
|
||||||
|
"accountSettingsSubtitle": "Manage your account and make it yours.",
|
||||||
"accountProfileEdit": "Edit your profile",
|
"accountProfileEdit": "Edit your profile",
|
||||||
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
|
"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.",
|
"accountProfileEditApplied": "Profile modification applied.",
|
||||||
"publishersNew": "New Publisher",
|
"publishersNew": "New Publisher",
|
||||||
"publisherNewSubtitle": "Create a new publisher identity.",
|
"publisherNewSubtitle": "Create a new publisher identity.",
|
||||||
@@ -179,8 +199,13 @@
|
|||||||
"other": "{} comments"
|
"other": "{} comments"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "Appearance",
|
"settingsAppearance": "Appearance",
|
||||||
|
"settingsDisplayLanguage": "Display Language",
|
||||||
|
"settingsDisplayLanguageDescription": "Set the application language.",
|
||||||
|
"settingsDisplayLanguageSystem": "Follow System",
|
||||||
"settingsAppBarTransparent": "Transparent App Bar",
|
"settingsAppBarTransparent": "Transparent App Bar",
|
||||||
"settingsAppBarTransparentDescription": "Enable transparent effect for the 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",
|
"settingsBackgroundImage": "Background Image",
|
||||||
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
|
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
|
||||||
"settingsBackgroundImageClear": "Clear Existing Background Image",
|
"settingsBackgroundImageClear": "Clear Existing Background Image",
|
||||||
@@ -191,6 +216,13 @@
|
|||||||
"settingsColorSchemeDescription": "Set the application primary color.",
|
"settingsColorSchemeDescription": "Set the application primary color.",
|
||||||
"settingsColorSeed": "Color Seed",
|
"settingsColorSeed": "Color Seed",
|
||||||
"settingsColorSeedDescription": "Select one of the present color schemes.",
|
"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",
|
"settingsNetwork": "Network",
|
||||||
"settingsNetworkServer": "HyperNet Server",
|
"settingsNetworkServer": "HyperNet Server",
|
||||||
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
|
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
|
||||||
@@ -209,12 +241,15 @@
|
|||||||
"settingsMisc": "Misc",
|
"settingsMisc": "Misc",
|
||||||
"settingsMiscAbout": "About",
|
"settingsMiscAbout": "About",
|
||||||
"settingsMiscAboutDescription": "View the version information of Solian.",
|
"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",
|
"sensitiveContent": "Sensitive Content",
|
||||||
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
||||||
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
||||||
"sensitiveContentReveal": "Reveal",
|
"sensitiveContentReveal": "Reveal",
|
||||||
"serverConnecting": "Connecting to server...",
|
"serverConnecting": "Connecting...",
|
||||||
"serverDisconnected": "Lost connection from server",
|
"serverDisconnected": "Connection Lost",
|
||||||
|
"serverConnected": "Connected",
|
||||||
"fieldChatAlias": "Channel Alias",
|
"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.",
|
"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",
|
"fieldChatName": "Name",
|
||||||
@@ -281,18 +316,25 @@
|
|||||||
"one": "{} attachment",
|
"one": "{} attachment",
|
||||||
"other": "{} attachments"
|
"other": "{} attachments"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} is typing...",
|
||||||
|
"other": "{} are typing..."
|
||||||
|
},
|
||||||
"fieldAttachmentRandomId": "Random ID",
|
"fieldAttachmentRandomId": "Random ID",
|
||||||
|
"fieldAttachmentAlt": "Alternative text",
|
||||||
"addAttachmentFromAlbum": "Add from album",
|
"addAttachmentFromAlbum": "Add from album",
|
||||||
"addAttachmentFromClipboard": "Paste file",
|
"addAttachmentFromClipboard": "Paste file",
|
||||||
"addAttachmentFromCameraPhoto": "Take photo",
|
"addAttachmentFromCameraPhoto": "Take photo",
|
||||||
"addAttachmentFromCameraVideo": "Take video",
|
"addAttachmentFromCameraVideo": "Take video",
|
||||||
"addAttachmentFromRandomId": "Link via RID",
|
"addAttachmentFromRandomId": "Link via RID",
|
||||||
|
"attachmentDetailInfo": "Attachment details",
|
||||||
"attachmentPastedImage": "Pasted Image",
|
"attachmentPastedImage": "Pasted Image",
|
||||||
"attachmentInsertLink": "Insert Link",
|
"attachmentInsertLink": "Insert Link",
|
||||||
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
|
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
|
||||||
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
||||||
"attachmentCompressVideo": "Re-encode video",
|
"attachmentCompressVideo": "Re-encode video",
|
||||||
"attachmentSetThumbnail": "Set thumbnail",
|
"attachmentSetThumbnail": "Set thumbnail",
|
||||||
|
"attachmentSetAlt": "Set alternative text",
|
||||||
"attachmentCopyRandomId": "Copy RID",
|
"attachmentCopyRandomId": "Copy RID",
|
||||||
"attachmentUpload": "Upload",
|
"attachmentUpload": "Upload",
|
||||||
"attachmentInputDialog": "Upload attachments",
|
"attachmentInputDialog": "Upload attachments",
|
||||||
@@ -309,7 +351,15 @@
|
|||||||
"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.",
|
"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",
|
"attachmentUploaded": "Uploaded",
|
||||||
"attachmentPending": "Pending",
|
"attachmentPending": "Pending",
|
||||||
"attachmentCopyCompressed": "Has compressed copy",
|
"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",
|
"notification": "Notification",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "All notifications read",
|
"zero": "All notifications read",
|
||||||
@@ -400,6 +450,9 @@
|
|||||||
"celebrateBirthday": "Happy birthday, {}!",
|
"celebrateBirthday": "Happy birthday, {}!",
|
||||||
"celebrateMerryXmas": "Merry christmas, {}!",
|
"celebrateMerryXmas": "Merry christmas, {}!",
|
||||||
"celebrateNewYear": "Happy new year, {}!",
|
"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, {}!",
|
"celebrateValentineDay": "Today is valentine's day, {}!",
|
||||||
"celebrateLaborDay": "Today is labor day, {}.",
|
"celebrateLaborDay": "Today is labor day, {}.",
|
||||||
"celebrateMotherDay": "Today is mother's day, {}.",
|
"celebrateMotherDay": "Today is mother's day, {}.",
|
||||||
@@ -409,6 +462,9 @@
|
|||||||
"celebrateThanksgiving": "Today is thanksgiving day, {}!",
|
"celebrateThanksgiving": "Today is thanksgiving day, {}!",
|
||||||
"pendingBirthday": "Birthday in {}",
|
"pendingBirthday": "Birthday in {}",
|
||||||
"pendingMerryXmas": "Christmas in {}",
|
"pendingMerryXmas": "Christmas in {}",
|
||||||
|
"pendingLunarNewYear": "Lunar new year in {}",
|
||||||
|
"pendingMidAutumn": "Mid-autumn festival in {}",
|
||||||
|
"pendingDragonBoat": "Dragon boat festival in {}",
|
||||||
"pendingNewYear": "New year in {}",
|
"pendingNewYear": "New year in {}",
|
||||||
"pendingValentineDay": "Valentine's day in {}",
|
"pendingValentineDay": "Valentine's day in {}",
|
||||||
"pendingLaborDay": "Labor day in {}",
|
"pendingLaborDay": "Labor day in {}",
|
||||||
@@ -450,6 +506,7 @@
|
|||||||
"accountJoinedAt": "Joined at {}",
|
"accountJoinedAt": "Joined at {}",
|
||||||
"accountBirthday": "Born on {}",
|
"accountBirthday": "Born on {}",
|
||||||
"accountBadge": "Badge",
|
"accountBadge": "Badge",
|
||||||
|
"accountCheckInNoRecords": "No check-in records",
|
||||||
"badgeCompanyStaff": "Solsynth Staff",
|
"badgeCompanyStaff": "Solsynth Staff",
|
||||||
"badgeSiteMigration": "Solar Network Native",
|
"badgeSiteMigration": "Solar Network Native",
|
||||||
"accountStatus": "Status",
|
"accountStatus": "Status",
|
||||||
@@ -458,6 +515,7 @@
|
|||||||
"accountStatusLastSeen": "Last seen at {}",
|
"accountStatusLastSeen": "Last seen at {}",
|
||||||
"postArticle": "Article on the Solar Network",
|
"postArticle": "Article on the Solar Network",
|
||||||
"postStory": "Story on the Solar Network",
|
"postStory": "Story on the Solar Network",
|
||||||
|
"postLocalDraftRestored": "Restored from device",
|
||||||
"articleWrittenAt": "Written at {}",
|
"articleWrittenAt": "Written at {}",
|
||||||
"articleEditedAt": "Edited at {}",
|
"articleEditedAt": "Edited at {}",
|
||||||
"attachmentSaved": "Saved to album",
|
"attachmentSaved": "Saved to album",
|
||||||
@@ -498,11 +556,15 @@
|
|||||||
"postImageShareAds": "Explore posts on the Solar Network",
|
"postImageShareAds": "Explore posts on the Solar Network",
|
||||||
"postShare": "Share",
|
"postShare": "Share",
|
||||||
"postShareImage": "Share via Image",
|
"postShareImage": "Share via Image",
|
||||||
|
"postGetInsight": "Get Insight",
|
||||||
|
"postGetInsightTitle": "AI Insight",
|
||||||
|
"postGetInsightDescription": "AI may make mistakes, check important information.",
|
||||||
"appInitializing": "Initializing",
|
"appInitializing": "Initializing",
|
||||||
"poweredBy": "Powered by {}",
|
"poweredBy": "Powered by {}",
|
||||||
"shareIntent": "Share",
|
"shareIntent": "Share",
|
||||||
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
||||||
"shareIntentPostStory": "Post a Story",
|
"shareIntentPostStory": "Post a Story",
|
||||||
|
"shareIntentSendChannel": "Share to Channel",
|
||||||
"updateAvailable": "Update Available",
|
"updateAvailable": "Update Available",
|
||||||
"updateOngoing": "Updating, please wait...",
|
"updateOngoing": "Updating, please wait...",
|
||||||
"custom": "Custom",
|
"custom": "Custom",
|
||||||
@@ -515,6 +577,7 @@
|
|||||||
"colorSchemeWhite": "White",
|
"colorSchemeWhite": "White",
|
||||||
"colorSchemeBlack": "Black",
|
"colorSchemeBlack": "Black",
|
||||||
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
||||||
|
"postFeaturedComment": "Featured Comment",
|
||||||
"postCategoryTechnology": "Technology",
|
"postCategoryTechnology": "Technology",
|
||||||
"postCategoryGaming": "Gaming",
|
"postCategoryGaming": "Gaming",
|
||||||
"postCategoryLife": "Life",
|
"postCategoryLife": "Life",
|
||||||
@@ -525,5 +588,27 @@
|
|||||||
"postCategoryKnowledge": "Knowledge",
|
"postCategoryKnowledge": "Knowledge",
|
||||||
"postCategoryLiterature": "Literature",
|
"postCategoryLiterature": "Literature",
|
||||||
"postCategoryFunny": "Funny",
|
"postCategoryFunny": "Funny",
|
||||||
"postCategoryUncategorized": "Uncategorized"
|
"postCategoryUncategorized": "Uncategorized",
|
||||||
|
"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": "编辑资料",
|
"screenAccountProfileEdit": "编辑资料",
|
||||||
"screenAbuseReport": "滥用检举",
|
"screenAbuseReport": "滥用检举",
|
||||||
"screenSettings": "设置",
|
"screenSettings": "设置",
|
||||||
|
"screenAccountSettings": "账号设置",
|
||||||
|
"screenFactorSettings": "验证因子",
|
||||||
|
"screenAccountWallet": "钱包",
|
||||||
|
"screenNews": "新闻",
|
||||||
"screenAlbum": "相册",
|
"screenAlbum": "相册",
|
||||||
"screenChat": "聊天",
|
"screenChat": "聊天",
|
||||||
"screenChatManage": "编辑聊天频道",
|
"screenChatManage": "编辑聊天频道",
|
||||||
@@ -87,8 +91,18 @@
|
|||||||
},
|
},
|
||||||
"loginEnterPassword": "验证代码",
|
"loginEnterPassword": "验证代码",
|
||||||
"loginSuccess": "登录为 {}",
|
"loginSuccess": "登录为 {}",
|
||||||
|
"authFactorDelete": "删除验证因子",
|
||||||
|
"authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
|
||||||
"authFactorPassword": "密码",
|
"authFactorPassword": "密码",
|
||||||
|
"authFactorPasswordDescription": "注册时选择设置的密码。",
|
||||||
"authFactorEmail": "电邮一次性验证码",
|
"authFactorEmail": "电邮一次性验证码",
|
||||||
|
"authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
|
||||||
|
"authFactorTOTP": "时序验证码",
|
||||||
|
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
|
||||||
|
"authFactorInAppNotify": "应用内通知验证码",
|
||||||
|
"authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
|
||||||
|
"authFactorAdd": "添加新验证因子",
|
||||||
|
"authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
|
||||||
"accountIntroTitle": "喜欢您来!",
|
"accountIntroTitle": "喜欢您来!",
|
||||||
"accountIntroSubtitle": "登陆以探索更广大的世界。",
|
"accountIntroSubtitle": "登陆以探索更广大的世界。",
|
||||||
"accountLogout": "退出登录",
|
"accountLogout": "退出登录",
|
||||||
@@ -97,8 +111,14 @@
|
|||||||
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
|
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
|
||||||
"accountPublishers": "你的发布者",
|
"accountPublishers": "你的发布者",
|
||||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||||
|
"accountSettings": "帐号设置",
|
||||||
|
"accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
|
||||||
"accountProfileEdit": "编辑资料",
|
"accountProfileEdit": "编辑资料",
|
||||||
"accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
|
"accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
|
||||||
|
"accountWallet": "钱包",
|
||||||
|
"accountWalletSubtitle": "查看你的余额和交易记录。",
|
||||||
|
"factorSettings": "验证因子",
|
||||||
|
"factorSettingsSubtitle": "管理你的登陆验证方式。",
|
||||||
"accountProfileEditApplied": "个人资料修改已被应用。",
|
"accountProfileEditApplied": "个人资料修改已被应用。",
|
||||||
"publishersNew": "新发布者",
|
"publishersNew": "新发布者",
|
||||||
"publisherNewSubtitle": "创建一个新的公共身份。",
|
"publisherNewSubtitle": "创建一个新的公共身份。",
|
||||||
@@ -177,6 +197,9 @@
|
|||||||
"other": "{} 条评论"
|
"other": "{} 条评论"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "外观",
|
"settingsAppearance": "外观",
|
||||||
|
"settingsDisplayLanguage": "显示语言",
|
||||||
|
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
|
||||||
|
"settingsDisplayLanguageSystem": "跟随系统",
|
||||||
"settingsBackgroundImage": "背景图片",
|
"settingsBackgroundImage": "背景图片",
|
||||||
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
|
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
|
||||||
"settingsBackgroundImageClear": "清除现存背景图",
|
"settingsBackgroundImageClear": "清除现存背景图",
|
||||||
@@ -185,10 +208,19 @@
|
|||||||
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
|
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
|
||||||
"settingsAppBarTransparent": "透明顶栏",
|
"settingsAppBarTransparent": "透明顶栏",
|
||||||
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
|
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
|
||||||
|
"settingsDrawerPreferCollapse": "侧边栏偏好折叠",
|
||||||
|
"settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
|
||||||
"settingsColorScheme": "主题色",
|
"settingsColorScheme": "主题色",
|
||||||
"settingsColorSchemeDescription": "设置应用主题色。",
|
"settingsColorSchemeDescription": "设置应用主题色。",
|
||||||
"settingsColorSeed": "预设色彩主题",
|
"settingsColorSeed": "预设色彩主题",
|
||||||
"settingsColorSeedDescription": "选择一个预设色彩主题。",
|
"settingsColorSeedDescription": "选择一个预设色彩主题。",
|
||||||
|
"settingsFeatures": "功能",
|
||||||
|
"settingsNotifyWithHaptic": "新通知时振动",
|
||||||
|
"settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
|
||||||
|
"settingsExpandPostLink": "展开帖子链接",
|
||||||
|
"settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。",
|
||||||
|
"settingsExpandChatLink": "展开聊天链接",
|
||||||
|
"settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。",
|
||||||
"settingsNetwork": "网络",
|
"settingsNetwork": "网络",
|
||||||
"settingsNetworkServer": "HyperNet 服务器",
|
"settingsNetworkServer": "HyperNet 服务器",
|
||||||
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
|
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
|
||||||
@@ -207,12 +239,15 @@
|
|||||||
"settingsMisc": "杂项",
|
"settingsMisc": "杂项",
|
||||||
"settingsMiscAbout": "关于",
|
"settingsMiscAbout": "关于",
|
||||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||||
|
"settingsAccountLanguage": "帐号偏好语言",
|
||||||
|
"settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。",
|
||||||
"sensitiveContent": "敏感内容",
|
"sensitiveContent": "敏感内容",
|
||||||
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
||||||
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
||||||
"sensitiveContentReveal": "显示内容",
|
"sensitiveContentReveal": "显示内容",
|
||||||
"serverConnecting": "正在连接服务器…",
|
"serverConnecting": "正在连接…",
|
||||||
"serverDisconnected": "已与服务器断开连接",
|
"serverDisconnected": "已断开连接",
|
||||||
|
"serverConnected": "已连接",
|
||||||
"fieldChatAlias": "频道别名",
|
"fieldChatAlias": "频道别名",
|
||||||
"fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
|
"fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
|
||||||
"fieldChatName": "名称",
|
"fieldChatName": "名称",
|
||||||
@@ -279,18 +314,25 @@
|
|||||||
"one": "{} 个附件",
|
"one": "{} 个附件",
|
||||||
"other": "{} 个附件"
|
"other": "{} 个附件"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} 正在输入",
|
||||||
|
"other": "{} 正在输入"
|
||||||
|
},
|
||||||
"fieldAttachmentRandomId": "访问 ID",
|
"fieldAttachmentRandomId": "访问 ID",
|
||||||
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘贴附件",
|
"addAttachmentFromClipboard": "粘贴附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||||
"addAttachmentFromCameraVideo": "拍摄视频",
|
"addAttachmentFromCameraVideo": "拍摄视频",
|
||||||
"addAttachmentFromRandomId": "通过访问 ID 链接",
|
"addAttachmentFromRandomId": "通过访问 ID 链接",
|
||||||
|
"attachmentDetailInfo": "附件详细信息",
|
||||||
"attachmentPastedImage": "粘贴的图片",
|
"attachmentPastedImage": "粘贴的图片",
|
||||||
"attachmentInsertLink": "插入连接",
|
"attachmentInsertLink": "插入连接",
|
||||||
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
||||||
"attachmentCompressVideo": "重新编码视频",
|
"attachmentCompressVideo": "重新编码视频",
|
||||||
"attachmentSetThumbnail": "设置缩略图",
|
"attachmentSetThumbnail": "设置缩略图",
|
||||||
|
"attachmentSetAlt": "设置概述文字",
|
||||||
"attachmentCopyRandomId": "复制访问 ID",
|
"attachmentCopyRandomId": "复制访问 ID",
|
||||||
"attachmentUpload": "上传",
|
"attachmentUpload": "上传",
|
||||||
"attachmentInputDialog": "上传附件",
|
"attachmentInputDialog": "上传附件",
|
||||||
@@ -308,6 +350,14 @@
|
|||||||
"attachmentUploaded": "已上传",
|
"attachmentUploaded": "已上传",
|
||||||
"attachmentPending": "未上传",
|
"attachmentPending": "未上传",
|
||||||
"attachmentCopyCompressed": "有压缩副本",
|
"attachmentCopyCompressed": "有压缩副本",
|
||||||
|
"attachmentGotBoosted": "有加速传递",
|
||||||
|
"attachmentBoost": "加速包",
|
||||||
|
"attachmentCreateBoost": "加速传递",
|
||||||
|
"attachmentBoostHint": "加速传递允许您将附件上传到更近的受众或更快的内容网络。该功能目前处于 Beta 阶段。该功能限时免费,当有价格计划更改时,您将会被通知。",
|
||||||
|
"attachmentDestinationRegion": "目标节点",
|
||||||
|
"attachmentDestinationRegionAPAC": "亚太地区",
|
||||||
|
"attachmentDestinationRegionNGB": "中国 · 浙江 · 宁波",
|
||||||
|
"attachmentDestinationRegionHKG": "香港",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "无未读通知",
|
"zero": "无未读通知",
|
||||||
@@ -396,6 +446,9 @@
|
|||||||
"dailyCheckNegativeHint6": "出门",
|
"dailyCheckNegativeHint6": "出门",
|
||||||
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
|
||||||
"celebrateBirthday": "生日快乐,{}!",
|
"celebrateBirthday": "生日快乐,{}!",
|
||||||
|
"celebrateLunarNewYear": "春节快乐,{}!",
|
||||||
|
"celebrateMidAutumn": "中秋节快乐,{}!",
|
||||||
|
"celebrateDragonBoat": "端午节快乐,{}!",
|
||||||
"celebrateMerryXmas": "圣诞快乐,{}!",
|
"celebrateMerryXmas": "圣诞快乐,{}!",
|
||||||
"celebrateNewYear": "新年快乐,{}!",
|
"celebrateNewYear": "新年快乐,{}!",
|
||||||
"celebrateValentineDay": "今天是情人节,{}!",
|
"celebrateValentineDay": "今天是情人节,{}!",
|
||||||
@@ -405,6 +458,9 @@
|
|||||||
"celebrateFatherDay": "今天是父亲节,{}。",
|
"celebrateFatherDay": "今天是父亲节,{}。",
|
||||||
"celebrateHalloween": "快乐在圣诞节,{}!",
|
"celebrateHalloween": "快乐在圣诞节,{}!",
|
||||||
"celebrateThanksgiving": "今天是感恩节,{}!",
|
"celebrateThanksgiving": "今天是感恩节,{}!",
|
||||||
|
"pendingLunarNewYear": "{} 过春节",
|
||||||
|
"pendingMidAutumn": "{} 过中秋节",
|
||||||
|
"pendingDragonBoat": "{} 过端午节",
|
||||||
"pendingBirthday": "{} 过生日",
|
"pendingBirthday": "{} 过生日",
|
||||||
"pendingMerryXmas": "{} 过圣诞节",
|
"pendingMerryXmas": "{} 过圣诞节",
|
||||||
"pendingNewYear": "{} 跨年",
|
"pendingNewYear": "{} 跨年",
|
||||||
@@ -448,6 +504,7 @@
|
|||||||
"accountJoinedAt": "加入于 {}",
|
"accountJoinedAt": "加入于 {}",
|
||||||
"accountBirthday": "出生于 {}",
|
"accountBirthday": "出生于 {}",
|
||||||
"accountBadge": "徽章",
|
"accountBadge": "徽章",
|
||||||
|
"accountCheckInNoRecords": "暂无运势记录",
|
||||||
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
|
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
|
||||||
"badgeSiteMigration": "Solar Network 原住民",
|
"badgeSiteMigration": "Solar Network 原住民",
|
||||||
"accountStatus": "状态",
|
"accountStatus": "状态",
|
||||||
@@ -456,6 +513,7 @@
|
|||||||
"accountStatusLastSeen": "最后一次上线于 {}",
|
"accountStatusLastSeen": "最后一次上线于 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
|
"postLocalDraftRestored": "从本地恢复草稿",
|
||||||
"articleWrittenAt": "发表于 {}",
|
"articleWrittenAt": "发表于 {}",
|
||||||
"articleEditedAt": "编辑于 {}",
|
"articleEditedAt": "编辑于 {}",
|
||||||
"attachmentSaved": "已保存到相册",
|
"attachmentSaved": "已保存到相册",
|
||||||
@@ -496,11 +554,15 @@
|
|||||||
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
|
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
|
||||||
"postShare": "分享",
|
"postShare": "分享",
|
||||||
"postShareImage": "分享帖图",
|
"postShareImage": "分享帖图",
|
||||||
|
"postGetInsight": "获取见解",
|
||||||
|
"postGetInsightTitle": "AI 见解",
|
||||||
|
"postGetInsightDescription": "AI 可能会出错,检查信息真实性。",
|
||||||
"appInitializing": "正在初始化",
|
"appInitializing": "正在初始化",
|
||||||
"poweredBy": "由 {} 提供支持",
|
"poweredBy": "由 {} 提供支持",
|
||||||
"shareIntent": "分享",
|
"shareIntent": "分享",
|
||||||
"shareIntentDescription": "您想对您分享的内容做些什么?",
|
"shareIntentDescription": "您想对您分享的内容做些什么?",
|
||||||
"shareIntentPostStory": "发布动态",
|
"shareIntentPostStory": "发布动态",
|
||||||
|
"shareIntentSendChannel": "分享到聊天频道",
|
||||||
"updateAvailable": "检测到更新可用",
|
"updateAvailable": "检测到更新可用",
|
||||||
"updateOngoing": "正在更新,请稍后……",
|
"updateOngoing": "正在更新,请稍后……",
|
||||||
"custom": "自定义",
|
"custom": "自定义",
|
||||||
@@ -513,6 +575,7 @@
|
|||||||
"colorSchemeWhite": "白色",
|
"colorSchemeWhite": "白色",
|
||||||
"colorSchemeBlack": "黑色",
|
"colorSchemeBlack": "黑色",
|
||||||
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
||||||
|
"postFeaturedComment": "精选评论",
|
||||||
"postCategoryTechnology": "技术",
|
"postCategoryTechnology": "技术",
|
||||||
"postCategoryGaming": "游戏",
|
"postCategoryGaming": "游戏",
|
||||||
"postCategoryLife": "生活",
|
"postCategoryLife": "生活",
|
||||||
@@ -523,5 +586,27 @@
|
|||||||
"postCategoryKnowledge": "知识",
|
"postCategoryKnowledge": "知识",
|
||||||
"postCategoryLiterature": "文学",
|
"postCategoryLiterature": "文学",
|
||||||
"postCategoryFunny": "搞笑",
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分类"
|
"postCategoryUncategorized": "未分类",
|
||||||
|
"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": "編輯資料",
|
"screenAccountProfileEdit": "編輯資料",
|
||||||
"screenAbuseReport": "濫用檢舉",
|
"screenAbuseReport": "濫用檢舉",
|
||||||
"screenSettings": "設置",
|
"screenSettings": "設置",
|
||||||
|
"screenAccountSettings": "賬號設置",
|
||||||
|
"screenFactorSettings": "驗證因子",
|
||||||
|
"screenAccountWallet": "錢包",
|
||||||
|
"screenNews": "新聞",
|
||||||
"screenAlbum": "相冊",
|
"screenAlbum": "相冊",
|
||||||
"screenChat": "聊天",
|
"screenChat": "聊天",
|
||||||
"screenChatManage": "編輯聊天頻道",
|
"screenChatManage": "編輯聊天頻道",
|
||||||
@@ -87,8 +91,18 @@
|
|||||||
},
|
},
|
||||||
"loginEnterPassword": "驗證代碼",
|
"loginEnterPassword": "驗證代碼",
|
||||||
"loginSuccess": "登錄為 {}",
|
"loginSuccess": "登錄為 {}",
|
||||||
|
"authFactorDelete": "刪除驗證因子",
|
||||||
|
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||||
"authFactorPassword": "密碼",
|
"authFactorPassword": "密碼",
|
||||||
|
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||||
"authFactorEmail": "電郵一次性驗證碼",
|
"authFactorEmail": "電郵一次性驗證碼",
|
||||||
|
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||||
|
"authFactorTOTP": "時序驗證碼",
|
||||||
|
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||||
|
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||||
|
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||||
|
"authFactorAdd": "添加新驗證因子",
|
||||||
|
"authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。",
|
||||||
"accountIntroTitle": "喜歡您來!",
|
"accountIntroTitle": "喜歡您來!",
|
||||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||||
"accountLogout": "退出登錄",
|
"accountLogout": "退出登錄",
|
||||||
@@ -97,8 +111,14 @@
|
|||||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||||
"accountPublishers": "你的發佈者",
|
"accountPublishers": "你的發佈者",
|
||||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||||
|
"accountSettings": "帳號設置",
|
||||||
|
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||||
"accountProfileEdit": "編輯資料",
|
"accountProfileEdit": "編輯資料",
|
||||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
|
"accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
|
||||||
|
"accountWallet": "錢包",
|
||||||
|
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||||
|
"factorSettings": "驗證因子",
|
||||||
|
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||||
"publishersNew": "新發布者",
|
"publishersNew": "新發布者",
|
||||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||||
@@ -177,6 +197,9 @@
|
|||||||
"other": "{} 條評論"
|
"other": "{} 條評論"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "外觀",
|
"settingsAppearance": "外觀",
|
||||||
|
"settingsDisplayLanguage": "顯示語言",
|
||||||
|
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||||
|
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||||
"settingsBackgroundImage": "背景圖片",
|
"settingsBackgroundImage": "背景圖片",
|
||||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||||
@@ -185,10 +208,19 @@
|
|||||||
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||||
"settingsAppBarTransparent": "透明頂欄",
|
"settingsAppBarTransparent": "透明頂欄",
|
||||||
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
|
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
|
||||||
|
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
|
||||||
|
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
|
||||||
"settingsColorScheme": "主題色",
|
"settingsColorScheme": "主題色",
|
||||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||||
"settingsColorSeed": "預設色彩主題",
|
"settingsColorSeed": "預設色彩主題",
|
||||||
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||||
|
"settingsFeatures": "功能",
|
||||||
|
"settingsNotifyWithHaptic": "新通知時振動",
|
||||||
|
"settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
|
||||||
|
"settingsExpandPostLink": "展開帖子鏈接",
|
||||||
|
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
|
||||||
|
"settingsExpandChatLink": "展開聊天鏈接",
|
||||||
|
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
|
||||||
"settingsNetwork": "網絡",
|
"settingsNetwork": "網絡",
|
||||||
"settingsNetworkServer": "HyperNet 服務器",
|
"settingsNetworkServer": "HyperNet 服務器",
|
||||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||||
@@ -207,12 +239,15 @@
|
|||||||
"settingsMisc": "雜項",
|
"settingsMisc": "雜項",
|
||||||
"settingsMiscAbout": "關於",
|
"settingsMiscAbout": "關於",
|
||||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||||
|
"settingsAccountLanguage": "帳號偏好語言",
|
||||||
|
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
|
||||||
"sensitiveContent": "敏感內容",
|
"sensitiveContent": "敏感內容",
|
||||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||||
"sensitiveContentReveal": "顯示內容",
|
"sensitiveContentReveal": "顯示內容",
|
||||||
"serverConnecting": "正在連接服務器…",
|
"serverConnecting": "正在連接…",
|
||||||
"serverDisconnected": "已與服務器斷開連接",
|
"serverDisconnected": "已斷開連接",
|
||||||
|
"serverConnected": "已連接",
|
||||||
"fieldChatAlias": "頻道別名",
|
"fieldChatAlias": "頻道別名",
|
||||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||||
"fieldChatName": "名稱",
|
"fieldChatName": "名稱",
|
||||||
@@ -279,18 +314,25 @@
|
|||||||
"one": "{} 個附件",
|
"one": "{} 個附件",
|
||||||
"other": "{} 個附件"
|
"other": "{} 個附件"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} 正在輸入",
|
||||||
|
"other": "{} 正在輸入"
|
||||||
|
},
|
||||||
"fieldAttachmentRandomId": "訪問 ID",
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘貼附件",
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||||
|
"attachmentDetailInfo": "附件詳細信息",
|
||||||
"attachmentPastedImage": "粘貼的圖片",
|
"attachmentPastedImage": "粘貼的圖片",
|
||||||
"attachmentInsertLink": "插入連接",
|
"attachmentInsertLink": "插入連接",
|
||||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||||
"attachmentCompressVideo": "重新編碼視頻",
|
"attachmentCompressVideo": "重新編碼視頻",
|
||||||
"attachmentSetThumbnail": "設置縮略圖",
|
"attachmentSetThumbnail": "設置縮略圖",
|
||||||
|
"attachmentSetAlt": "設置概述文字",
|
||||||
"attachmentCopyRandomId": "複製訪問 ID",
|
"attachmentCopyRandomId": "複製訪問 ID",
|
||||||
"attachmentUpload": "上傳",
|
"attachmentUpload": "上傳",
|
||||||
"attachmentInputDialog": "上傳附件",
|
"attachmentInputDialog": "上傳附件",
|
||||||
@@ -308,6 +350,14 @@
|
|||||||
"attachmentUploaded": "已上傳",
|
"attachmentUploaded": "已上傳",
|
||||||
"attachmentPending": "未上傳",
|
"attachmentPending": "未上傳",
|
||||||
"attachmentCopyCompressed": "有壓縮副本",
|
"attachmentCopyCompressed": "有壓縮副本",
|
||||||
|
"attachmentGotBoosted": "有加速傳遞",
|
||||||
|
"attachmentBoost": "加速包",
|
||||||
|
"attachmentCreateBoost": "加速傳遞",
|
||||||
|
"attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。",
|
||||||
|
"attachmentDestinationRegion": "目標節點",
|
||||||
|
"attachmentDestinationRegionAPAC": "亞太地區",
|
||||||
|
"attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波",
|
||||||
|
"attachmentDestinationRegionHKG": "香港",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "無未讀通知",
|
"zero": "無未讀通知",
|
||||||
@@ -396,6 +446,9 @@
|
|||||||
"dailyCheckNegativeHint6": "出門",
|
"dailyCheckNegativeHint6": "出門",
|
||||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||||
"celebrateBirthday": "生日快樂,{}!",
|
"celebrateBirthday": "生日快樂,{}!",
|
||||||
|
"celebrateLunarNewYear": "春節快樂,{}!",
|
||||||
|
"celebrateMidAutumn": "中秋節快樂,{}!",
|
||||||
|
"celebrateDragonBoat": "端午節快樂,{}!",
|
||||||
"celebrateMerryXmas": "聖誕快樂,{}!",
|
"celebrateMerryXmas": "聖誕快樂,{}!",
|
||||||
"celebrateNewYear": "新年快樂,{}!",
|
"celebrateNewYear": "新年快樂,{}!",
|
||||||
"celebrateValentineDay": "今天是情人節,{}!",
|
"celebrateValentineDay": "今天是情人節,{}!",
|
||||||
@@ -405,6 +458,9 @@
|
|||||||
"celebrateFatherDay": "今天是父親節,{}。",
|
"celebrateFatherDay": "今天是父親節,{}。",
|
||||||
"celebrateHalloween": "快樂在聖誕節,{}!",
|
"celebrateHalloween": "快樂在聖誕節,{}!",
|
||||||
"celebrateThanksgiving": "今天是感恩節,{}!",
|
"celebrateThanksgiving": "今天是感恩節,{}!",
|
||||||
|
"pendingLunarNewYear": "{} 過春節",
|
||||||
|
"pendingMidAutumn": "{} 過中秋節",
|
||||||
|
"pendingDragonBoat": "{} 過端午節",
|
||||||
"pendingBirthday": "{} 過生日",
|
"pendingBirthday": "{} 過生日",
|
||||||
"pendingMerryXmas": "{} 過聖誕節",
|
"pendingMerryXmas": "{} 過聖誕節",
|
||||||
"pendingNewYear": "{} 跨年",
|
"pendingNewYear": "{} 跨年",
|
||||||
@@ -448,6 +504,7 @@
|
|||||||
"accountJoinedAt": "加入於 {}",
|
"accountJoinedAt": "加入於 {}",
|
||||||
"accountBirthday": "出生於 {}",
|
"accountBirthday": "出生於 {}",
|
||||||
"accountBadge": "徽章",
|
"accountBadge": "徽章",
|
||||||
|
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||||
"badgeSiteMigration": "Solar Network 原住民",
|
"badgeSiteMigration": "Solar Network 原住民",
|
||||||
"accountStatus": "狀態",
|
"accountStatus": "狀態",
|
||||||
@@ -456,6 +513,7 @@
|
|||||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
|
"postLocalDraftRestored": "從本地恢復草稿",
|
||||||
"articleWrittenAt": "發表於 {}",
|
"articleWrittenAt": "發表於 {}",
|
||||||
"articleEditedAt": "編輯於 {}",
|
"articleEditedAt": "編輯於 {}",
|
||||||
"attachmentSaved": "已保存到相冊",
|
"attachmentSaved": "已保存到相冊",
|
||||||
@@ -496,11 +554,15 @@
|
|||||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||||
"postShare": "分享",
|
"postShare": "分享",
|
||||||
"postShareImage": "分享帖圖",
|
"postShareImage": "分享帖圖",
|
||||||
|
"postGetInsight": "獲取見解",
|
||||||
|
"postGetInsightTitle": "AI 見解",
|
||||||
|
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||||
"appInitializing": "正在初始化",
|
"appInitializing": "正在初始化",
|
||||||
"poweredBy": "由 {} 提供支持",
|
"poweredBy": "由 {} 提供支持",
|
||||||
"shareIntent": "分享",
|
"shareIntent": "分享",
|
||||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||||
"shareIntentPostStory": "發佈動態",
|
"shareIntentPostStory": "發佈動態",
|
||||||
|
"shareIntentSendChannel": "分享到聊天頻道",
|
||||||
"updateAvailable": "檢測到更新可用",
|
"updateAvailable": "檢測到更新可用",
|
||||||
"updateOngoing": "正在更新,請稍後……",
|
"updateOngoing": "正在更新,請稍後……",
|
||||||
"custom": "自定義",
|
"custom": "自定義",
|
||||||
@@ -513,6 +575,7 @@
|
|||||||
"colorSchemeWhite": "白色",
|
"colorSchemeWhite": "白色",
|
||||||
"colorSchemeBlack": "黑色",
|
"colorSchemeBlack": "黑色",
|
||||||
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
||||||
|
"postFeaturedComment": "精選評論",
|
||||||
"postCategoryTechnology": "技術",
|
"postCategoryTechnology": "技術",
|
||||||
"postCategoryGaming": "遊戲",
|
"postCategoryGaming": "遊戲",
|
||||||
"postCategoryLife": "生活",
|
"postCategoryLife": "生活",
|
||||||
@@ -523,5 +586,26 @@
|
|||||||
"postCategoryKnowledge": "知識",
|
"postCategoryKnowledge": "知識",
|
||||||
"postCategoryLiterature": "文學",
|
"postCategoryLiterature": "文學",
|
||||||
"postCategoryFunny": "搞笑",
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分類"
|
"postCategoryUncategorized": "未分類",
|
||||||
|
"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": "編輯資料",
|
"screenAccountProfileEdit": "編輯資料",
|
||||||
"screenAbuseReport": "濫用檢舉",
|
"screenAbuseReport": "濫用檢舉",
|
||||||
"screenSettings": "設置",
|
"screenSettings": "設置",
|
||||||
|
"screenAccountSettings": "賬號設置",
|
||||||
|
"screenFactorSettings": "驗證因子",
|
||||||
|
"screenAccountWallet": "錢包",
|
||||||
|
"screenNews": "新聞",
|
||||||
"screenAlbum": "相冊",
|
"screenAlbum": "相冊",
|
||||||
"screenChat": "聊天",
|
"screenChat": "聊天",
|
||||||
"screenChatManage": "編輯聊天頻道",
|
"screenChatManage": "編輯聊天頻道",
|
||||||
@@ -87,8 +91,18 @@
|
|||||||
},
|
},
|
||||||
"loginEnterPassword": "驗證代碼",
|
"loginEnterPassword": "驗證代碼",
|
||||||
"loginSuccess": "登錄為 {}",
|
"loginSuccess": "登錄為 {}",
|
||||||
|
"authFactorDelete": "刪除驗證因子",
|
||||||
|
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||||
"authFactorPassword": "密碼",
|
"authFactorPassword": "密碼",
|
||||||
|
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||||
"authFactorEmail": "電郵一次性驗證碼",
|
"authFactorEmail": "電郵一次性驗證碼",
|
||||||
|
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||||
|
"authFactorTOTP": "時序驗證碼",
|
||||||
|
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||||
|
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||||
|
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||||
|
"authFactorAdd": "添加新驗證因子",
|
||||||
|
"authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。",
|
||||||
"accountIntroTitle": "喜歡您來!",
|
"accountIntroTitle": "喜歡您來!",
|
||||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||||
"accountLogout": "退出登錄",
|
"accountLogout": "退出登錄",
|
||||||
@@ -97,8 +111,14 @@
|
|||||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||||
"accountPublishers": "你的發佈者",
|
"accountPublishers": "你的發佈者",
|
||||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||||
|
"accountSettings": "帳號設置",
|
||||||
|
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||||
"accountProfileEdit": "編輯資料",
|
"accountProfileEdit": "編輯資料",
|
||||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
|
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
|
||||||
|
"accountWallet": "錢包",
|
||||||
|
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||||
|
"factorSettings": "驗證因子",
|
||||||
|
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||||
"publishersNew": "新發布者",
|
"publishersNew": "新發布者",
|
||||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||||
@@ -177,6 +197,9 @@
|
|||||||
"other": "{} 條評論"
|
"other": "{} 條評論"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "外觀",
|
"settingsAppearance": "外觀",
|
||||||
|
"settingsDisplayLanguage": "顯示語言",
|
||||||
|
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||||
|
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||||
"settingsBackgroundImage": "背景圖片",
|
"settingsBackgroundImage": "背景圖片",
|
||||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||||
@@ -185,10 +208,19 @@
|
|||||||
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||||
"settingsAppBarTransparent": "透明頂欄",
|
"settingsAppBarTransparent": "透明頂欄",
|
||||||
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
|
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
|
||||||
|
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
|
||||||
|
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
|
||||||
"settingsColorScheme": "主題色",
|
"settingsColorScheme": "主題色",
|
||||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||||
"settingsColorSeed": "預設色彩主題",
|
"settingsColorSeed": "預設色彩主題",
|
||||||
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||||
|
"settingsFeatures": "功能",
|
||||||
|
"settingsNotifyWithHaptic": "新通知時振動",
|
||||||
|
"settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
|
||||||
|
"settingsExpandPostLink": "展開帖子鏈接",
|
||||||
|
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
|
||||||
|
"settingsExpandChatLink": "展開聊天鏈接",
|
||||||
|
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
|
||||||
"settingsNetwork": "網絡",
|
"settingsNetwork": "網絡",
|
||||||
"settingsNetworkServer": "HyperNet 服務器",
|
"settingsNetworkServer": "HyperNet 服務器",
|
||||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||||
@@ -207,12 +239,15 @@
|
|||||||
"settingsMisc": "雜項",
|
"settingsMisc": "雜項",
|
||||||
"settingsMiscAbout": "關於",
|
"settingsMiscAbout": "關於",
|
||||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||||
|
"settingsAccountLanguage": "帳號偏好語言",
|
||||||
|
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
|
||||||
"sensitiveContent": "敏感內容",
|
"sensitiveContent": "敏感內容",
|
||||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||||
"sensitiveContentReveal": "顯示內容",
|
"sensitiveContentReveal": "顯示內容",
|
||||||
"serverConnecting": "正在連接服務器…",
|
"serverConnecting": "正在連接…",
|
||||||
"serverDisconnected": "已與服務器斷開連接",
|
"serverDisconnected": "已斷開連接",
|
||||||
|
"serverConnected": "已連接",
|
||||||
"fieldChatAlias": "頻道別名",
|
"fieldChatAlias": "頻道別名",
|
||||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||||
"fieldChatName": "名稱",
|
"fieldChatName": "名稱",
|
||||||
@@ -279,18 +314,25 @@
|
|||||||
"one": "{} 個附件",
|
"one": "{} 個附件",
|
||||||
"other": "{} 個附件"
|
"other": "{} 個附件"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} 正在輸入",
|
||||||
|
"other": "{} 正在輸入"
|
||||||
|
},
|
||||||
"fieldAttachmentRandomId": "訪問 ID",
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘貼附件",
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||||
|
"attachmentDetailInfo": "附件詳細信息",
|
||||||
"attachmentPastedImage": "粘貼的圖片",
|
"attachmentPastedImage": "粘貼的圖片",
|
||||||
"attachmentInsertLink": "插入連接",
|
"attachmentInsertLink": "插入連接",
|
||||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||||
"attachmentCompressVideo": "重新編碼視頻",
|
"attachmentCompressVideo": "重新編碼視頻",
|
||||||
"attachmentSetThumbnail": "設置縮略圖",
|
"attachmentSetThumbnail": "設置縮略圖",
|
||||||
|
"attachmentSetAlt": "設置概述文字",
|
||||||
"attachmentCopyRandomId": "複製訪問 ID",
|
"attachmentCopyRandomId": "複製訪問 ID",
|
||||||
"attachmentUpload": "上傳",
|
"attachmentUpload": "上傳",
|
||||||
"attachmentInputDialog": "上傳附件",
|
"attachmentInputDialog": "上傳附件",
|
||||||
@@ -308,6 +350,14 @@
|
|||||||
"attachmentUploaded": "已上傳",
|
"attachmentUploaded": "已上傳",
|
||||||
"attachmentPending": "未上傳",
|
"attachmentPending": "未上傳",
|
||||||
"attachmentCopyCompressed": "有壓縮副本",
|
"attachmentCopyCompressed": "有壓縮副本",
|
||||||
|
"attachmentGotBoosted": "有加速傳遞",
|
||||||
|
"attachmentBoost": "加速包",
|
||||||
|
"attachmentCreateBoost": "加速傳遞",
|
||||||
|
"attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。",
|
||||||
|
"attachmentDestinationRegion": "目標節點",
|
||||||
|
"attachmentDestinationRegionAPAC": "亞太地區",
|
||||||
|
"attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波",
|
||||||
|
"attachmentDestinationRegionHKG": "香港",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "無未讀通知",
|
"zero": "無未讀通知",
|
||||||
@@ -396,6 +446,9 @@
|
|||||||
"dailyCheckNegativeHint6": "出門",
|
"dailyCheckNegativeHint6": "出門",
|
||||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||||
"celebrateBirthday": "生日快樂,{}!",
|
"celebrateBirthday": "生日快樂,{}!",
|
||||||
|
"celebrateLunarNewYear": "春節快樂,{}!",
|
||||||
|
"celebrateMidAutumn": "中秋節快樂,{}!",
|
||||||
|
"celebrateDragonBoat": "端午節快樂,{}!",
|
||||||
"celebrateMerryXmas": "聖誕快樂,{}!",
|
"celebrateMerryXmas": "聖誕快樂,{}!",
|
||||||
"celebrateNewYear": "新年快樂,{}!",
|
"celebrateNewYear": "新年快樂,{}!",
|
||||||
"celebrateValentineDay": "今天是情人節,{}!",
|
"celebrateValentineDay": "今天是情人節,{}!",
|
||||||
@@ -405,6 +458,9 @@
|
|||||||
"celebrateFatherDay": "今天是父親節,{}。",
|
"celebrateFatherDay": "今天是父親節,{}。",
|
||||||
"celebrateHalloween": "快樂在聖誕節,{}!",
|
"celebrateHalloween": "快樂在聖誕節,{}!",
|
||||||
"celebrateThanksgiving": "今天是感恩節,{}!",
|
"celebrateThanksgiving": "今天是感恩節,{}!",
|
||||||
|
"pendingLunarNewYear": "{} 過春節",
|
||||||
|
"pendingMidAutumn": "{} 過中秋節",
|
||||||
|
"pendingDragonBoat": "{} 過端午節",
|
||||||
"pendingBirthday": "{} 過生日",
|
"pendingBirthday": "{} 過生日",
|
||||||
"pendingMerryXmas": "{} 過聖誕節",
|
"pendingMerryXmas": "{} 過聖誕節",
|
||||||
"pendingNewYear": "{} 跨年",
|
"pendingNewYear": "{} 跨年",
|
||||||
@@ -448,6 +504,7 @@
|
|||||||
"accountJoinedAt": "加入於 {}",
|
"accountJoinedAt": "加入於 {}",
|
||||||
"accountBirthday": "出生於 {}",
|
"accountBirthday": "出生於 {}",
|
||||||
"accountBadge": "徽章",
|
"accountBadge": "徽章",
|
||||||
|
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||||
"badgeSiteMigration": "Solar Network 原住民",
|
"badgeSiteMigration": "Solar Network 原住民",
|
||||||
"accountStatus": "狀態",
|
"accountStatus": "狀態",
|
||||||
@@ -456,6 +513,7 @@
|
|||||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
|
"postLocalDraftRestored": "從本地恢復草稿",
|
||||||
"articleWrittenAt": "發表於 {}",
|
"articleWrittenAt": "發表於 {}",
|
||||||
"articleEditedAt": "編輯於 {}",
|
"articleEditedAt": "編輯於 {}",
|
||||||
"attachmentSaved": "已保存到相冊",
|
"attachmentSaved": "已保存到相冊",
|
||||||
@@ -496,11 +554,15 @@
|
|||||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||||
"postShare": "分享",
|
"postShare": "分享",
|
||||||
"postShareImage": "分享帖圖",
|
"postShareImage": "分享帖圖",
|
||||||
|
"postGetInsight": "獲取見解",
|
||||||
|
"postGetInsightTitle": "AI 見解",
|
||||||
|
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||||
"appInitializing": "正在初始化",
|
"appInitializing": "正在初始化",
|
||||||
"poweredBy": "由 {} 提供支持",
|
"poweredBy": "由 {} 提供支持",
|
||||||
"shareIntent": "分享",
|
"shareIntent": "分享",
|
||||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||||
"shareIntentPostStory": "發佈動態",
|
"shareIntentPostStory": "發佈動態",
|
||||||
|
"shareIntentSendChannel": "分享到聊天頻道",
|
||||||
"updateAvailable": "檢測到更新可用",
|
"updateAvailable": "檢測到更新可用",
|
||||||
"updateOngoing": "正在更新,請稍後……",
|
"updateOngoing": "正在更新,請稍後……",
|
||||||
"custom": "自定義",
|
"custom": "自定義",
|
||||||
@@ -513,6 +575,7 @@
|
|||||||
"colorSchemeWhite": "白色",
|
"colorSchemeWhite": "白色",
|
||||||
"colorSchemeBlack": "黑色",
|
"colorSchemeBlack": "黑色",
|
||||||
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
||||||
|
"postFeaturedComment": "精選評論",
|
||||||
"postCategoryTechnology": "技術",
|
"postCategoryTechnology": "技術",
|
||||||
"postCategoryGaming": "遊戲",
|
"postCategoryGaming": "遊戲",
|
||||||
"postCategoryLife": "生活",
|
"postCategoryLife": "生活",
|
||||||
@@ -523,5 +586,26 @@
|
|||||||
"postCategoryKnowledge": "知識",
|
"postCategoryKnowledge": "知識",
|
||||||
"postCategoryLiterature": "文學",
|
"postCategoryLiterature": "文學",
|
||||||
"postCategoryFunny": "搞笑",
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分類"
|
"postCategoryUncategorized": "未分類",
|
||||||
|
"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": "帳號設置已應用。"
|
||||||
}
|
}
|
||||||
|
|||||||
112
ios/Podfile.lock
112
ios/Podfile.lock
@@ -43,58 +43,58 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- file_saver (0.0.1):
|
- file_saver (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Firebase/Analytics (11.4.0):
|
- Firebase/Analytics (11.7.0):
|
||||||
- Firebase/Core
|
- Firebase/Core
|
||||||
- Firebase/Core (11.4.0):
|
- Firebase/Core (11.7.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseAnalytics (~> 11.4.0)
|
- FirebaseAnalytics (~> 11.7.0)
|
||||||
- Firebase/CoreOnly (11.4.0):
|
- Firebase/CoreOnly (11.7.0):
|
||||||
- FirebaseCore (= 11.4.0)
|
- FirebaseCore (~> 11.7.0)
|
||||||
- Firebase/Messaging (11.4.0):
|
- Firebase/Messaging (11.7.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 11.4.0)
|
- FirebaseMessaging (~> 11.7.0)
|
||||||
- firebase_analytics (11.3.6):
|
- firebase_analytics (11.4.2):
|
||||||
- Firebase/Analytics (= 11.4.0)
|
- Firebase/Analytics (= 11.7.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_core (3.9.0):
|
- firebase_core (3.11.0):
|
||||||
- Firebase/CoreOnly (= 11.4.0)
|
- Firebase/CoreOnly (= 11.7.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_messaging (15.1.6):
|
- firebase_messaging (15.2.2):
|
||||||
- Firebase/Messaging (= 11.4.0)
|
- Firebase/Messaging (= 11.7.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- FirebaseAnalytics (11.4.0):
|
- FirebaseAnalytics (11.7.0):
|
||||||
- FirebaseAnalytics/AdIdSupport (= 11.4.0)
|
- FirebaseAnalytics/AdIdSupport (= 11.7.0)
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.7.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseAnalytics/AdIdSupport (11.4.0):
|
- FirebaseAnalytics/AdIdSupport (11.7.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.7.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleAppMeasurement (= 11.4.0)
|
- GoogleAppMeasurement (= 11.7.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseCore (11.4.0):
|
- FirebaseCore (11.7.0):
|
||||||
- FirebaseCoreInternal (~> 11.0)
|
- FirebaseCoreInternal (~> 11.7.0)
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- GoogleUtilities/Logger (~> 8.0)
|
- GoogleUtilities/Logger (~> 8.0)
|
||||||
- FirebaseCoreInternal (11.6.0):
|
- FirebaseCoreInternal (11.7.0):
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- FirebaseInstallations (11.4.0):
|
- FirebaseInstallations (11.7.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.7.0)
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseMessaging (11.4.0):
|
- FirebaseMessaging (11.7.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.7.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleDataTransport (~> 10.0)
|
- GoogleDataTransport (~> 10.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
@@ -105,32 +105,39 @@ PODS:
|
|||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_app_update (0.0.1):
|
- flutter_app_update (0.0.1):
|
||||||
- Flutter
|
- 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_native_splash (2.4.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- flutter_webrtc (0.12.2):
|
- flutter_webrtc (0.12.6):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- GoogleAppMeasurement (11.4.0):
|
- GoogleAppMeasurement (11.7.0):
|
||||||
- GoogleAppMeasurement/AdIdSupport (= 11.4.0)
|
- GoogleAppMeasurement/AdIdSupport (= 11.7.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/AdIdSupport (11.4.0):
|
- GoogleAppMeasurement/AdIdSupport (11.7.0):
|
||||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
|
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
|
- GoogleAppMeasurement/WithoutAdIdSupport (11.7.0):
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
@@ -172,8 +179,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.1.3)
|
- Kingfisher (8.2.0)
|
||||||
- livekit_client (2.3.3):
|
- livekit_client (2.3.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
@@ -188,6 +195,7 @@ PODS:
|
|||||||
- nanopb/encode (= 3.30910.0)
|
- nanopb/encode (= 3.30910.0)
|
||||||
- nanopb/decode (3.30910.0)
|
- nanopb/decode (3.30910.0)
|
||||||
- nanopb/encode (3.30910.0)
|
- nanopb/encode (3.30910.0)
|
||||||
|
- OrderedSet (6.0.3)
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- pasteboard (0.0.1):
|
- pasteboard (0.0.1):
|
||||||
@@ -239,6 +247,7 @@ DEPENDENCIES:
|
|||||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
|
- 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_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||||
@@ -282,6 +291,7 @@ SPEC REPOS:
|
|||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
- nanopb
|
- nanopb
|
||||||
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
@@ -309,6 +319,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_app_update:
|
flutter_app_update:
|
||||||
:path: ".symlinks/plugins/flutter_app_update/ios"
|
:path: ".symlinks/plugins/flutter_app_update/ios"
|
||||||
|
flutter_inappwebview_ios:
|
||||||
|
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_udid:
|
flutter_udid:
|
||||||
@@ -367,35 +379,37 @@ SPEC CHECKSUMS:
|
|||||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||||
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
|
Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4
|
||||||
firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75
|
firebase_analytics: 7236e6115c1b4e62c2270faa29c052a317e31107
|
||||||
firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10
|
firebase_core: aa979ae726f00b3ef4ccf59dfb96170af84efbd4
|
||||||
firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9
|
firebase_messaging: 3af84b6a90aeac4d7a67fbf4c43a91e7083bea1f
|
||||||
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
|
FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec
|
||||||
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
|
FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4
|
||||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881
|
||||||
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
|
FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9
|
||||||
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
|
FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||||
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
|
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||||
|
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
|
||||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||||
flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563
|
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
||||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||||
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
|
GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||||
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
|
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
|
||||||
livekit_client: 02cf2cc4357a655af12ccee70ff5596ae4e6feef
|
livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
|
||||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||||
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
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/user_directory.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
|
import 'package:surface/types/websocket.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class ChatMessageController extends ChangeNotifier {
|
class ChatMessageController extends ChangeNotifier {
|
||||||
@@ -36,8 +38,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
|
|
||||||
int? messageTotal;
|
int? messageTotal;
|
||||||
|
|
||||||
bool get isAllLoaded =>
|
bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!;
|
||||||
messageTotal != null && messages.length >= messageTotal!;
|
|
||||||
|
|
||||||
String? _boxKey;
|
String? _boxKey;
|
||||||
SnChannel? channel;
|
SnChannel? channel;
|
||||||
@@ -50,8 +51,10 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
/// Stored as a list of nonce to provide the loading state
|
/// Stored as a list of nonce to provide the loading state
|
||||||
final List<String> unconfirmedMessages = List.empty(growable: true);
|
final List<String> unconfirmedMessages = List.empty(growable: true);
|
||||||
|
|
||||||
Box<SnChatMessage>? get _box =>
|
Box<SnChatMessage>? get _box => (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
|
||||||
(_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 {
|
Future<void> initialize(SnChannel chan) async {
|
||||||
channel = chan;
|
channel = chan;
|
||||||
@@ -71,6 +74,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
_wsSubscription = _ws.stream.stream.listen((event) {
|
_wsSubscription = _ws.stream.stream.listen((event) {
|
||||||
switch (event.method) {
|
switch (event.method) {
|
||||||
case 'events.new':
|
case 'events.new':
|
||||||
|
if (event.payload?['channel_id'] != channel?.id) break;
|
||||||
final payload = SnChatMessage.fromJson(event.payload!);
|
final payload = SnChatMessage.fromJson(event.payload!);
|
||||||
_addMessage(payload);
|
_addMessage(payload);
|
||||||
break;
|
break;
|
||||||
@@ -78,22 +82,16 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
if (event.payload?['channel_id'] != channel?.id) break;
|
if (event.payload?['channel_id'] != channel?.id) break;
|
||||||
final member = SnChannelMember.fromJson(event.payload!['member']);
|
final member = SnChannelMember.fromJson(event.payload!['member']);
|
||||||
if (member.id == profile?.id) break;
|
if (member.id == profile?.id) break;
|
||||||
// TODO impl typing users
|
if (!typingMembers.any((x) => x.id == member.id)) {
|
||||||
// if (!_typingUsers.any((x) => x.id == member.id)) {
|
typingMembers.add(member);
|
||||||
// setState(() {
|
notifyListeners();
|
||||||
// _typingUsers.add(member);
|
}
|
||||||
// });
|
typingInactiveTimer[member.id]?.cancel();
|
||||||
// }
|
typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () {
|
||||||
// _typingInactiveTimer[member.id]?.cancel();
|
typingMembers.removeWhere((x) => x.id == member.id);
|
||||||
// _typingInactiveTimer[member.id] = Timer(
|
typingInactiveTimer.remove(member.id);
|
||||||
// const Duration(seconds: 3),
|
notifyListeners();
|
||||||
// () {
|
});
|
||||||
// setState(() {
|
|
||||||
// _typingUsers.removeWhere((x) => x.id == member.id);
|
|
||||||
// _typingInactiveTimer.remove(member.id);
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,6 +99,35 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
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 {
|
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
|
||||||
if (_box == null) return;
|
if (_box == null) return;
|
||||||
await _box!.putAll({
|
await _box!.putAll({
|
||||||
@@ -167,8 +194,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'messages.edit':
|
case 'messages.edit':
|
||||||
if (message.relatedEventId != null) {
|
if (message.relatedEventId != null) {
|
||||||
final idx =
|
final idx = messages.indexWhere((x) => x.id == message.relatedEventId);
|
||||||
messages.indexWhere((x) => x.id == message.relatedEventId);
|
|
||||||
if (idx != -1) {
|
if (idx != -1) {
|
||||||
final newBody = message.body;
|
final newBody = message.body;
|
||||||
newBody.remove('related_event');
|
newBody.remove('related_event');
|
||||||
@@ -207,8 +233,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
'algorithm': 'plain',
|
'algorithm': 'plain',
|
||||||
if (quoteId != null) 'quote_event': quoteId,
|
if (quoteId != null) 'quote_event': quoteId,
|
||||||
if (relatedId != null) 'related_event': relatedId,
|
if (relatedId != null) 'related_event': relatedId,
|
||||||
if (attachments != null && attachments.isNotEmpty)
|
if (attachments != null && attachments.isNotEmpty) 'attachments': attachments,
|
||||||
'attachments': attachments,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock the message locally
|
// Mock the message locally
|
||||||
@@ -305,8 +330,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
|
|
||||||
if (out == null) {
|
if (out == null) {
|
||||||
try {
|
try {
|
||||||
final resp = await _sn.client
|
final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
||||||
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
|
||||||
out = SnChatMessage.fromJson(resp.data);
|
out = SnChatMessage.fromJson(resp.data);
|
||||||
_saveMessageToLocal([out]);
|
_saveMessageToLocal([out]);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -341,9 +365,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
bool forceRemote = false,
|
bool forceRemote = false,
|
||||||
}) async {
|
}) async {
|
||||||
late List<SnChatMessage> out;
|
late List<SnChatMessage> out;
|
||||||
if (_box != null &&
|
if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) {
|
||||||
(_box!.length >= take + offset || forceLocal) &&
|
|
||||||
!forceRemote) {
|
|
||||||
out = _box!.keys
|
out = _box!.keys
|
||||||
.toList()
|
.toList()
|
||||||
.cast<int>()
|
.cast<int>()
|
||||||
@@ -386,8 +408,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
quoteEvent: quoteEvent,
|
quoteEvent: quoteEvent,
|
||||||
attachments: attachments
|
attachments: attachments
|
||||||
.where(
|
.where(
|
||||||
(ele) =>
|
(ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
||||||
out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
@@ -395,10 +416,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Preload sender accounts
|
// Preload sender accounts
|
||||||
final accountId = out
|
final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet();
|
||||||
.where((ele) => ele.sender.accountId >= 0)
|
|
||||||
.map((ele) => ele.sender.accountId)
|
|
||||||
.toSet();
|
|
||||||
await _ud.listAccount(accountId);
|
await _ud.listAccount(accountId);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
@@ -8,6 +11,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
@@ -100,7 +104,7 @@ class PostWriteMedia {
|
|||||||
if (attachment != null) {
|
if (attachment != null) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
||||||
if (width != null && height != null) {
|
if (width != null && height != null && !kIsWeb) {
|
||||||
return ResizeImage(
|
return ResizeImage(
|
||||||
provider,
|
provider,
|
||||||
width: width,
|
width: width,
|
||||||
@@ -150,9 +154,22 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
final TextEditingController descriptionController = TextEditingController();
|
final TextEditingController descriptionController = TextEditingController();
|
||||||
final TextEditingController aliasController = TextEditingController();
|
final TextEditingController aliasController = TextEditingController();
|
||||||
|
|
||||||
PostWriteController() {
|
bool _temporarySaveActive = false;
|
||||||
titleController.addListener(() => notifyListeners());
|
|
||||||
descriptionController.addListener(() => notifyListeners());
|
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;
|
String mode = kTitleMap.keys.first;
|
||||||
@@ -199,11 +216,11 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
aliasController.text = post.alias ?? '';
|
aliasController.text = post.alias ?? '';
|
||||||
publishedAt = post.publishedAt;
|
publishedAt = post.publishedAt;
|
||||||
publishedUntil = post.publishedUntil;
|
publishedUntil = post.publishedUntil;
|
||||||
visibleUsers = List.from(post.visibleUsersList ?? []);
|
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||||
invisibleUsers = List.from(post.invisibleUsersList ?? []);
|
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
|
||||||
visibility = post.visibility;
|
visibility = post.visibility;
|
||||||
tags = List.from(post.tags.map((ele) => ele.alias));
|
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
||||||
categories = List.from(post.categories.map((ele) => ele.alias));
|
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||||
|
|
||||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||||
@@ -231,7 +248,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media, {bool isCompressed = false}) async {
|
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media,
|
||||||
|
{bool isCompressed = false}) async {
|
||||||
final attach = context.read<SnAttachmentProvider>();
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
|
||||||
final place = await attach.chunkedUploadInitialize(
|
final place = await attach.chunkedUploadInitialize(
|
||||||
@@ -242,10 +260,11 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
final item = await attach.chunkedUploadParts(
|
var item = await attach.chunkedUploadParts(
|
||||||
media.toFile()!,
|
media.toFile()!,
|
||||||
place.$1,
|
place.$1,
|
||||||
place.$2,
|
place.$2,
|
||||||
|
analyzeNow: media.type == SnMediaType.image,
|
||||||
onProgress: (value) {
|
onProgress: (value) {
|
||||||
progress = value;
|
progress = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -253,9 +272,13 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
|
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
|
||||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
try {
|
||||||
if (compressedAttachment != null) {
|
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||||
await attach.updateOne(item, compressedId: compressedAttachment.id);
|
if (compressedAttachment != null) {
|
||||||
|
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +315,83 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
return compressedAttachment;
|
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 {
|
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
|
||||||
if (isBusy) return;
|
if (isBusy) return;
|
||||||
|
|
||||||
@@ -336,7 +436,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
final item = await attach.chunkedUploadParts(
|
var item = await attach.chunkedUploadParts(
|
||||||
media.toFile()!,
|
media.toFile()!,
|
||||||
place.$1,
|
place.$1,
|
||||||
place.$2,
|
place.$2,
|
||||||
@@ -347,11 +447,15 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (media.type == SnMediaType.video && context.mounted) {
|
try {
|
||||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
if (context.mounted) {
|
||||||
if (compressedAttachment != null) {
|
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||||
await attach.updateOne(item, compressedId: compressedAttachment.id);
|
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;
|
progress = (i + 1) / attachments.length * kAttachmentProgressWeight;
|
||||||
@@ -407,6 +511,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
method: editingPost != null ? 'PUT' : 'POST',
|
method: editingPost != null ? 'PUT' : 'POST',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
reset();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -455,73 +560,88 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
|
|
||||||
void setPublisher(SnPublisher? item) {
|
void setPublisher(SnPublisher? item) {
|
||||||
publisher = item;
|
publisher = item;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPublishedAt(DateTime? value) {
|
void setPublishedAt(DateTime? value) {
|
||||||
publishedAt = value;
|
publishedAt = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPublishedUntil(DateTime? value) {
|
void setPublishedUntil(DateTime? value) {
|
||||||
publishedUntil = value;
|
publishedUntil = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setTags(List<String> value) {
|
void setTags(List<String> value) {
|
||||||
tags = value;
|
tags = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setCategories(List<String> value) {
|
void setCategories(List<String> value) {
|
||||||
categories = value;
|
categories = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setVisibility(int value) {
|
void setVisibility(int value) {
|
||||||
visibility = value;
|
visibility = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setVisibleUsers(List<int> value) {
|
void setVisibleUsers(List<int> value) {
|
||||||
visibleUsers = value;
|
visibleUsers = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setInvisibleUsers(List<int> value) {
|
void setInvisibleUsers(List<int> value) {
|
||||||
invisibleUsers = value;
|
invisibleUsers = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setProgress(double? value) {
|
void setProgress(double? value) {
|
||||||
progress = value;
|
progress = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setIsBusy(bool value) {
|
void setIsBusy(bool value) {
|
||||||
isBusy = value;
|
isBusy = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMode(String value) {
|
void setMode(String value) {
|
||||||
mode = value;
|
mode = value;
|
||||||
|
_temporaryPlanSave();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
publishedAt = null;
|
publishedAt = null;
|
||||||
publishedUntil = null;
|
publishedUntil = null;
|
||||||
|
thumbnail = null;
|
||||||
|
visibility = 0;
|
||||||
titleController.clear();
|
titleController.clear();
|
||||||
descriptionController.clear();
|
descriptionController.clear();
|
||||||
contentController.clear();
|
contentController.clear();
|
||||||
aliasController.clear();
|
aliasController.clear();
|
||||||
tags.clear();
|
tags = List.empty(growable: true);
|
||||||
categories.clear();
|
categories = List.empty(growable: true);
|
||||||
attachments.clear();
|
attachments = List.empty(growable: true);
|
||||||
editingPost = null;
|
editingPost = null;
|
||||||
replyingPost = null;
|
replyingPost = null;
|
||||||
repostingPost = null;
|
repostingPost = null;
|
||||||
mode = kTitleMap.keys.first;
|
mode = kTitleMap.keys.first;
|
||||||
|
temporaryRestored = false;
|
||||||
|
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
159
lib/main.dart
159
lib/main.dart
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:croppy/croppy.dart';
|
import 'package:croppy/croppy.dart';
|
||||||
@@ -10,15 +11,15 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
|
|||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.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:go_router/go_router.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.dart';
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
import 'package:surface/firebase_options.dart';
|
import 'package:surface/firebase_options.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/chat_call.dart';
|
import 'package:surface/providers/chat_call.dart';
|
||||||
@@ -30,6 +31,7 @@ import 'package:surface/providers/post.dart';
|
|||||||
import 'package:surface/providers/relationship.dart';
|
import 'package:surface/providers/relationship.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.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/special_day.dart';
|
||||||
import 'package:surface/providers/theme.dart';
|
import 'package:surface/providers/theme.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
@@ -41,7 +43,7 @@ import 'package:surface/types/chat.dart';
|
|||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||||
import 'package:surface/widgets/dialog.dart';
|
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:version/version.dart';
|
||||||
import 'package:workmanager/workmanager.dart';
|
import 'package:workmanager/workmanager.dart';
|
||||||
import 'package:in_app_review/in_app_review.dart';
|
import 'package:in_app_review/in_app_review.dart';
|
||||||
@@ -144,6 +146,7 @@ class SolianApp extends StatelessWidget {
|
|||||||
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
||||||
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
||||||
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
|
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
|
||||||
|
Provider(create: (ctx) => SnStickerProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
||||||
@@ -207,9 +210,7 @@ class _AppSplashScreen extends StatefulWidget {
|
|||||||
State<_AppSplashScreen> createState() => _AppSplashScreenState();
|
State<_AppSplashScreen> createState() => _AppSplashScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppSplashScreenState extends State<_AppSplashScreen> {
|
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||||
bool _isReady = false;
|
|
||||||
|
|
||||||
void _tryRequestRating() async {
|
void _tryRequestRating() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (prefs.containsKey('first_boot_time')) {
|
if (prefs.containsKey('first_boot_time')) {
|
||||||
@@ -261,6 +262,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
|
|
||||||
Future<void> _initialize() async {
|
Future<void> _initialize() async {
|
||||||
try {
|
try {
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
cfg.calcDrawerSize(context);
|
||||||
|
});
|
||||||
final home = context.read<HomeWidgetProvider>();
|
final home = context.read<HomeWidgetProvider>();
|
||||||
await home.initialize();
|
await home.initialize();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -278,12 +283,14 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
await ws.tryConnect();
|
await ws.tryConnect();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final notify = context.read<NotificationProvider>();
|
final notify = context.read<NotificationProvider>();
|
||||||
|
notify.listen();
|
||||||
await notify.registerPushNotifications();
|
await notify.registerPushNotifications();
|
||||||
|
if (!mounted) return;
|
||||||
|
final sticker = context.read<SnStickerProvider>();
|
||||||
|
await sticker.listStickerEagerly();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await context.showErrorDialog(err);
|
await context.showErrorDialog(err);
|
||||||
} finally {
|
|
||||||
setState(() => _isReady = true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,9 +298,62 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
await widgetUpdateRandomPost();
|
await widgetUpdateRandomPost();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _hotkeyInitialization() async {
|
||||||
|
if (kIsWeb) return;
|
||||||
|
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
HotKey quitHotKey = HotKey(
|
||||||
|
key: PhysicalKeyboardKey.keyQ,
|
||||||
|
modifiers: [HotKeyModifier.meta],
|
||||||
|
scope: HotKeyScope.inapp,
|
||||||
|
);
|
||||||
|
await hotKeyManager.register(quitHotKey, keyUpHandler: (_) {
|
||||||
|
_appLifecycleListener?.dispose();
|
||||||
|
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
||||||
|
_appLifecycleListener = AppLifecycleListener(
|
||||||
|
onExitRequested: _onExitRequested,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_trayInitialization();
|
||||||
|
_hotkeyInitialization();
|
||||||
_initialize().then((_) {
|
_initialize().then((_) {
|
||||||
_postInitialization();
|
_postInitialization();
|
||||||
_tryRequestRating();
|
_tryRequestRating();
|
||||||
@@ -301,34 +361,63 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<AppExitResponse> _onExitRequested() async {
|
||||||
|
appWindow.hide();
|
||||||
|
return AppExitResponse.cancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onTrayIconMouseDown() {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
context.read<NotificationProvider>().clearTray();
|
||||||
|
appWindow.show();
|
||||||
|
} else {
|
||||||
|
trayManager.popUpContextMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onTrayIconRightMouseDown() {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
trayManager.popUpContextMenu();
|
||||||
|
} else {
|
||||||
|
context.read<NotificationProvider>().clearTray();
|
||||||
|
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);
|
||||||
|
hotKeyManager.unregisterAll();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!_isReady) {
|
final cfg = context.read<ConfigProvider>();
|
||||||
return Scaffold(
|
return NotificationListener<SizeChangedLayoutNotification>(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
onNotification: (notification) {
|
||||||
body: Container(
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
constraints: const BoxConstraints(maxWidth: 180),
|
cfg.calcDrawerSize(context);
|
||||||
child: Column(
|
});
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
return false;
|
||||||
mainAxisSize: MainAxisSize.min,
|
},
|
||||||
children: [
|
child: SizeChangedLayoutNotifier(
|
||||||
if (MediaQuery.of(context).platformBrightness == Brightness.dark)
|
child: widget.child,
|
||||||
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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return widget.child;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:surface/providers/widget.dart';
|
import 'package:surface/providers/widget.dart';
|
||||||
|
|
||||||
@@ -12,6 +13,10 @@ const kNetworkServerStoreKey = 'app_server_url';
|
|||||||
const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
||||||
const kAppBackgroundStoreKey = 'app_has_background';
|
const kAppBackgroundStoreKey = 'app_has_background';
|
||||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
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 = {
|
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||||
'settingsImageQualityLowest': FilterQuality.none,
|
'settingsImageQualityLowest': FilterQuality.none,
|
||||||
@@ -33,6 +38,32 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
prefs = await SharedPreferences.getInstance();
|
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 {
|
FilterQuality get imageQuality {
|
||||||
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
|
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
screen: 'realm',
|
screen: 'realm',
|
||||||
label: 'screenRealm',
|
label: 'screenRealm',
|
||||||
),
|
),
|
||||||
|
AppNavDestination(
|
||||||
|
icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
|
||||||
|
screen: 'news',
|
||||||
|
label: 'screenNews',
|
||||||
|
),
|
||||||
AppNavDestination(
|
AppNavDestination(
|
||||||
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
||||||
screen: 'album',
|
screen: 'album',
|
||||||
@@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
List<AppNavDestination> destinations = [];
|
List<AppNavDestination> destinations = [];
|
||||||
|
|
||||||
int get pinnedDestinationCount =>
|
int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
|
||||||
destinations.where((ele) => ele.isPinned).length;
|
|
||||||
|
|
||||||
NavigationProvider() {
|
NavigationProvider() {
|
||||||
buildDestinations(kDefaultPinnedDestination);
|
buildDestinations(kDefaultPinnedDestination);
|
||||||
@@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isIndexInRange(int min, int max) {
|
bool isIndexInRange(int min, int max) {
|
||||||
return _currentIndex != null &&
|
return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
|
||||||
_currentIndex! >= min &&
|
|
||||||
_currentIndex! < max;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void autoDetectIndex(GoRouter? state) {
|
void autoDetectIndex(GoRouter? state) {
|
||||||
if (state == null) return;
|
if (state == null) return;
|
||||||
final idx = destinations.indexWhere(
|
final idx = destinations.indexWhere(
|
||||||
(ele) =>
|
(ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
|
||||||
ele.screen ==
|
|
||||||
state.routerDelegate.currentConfiguration.last.route.name,
|
|
||||||
);
|
);
|
||||||
_currentIndex = idx == -1 ? null : idx;
|
_currentIndex = idx == -1 ? null : idx;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -4,18 +4,27 @@ import 'dart:io';
|
|||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_udid/flutter_udid.dart';
|
import 'package:flutter_udid/flutter_udid.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.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 {
|
class NotificationProvider extends ChangeNotifier {
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
late final UserProvider _ua;
|
late final UserProvider _ua;
|
||||||
|
late final WebSocketProvider _ws;
|
||||||
|
late final ConfigProvider _cfg;
|
||||||
|
|
||||||
NotificationProvider(BuildContext context) {
|
NotificationProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
_ua = context.read<UserProvider>();
|
_ua = context.read<UserProvider>();
|
||||||
|
_ws = context.read<WebSocketProvider>();
|
||||||
|
_cfg = context.read<ConfigProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> registerPushNotifications() async {
|
Future<void> registerPushNotifications() async {
|
||||||
@@ -62,4 +71,49 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int showingCount = 0;
|
||||||
|
int showingTrayCount = 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++;
|
||||||
|
showingTrayCount++;
|
||||||
|
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 clearTray() {
|
||||||
|
showingTrayCount = 0;
|
||||||
|
updateTray();
|
||||||
|
}
|
||||||
|
|
||||||
|
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, {
|
Map<String, dynamic>? metadata, {
|
||||||
String? mimetype,
|
String? mimetype,
|
||||||
Function(double progress)? onProgress,
|
Function(double progress)? onProgress,
|
||||||
|
bool analyzeNow = false,
|
||||||
}) async {
|
}) async {
|
||||||
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
||||||
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
||||||
@@ -108,6 +109,7 @@ class SnAttachmentProvider {
|
|||||||
final resp = await _sn.client.post(
|
final resp = await _sn.client.post(
|
||||||
'/cgi/uc/attachments',
|
'/cgi/uc/attachments',
|
||||||
data: formData,
|
data: formData,
|
||||||
|
queryParameters: {'analyzeNow': analyzeNow},
|
||||||
onSendProgress: (count, total) {
|
onSendProgress: (count, total) {
|
||||||
if (onProgress != null) {
|
if (onProgress != null) {
|
||||||
onProgress(count / total);
|
onProgress(count / total);
|
||||||
@@ -152,10 +154,12 @@ class SnAttachmentProvider {
|
|||||||
String rid,
|
String rid,
|
||||||
String cid, {
|
String cid, {
|
||||||
Function(double progress)? onProgress,
|
Function(double progress)? onProgress,
|
||||||
|
bool analyzeNow = false,
|
||||||
}) async {
|
}) async {
|
||||||
final resp = await _sn.client.post(
|
final resp = await _sn.client.post(
|
||||||
'/cgi/uc/fragments/$rid/$cid',
|
'/cgi/uc/fragments/$rid/$cid',
|
||||||
data: data,
|
data: data,
|
||||||
|
queryParameters: {'analyzeNow': analyzeNow},
|
||||||
options: Options(headers: {'Content-Type': 'application/octet-stream'}),
|
options: Options(headers: {'Content-Type': 'application/octet-stream'}),
|
||||||
onSendProgress: (count, total) {
|
onSendProgress: (count, total) {
|
||||||
if (onProgress != null) {
|
if (onProgress != null) {
|
||||||
@@ -176,6 +180,7 @@ class SnAttachmentProvider {
|
|||||||
SnAttachmentFragment place,
|
SnAttachmentFragment place,
|
||||||
int chunkSize, {
|
int chunkSize, {
|
||||||
Function(double progress)? onProgress,
|
Function(double progress)? onProgress,
|
||||||
|
bool analyzeNow = false,
|
||||||
}) async {
|
}) async {
|
||||||
final Map<String, dynamic> chunks = place.fileChunks;
|
final Map<String, dynamic> chunks = place.fileChunks;
|
||||||
var completedTasks = 0;
|
var completedTasks = 0;
|
||||||
@@ -198,6 +203,7 @@ class SnAttachmentProvider {
|
|||||||
data,
|
data,
|
||||||
place.rid,
|
place.rid,
|
||||||
entry.key,
|
entry.key,
|
||||||
|
analyzeNow: analyzeNow,
|
||||||
onProgress: (progress) {
|
onProgress: (progress) {
|
||||||
final overallProgress = (completedTasks + progress) / chunks.length;
|
final overallProgress = (completedTasks + progress) / chunks.length;
|
||||||
onProgress?.call(overallProgress);
|
onProgress?.call(overallProgress);
|
||||||
|
|||||||
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';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
|
||||||
// Stored as key: month, day
|
// 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
|
// Birthday is dynamically generated according to the user's profile
|
||||||
'NewYear': (1, 1),
|
'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),
|
'ValentineDay': (2, 14),
|
||||||
'LaborDay': (5, 1),
|
'LaborDay': (5, 1),
|
||||||
'MotherDay': (5, 11),
|
'MotherDay': (5, 11),
|
||||||
@@ -19,6 +22,9 @@ const Map<String, (int, int)> kSpecialDays = {
|
|||||||
const Map<String, String> kSpecialDaysSymbol = {
|
const Map<String, String> kSpecialDaysSymbol = {
|
||||||
'Birthday': '🎂',
|
'Birthday': '🎂',
|
||||||
'NewYear': '🎉',
|
'NewYear': '🎉',
|
||||||
|
'LunarNewYear': '🎉',
|
||||||
|
'MidAutumn': '🥮',
|
||||||
|
'DragonBoat': '🐲',
|
||||||
'MerryXmas': '🎄',
|
'MerryXmas': '🎄',
|
||||||
'ValentineDay': '💑',
|
'ValentineDay': '💑',
|
||||||
'LaborDay': '🏋️',
|
'LaborDay': '🏋️',
|
||||||
@@ -134,3 +140,45 @@ class SpecialDayProvider {
|
|||||||
return (elapsedDuration / totalDuration).clamp(0.0, 1.0);
|
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;
|
user = null;
|
||||||
notifyListeners();
|
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();
|
await connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Completer<void>? _connectCompleter;
|
||||||
|
|
||||||
Future<void> connect({noRetry = false}) async {
|
Future<void> connect({noRetry = false}) async {
|
||||||
|
if(_connectCompleter != null) {
|
||||||
|
await _connectCompleter!.future;
|
||||||
|
_connectCompleter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectCompleter = Completer<void>();
|
||||||
|
|
||||||
if (!_ua.isAuthorized) return;
|
if (!_ua.isAuthorized) return;
|
||||||
if (isConnected) {
|
if (isConnected || conn != null) {
|
||||||
disconnect();
|
disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,12 +73,13 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
log('Retry connecting to websocket in 3 seconds...');
|
log('Retry connecting to websocket in 3 seconds...');
|
||||||
return Future.delayed(
|
return Future.delayed(
|
||||||
const Duration(seconds: 3),
|
const Duration(seconds: 3),
|
||||||
() => connect(noRetry: true),
|
() => connect(noRetry: true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isBusy = false;
|
isBusy = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
_connectCompleter!.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +107,7 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
onError: (err) {
|
onError: (err) {
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
notifyListeners();
|
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 {
|
Future<void> widgetUpdateRandomPost() async {
|
||||||
|
if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) return;
|
||||||
final snc = await SnNetworkProvider.createOffContextClient();
|
final snc = await SnNetworkProvider.createOffContextClient();
|
||||||
final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1');
|
final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1');
|
||||||
final post = SnPost.fromJson(resp.data['data'][0]);
|
final post = SnPost.fromJson(resp.data['data'][0]);
|
||||||
|
|||||||
425
lib/router.dart
425
lib/router.dart
@@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:surface/screens/abuse_report.dart';
|
import 'package:surface/screens/abuse_report.dart';
|
||||||
import 'package:surface/screens/account.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/profile_edit.dart';
|
||||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||||
import 'package:surface/screens/account/publishers/publisher_new.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/explore.dart';
|
||||||
import 'package:surface/screens/friend.dart';
|
import 'package:surface/screens/friend.dart';
|
||||||
import 'package:surface/screens/home.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/notification.dart';
|
||||||
import 'package:surface/screens/post/post_detail.dart';
|
import 'package:surface/screens/post/post_detail.dart';
|
||||||
import 'package:surface/screens/post/post_editor.dart';
|
import 'package:surface/screens/post/post_editor.dart';
|
||||||
@@ -29,290 +33,227 @@ import 'package:surface/screens/realm/manage.dart';
|
|||||||
import 'package:surface/screens/realm/realm_detail.dart';
|
import 'package:surface/screens/realm/realm_detail.dart';
|
||||||
import 'package:surface/screens/settings.dart';
|
import 'package:surface/screens/settings.dart';
|
||||||
import 'package:surface/screens/sharing.dart';
|
import 'package:surface/screens/sharing.dart';
|
||||||
|
import 'package:surface/screens/wallet.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/about.dart';
|
import 'package:surface/widgets/about.dart';
|
||||||
import 'package:surface/widgets/navigation/app_background.dart';
|
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.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 = [
|
final _appRoutes = [
|
||||||
ShellRoute(
|
GoRoute(
|
||||||
builder: (context, state, child) => AppPageScaffold(
|
path: '/',
|
||||||
body: child,
|
name: 'home',
|
||||||
showAppBar: false,
|
builder: (context, state) => const HomeScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/posts',
|
||||||
|
name: 'explore',
|
||||||
|
builder: (context, state) => const ExploreScreen(),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/',
|
path: '/write/:mode',
|
||||||
name: 'home',
|
name: 'postEditor',
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
builder: (context, state) => PostEditorScreen(
|
||||||
child: const HomeScreen(),
|
mode: state.pathParameters['mode']!,
|
||||||
|
postEditId: int.tryParse(
|
||||||
|
state.uri.queryParameters['editing'] ?? '',
|
||||||
|
),
|
||||||
|
postReplyId: int.tryParse(
|
||||||
|
state.uri.queryParameters['replying'] ?? '',
|
||||||
|
),
|
||||||
|
postRepostId: int.tryParse(
|
||||||
|
state.uri.queryParameters['reposting'] ?? '',
|
||||||
|
),
|
||||||
|
extraProps: state.extra as PostEditorExtra?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/posts',
|
path: '/search',
|
||||||
name: 'explore',
|
name: 'postSearch',
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
builder: (context, state) => PostSearchScreen(
|
||||||
child: const ExploreScreen(),
|
initialTags: state.uri.queryParameters['tags']?.split(','),
|
||||||
),
|
initialCategories: state.uri.queryParameters['categories']?.split(','),
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: '/write/:mode',
|
|
||||||
name: 'postEditor',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: PostEditorScreen(
|
|
||||||
mode: state.pathParameters['mode']!,
|
|
||||||
postEditId: int.tryParse(
|
|
||||||
state.uri.queryParameters['editing'] ?? '',
|
|
||||||
),
|
|
||||||
postReplyId: int.tryParse(
|
|
||||||
state.uri.queryParameters['replying'] ?? '',
|
|
||||||
),
|
|
||||||
postRepostId: int.tryParse(
|
|
||||||
state.uri.queryParameters['reposting'] ?? '',
|
|
||||||
),
|
|
||||||
extraProps: state.extra as PostEditorExtraProps?,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/search',
|
|
||||||
name: 'postSearch',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: 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']!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/:slug',
|
|
||||||
name: 'postDetail',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: PostDetailScreen(
|
|
||||||
slug: state.pathParameters['slug']!,
|
|
||||||
preload: state.extra as SnPost?,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/account',
|
|
||||||
name: 'account',
|
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
|
||||||
child: const AccountScreen(),
|
|
||||||
),
|
|
||||||
routes: [],
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/chat',
|
|
||||||
name: 'chat',
|
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
|
||||||
child: const ChatScreen(),
|
|
||||||
),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: '/:scope/:alias',
|
|
||||||
name: 'chatRoom',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: ChatRoomScreen(
|
|
||||||
scope: state.pathParameters['scope']!,
|
|
||||||
alias: state.pathParameters['alias']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/:scope/:alias/call',
|
|
||||||
name: 'chatCallRoom',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: CallRoomScreen(
|
|
||||||
scope: state.pathParameters['scope']!,
|
|
||||||
alias: state.pathParameters['alias']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/:scope/:alias/detail',
|
|
||||||
name: 'channelDetail',
|
|
||||||
builder: (context, state) => AppBackground(
|
|
||||||
child: ChannelDetailScreen(
|
|
||||||
scope: state.pathParameters['scope']!,
|
|
||||||
alias: state.pathParameters['alias']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/manage',
|
|
||||||
name: 'chatManage',
|
|
||||||
pageBuilder: (context, state) => CustomTransitionPage(
|
|
||||||
child: 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(
|
|
||||||
child: const RealmScreen(),
|
|
||||||
),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: '/manage',
|
|
||||||
name: 'realmManage',
|
|
||||||
pageBuilder: (context, state) => CustomTransitionPage(
|
|
||||||
child: 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: '/album',
|
|
||||||
name: 'album',
|
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
|
||||||
child: const AlbumScreen(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/friend',
|
path: '/publishers/:name',
|
||||||
name: 'friend',
|
name: 'postPublisher',
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
|
||||||
child: const FriendScreen(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/notification',
|
path: '/:slug',
|
||||||
name: 'notification',
|
name: 'postDetail',
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
builder: (context, state) => PostDetailScreen(
|
||||||
child: const NotificationScreen(),
|
slug: state.pathParameters['slug']!,
|
||||||
|
preload: state.extra as SnPost?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ShellRoute(
|
GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
|
||||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
GoRoute(
|
||||||
|
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: UserScreen(name: state.pathParameters['name']!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
GoRoute(
|
||||||
|
path: '/chat',
|
||||||
|
name: 'chat',
|
||||||
|
builder: (context, state) => const ChatScreen(),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/auth/login',
|
path: '/:scope/:alias',
|
||||||
name: 'authLogin',
|
name: 'chatRoom',
|
||||||
builder: (context, state) => const AppBackground(
|
builder: (context, state) => ChatRoomScreen(
|
||||||
child: LoginScreen(),
|
scope: state.pathParameters['scope']!,
|
||||||
|
alias: state.pathParameters['alias']!,
|
||||||
|
extra: state.extra as ChatRoomScreenExtra?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/auth/register',
|
path: '/:scope/:alias/call',
|
||||||
name: 'authRegister',
|
name: 'chatCallRoom',
|
||||||
builder: (context, state) => const AppBackground(
|
builder: (context, state) => CallRoomScreen(
|
||||||
child: RegisterScreen(),
|
scope: state.pathParameters['scope']!,
|
||||||
|
alias: state.pathParameters['alias']!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/reports',
|
path: '/:scope/:alias/detail',
|
||||||
name: 'abuseReport',
|
name: 'channelDetail',
|
||||||
builder: (context, state) => const AppBackground(
|
builder: (context, state) => ChannelDetailScreen(
|
||||||
child: AbuseReportScreen(),
|
scope: state.pathParameters['scope']!,
|
||||||
|
alias: state.pathParameters['alias']!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/account/profile/edit',
|
path: '/manage',
|
||||||
name: 'accountProfileEdit',
|
name: 'chatManage',
|
||||||
builder: (context, state) => const AppBackground(
|
builder: (context, state) => ChatManageScreen(
|
||||||
child: ProfileEditScreen(),
|
editingChannelAlias: state.uri.queryParameters['editing'],
|
||||||
),
|
|
||||||
),
|
|
||||||
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(
|
GoRoute(
|
||||||
path: '/account/:name',
|
path: '/realm',
|
||||||
name: 'accountProfilePage',
|
name: 'realm',
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
pageBuilder: (context, state) => CustomTransitionPage(
|
||||||
child: UserScreen(name: state.pathParameters['name']!),
|
transitionsBuilder: _fadeThroughTransition,
|
||||||
|
child: const RealmScreen(),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
ShellRoute(
|
|
||||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/settings',
|
path: '/:alias',
|
||||||
name: 'settings',
|
name: 'realmDetail',
|
||||||
builder: (context, state) => const AppBackground(
|
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||||
child: SettingsScreen(),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/manage',
|
||||||
|
name: 'realmManage',
|
||||||
|
builder: (context, state) => RealmManageScreen(
|
||||||
|
editingRealmAlias: state.uri.queryParameters['editing'],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ShellRoute(
|
GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
|
||||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
GoRoute(
|
||||||
routes: [
|
path: '/:hash',
|
||||||
GoRoute(
|
name: 'newsDetail',
|
||||||
path: '/about',
|
builder: (context, state) => NewsDetailScreen(
|
||||||
name: 'about',
|
hash: state.pathParameters['hash']!,
|
||||||
builder: (context, state) => const AppBackground(
|
|
||||||
child: AboutScreen(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
]),
|
||||||
|
GoRoute(
|
||||||
|
path: '/album',
|
||||||
|
name: 'album',
|
||||||
|
builder: (context, state) => const AlbumScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/friend',
|
||||||
|
name: 'friend',
|
||||||
|
builder: (context, state) => const FriendScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/notification',
|
||||||
|
name: 'notification',
|
||||||
|
builder: (context, state) => const NotificationScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/auth/login',
|
||||||
|
name: 'authLogin',
|
||||||
|
builder: (context, state) => LoginScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/auth/register',
|
||||||
|
name: 'authRegister',
|
||||||
|
builder: (context, state) => RegisterScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/reports',
|
||||||
|
name: 'abuseReport',
|
||||||
|
builder: (context, state) => AbuseReportScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/settings',
|
||||||
|
name: 'settings',
|
||||||
|
builder: (context, state) => SettingsScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/about',
|
||||||
|
name: 'about',
|
||||||
|
builder: (context, state) => AboutScreen(),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
import '../types/account.dart';
|
import '../types/account.dart';
|
||||||
|
|
||||||
@@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('screenAbuseReport').tr(),
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
|||||||
else
|
else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
itemCount: _reports.length,
|
itemCount: _reports.length,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.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/account/account_image.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.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 {
|
class AccountScreen extends StatelessWidget {
|
||||||
const AccountScreen({super.key});
|
const AccountScreen({super.key});
|
||||||
@@ -19,11 +23,51 @@ class AccountScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
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: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.settings, fill: 1),
|
icon: const Icon(Symbols.settings, fill: 1),
|
||||||
@@ -82,16 +126,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}).padding(all: 20),
|
}).padding(all: 20),
|
||||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
).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(
|
ListTile(
|
||||||
title: Text('accountPublishers').tr(),
|
title: Text('accountPublishers').tr(),
|
||||||
subtitle: Text('accountPublishersSubtitle').tr(),
|
subtitle: Text('accountPublishersSubtitle').tr(),
|
||||||
@@ -112,6 +146,36 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
|||||||
GoRouter.of(context).pushNamed('abuseReport');
|
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(
|
ListTile(
|
||||||
title: Text('accountLogout').tr(),
|
title: Text('accountLogout').tr(),
|
||||||
subtitle: Text('accountLogoutSubtitle').tr(),
|
subtitle: Text('accountLogoutSubtitle').tr(),
|
||||||
@@ -133,33 +197,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
|||||||
await Hive.initFlutter();
|
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/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.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:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
class ProfileEditScreen extends StatefulWidget {
|
class ProfileEditScreen extends StatefulWidget {
|
||||||
@@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
onDateTimeChanged: (DateTime newDate) {
|
onDateTimeChanged: (DateTime newDate) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_birthday = newDate;
|
_birthday = newDate;
|
||||||
_birthdayController.text =
|
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
|
||||||
DateFormat(_kDateFormat).format(_birthday!);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
final ImageProvider imageProvider =
|
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||||
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
final aspectRatios =
|
||||||
final aspectRatios = place == 'banner'
|
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
|
||||||
? [CropAspectRatio(width: 16, height: 7)]
|
|
||||||
: [CropAspectRatio(width: 1, height: 1)];
|
|
||||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||||
? await showCupertinoImageCropper(
|
? await showCupertinoImageCropper(
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
@@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
final rawBytes =
|
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
||||||
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
|
||||||
.buffer
|
|
||||||
.asUint8List();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final attachment = await attach.directUploadOne(
|
final attachment = await attach.directUploadOne(
|
||||||
@@ -212,136 +207,141 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return AppScaffold(
|
||||||
child: Column(
|
appBar: AppBar(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
leading: const PageBackButton(),
|
||||||
children: [
|
title: Text('screenAccountProfileEdit').tr(),
|
||||||
LoadingIndicator(isActive: _isBusy),
|
),
|
||||||
const Gap(24),
|
body: SingleChildScrollView(
|
||||||
Stack(
|
child: Column(
|
||||||
clipBehavior: Clip.none,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Material(
|
LoadingIndicator(isActive: _isBusy),
|
||||||
elevation: 0,
|
const Gap(24),
|
||||||
child: InkWell(
|
Stack(
|
||||||
child: ClipRRect(
|
clipBehavior: Clip.none,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
children: [
|
||||||
child: AspectRatio(
|
Material(
|
||||||
aspectRatio: 16 / 9,
|
elevation: 0,
|
||||||
child: Container(
|
child: InkWell(
|
||||||
color:
|
child: ClipRRect(
|
||||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: _banner != null
|
child: AspectRatio(
|
||||||
? AutoResizeUniversalImage(
|
aspectRatio: 16 / 9,
|
||||||
sn.getAttachmentUrl(_banner!),
|
child: Container(
|
||||||
fit: BoxFit.cover,
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
)
|
child: _banner != null
|
||||||
: const SizedBox.shrink(),
|
? AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(_banner!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_updateImage('banner');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: -28,
|
|
||||||
left: 16,
|
|
||||||
child: Material(
|
|
||||||
elevation: 2,
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(40)),
|
|
||||||
child: InkWell(
|
|
||||||
child: AccountImage(content: _avatar, radius: 40),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_updateImage('avatar');
|
_updateImage('banner');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Positioned(
|
||||||
],
|
bottom: -28,
|
||||||
).padding(horizontal: padding),
|
left: 16,
|
||||||
const Gap(8 + 28),
|
child: Material(
|
||||||
Column(
|
elevation: 2,
|
||||||
children: [
|
borderRadius: const BorderRadius.all(Radius.circular(40)),
|
||||||
TextField(
|
child: InkWell(
|
||||||
readOnly: true,
|
child: AccountImage(content: _avatar, radius: 40),
|
||||||
controller: _usernameController,
|
onTap: () {
|
||||||
decoration: InputDecoration(
|
_updateImage('avatar');
|
||||||
border: const UnderlineInputBorder(),
|
},
|
||||||
labelText: 'fieldUsername'.tr(),
|
|
||||||
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
TextField(
|
|
||||||
controller: _nicknameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'fieldNickname'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: TextField(
|
|
||||||
controller: _firstNameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'fieldFirstName'.tr(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
),
|
||||||
Flexible(
|
],
|
||||||
flex: 1,
|
).padding(horizontal: padding),
|
||||||
child: TextField(
|
const Gap(8 + 28),
|
||||||
controller: _lastNameController,
|
Column(
|
||||||
decoration: InputDecoration(
|
children: [
|
||||||
border: const UnderlineInputBorder(),
|
TextField(
|
||||||
labelText: 'fieldLastName'.tr(),
|
readOnly: true,
|
||||||
|
controller: _usernameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldUsername'.tr(),
|
||||||
|
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
TextField(
|
||||||
|
controller: _nicknameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldNickname'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: TextField(
|
||||||
|
controller: _firstNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldFirstName'.tr(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: TextField(
|
||||||
|
controller: _lastNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldLastName'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
TextField(
|
||||||
|
controller: _descriptionController,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
maxLines: null,
|
||||||
|
minLines: 3,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldDescription'.tr(),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
TextField(
|
|
||||||
controller: _descriptionController,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
maxLines: null,
|
|
||||||
minLines: 3,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'fieldDescription'.tr(),
|
|
||||||
),
|
),
|
||||||
),
|
const Gap(4),
|
||||||
const Gap(4),
|
TextField(
|
||||||
TextField(
|
controller: _birthdayController,
|
||||||
controller: _birthdayController,
|
readOnly: true,
|
||||||
readOnly: true,
|
decoration: InputDecoration(
|
||||||
decoration: InputDecoration(
|
border: const UnderlineInputBorder(),
|
||||||
border: const UnderlineInputBorder(),
|
labelText: 'fieldBirthday'.tr(),
|
||||||
labelText: 'fieldBirthday'.tr(),
|
),
|
||||||
|
onTap: () => _selectBirthday(),
|
||||||
),
|
),
|
||||||
onTap: () => _selectBirthday(),
|
],
|
||||||
),
|
).padding(horizontal: padding + 8),
|
||||||
],
|
const Gap(12),
|
||||||
).padding(horizontal: padding + 8),
|
Row(
|
||||||
const Gap(12),
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
Row(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
ElevatedButton.icon(
|
||||||
children: [
|
onPressed: _isBusy ? null : _updateUserInfo,
|
||||||
ElevatedButton.icon(
|
icon: const Icon(Symbols.save),
|
||||||
onPressed: _isBusy ? null : _updateUserInfo,
|
label: Text('apply').tr(),
|
||||||
icon: const Icon(Symbols.save),
|
),
|
||||||
label: Text('apply').tr(),
|
],
|
||||||
),
|
).padding(horizontal: padding),
|
||||||
],
|
],
|
||||||
).padding(horizontal: padding),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
@@ -517,6 +518,12 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
future: _getCheckInRecords(),
|
future: _getCheckInRecords(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) return const SizedBox.shrink();
|
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!;
|
final records = snapshot.data!;
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -588,7 +595,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
subtitle: Text('@${ele.name}'),
|
subtitle: Text('@${ele.name}'),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).goNamed(
|
||||||
'postPublisher',
|
'postPublisher',
|
||||||
pathParameters: {'name': ele.name},
|
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/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.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:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
class AccountPublisherEditScreen extends StatefulWidget {
|
class AccountPublisherEditScreen extends StatefulWidget {
|
||||||
@@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
|
|||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
class AccountPublisherNewScreen extends StatefulWidget {
|
class AccountPublisherNewScreen extends StatefulWidget {
|
||||||
const AccountPublisherNewScreen({super.key});
|
const AccountPublisherNewScreen({super.key});
|
||||||
@@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('screenAccountPublisherNew').tr(),
|
||||||
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:surface/types/post.dart';
|
|||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
class PublisherScreen extends StatefulWidget {
|
class PublisherScreen extends StatefulWidget {
|
||||||
const PublisherScreen({super.key});
|
const PublisherScreen({super.key});
|
||||||
@@ -32,8 +33,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final resp = await sn.client.get('/cgi/co/publishers/me');
|
final resp = await sn.client.get('/cgi/co/publishers/me');
|
||||||
final List<SnPublisher> out = List<SnPublisher>.from(
|
final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
||||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
@@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('screenAccountPublishers').tr(),
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -62,9 +66,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Symbols.add_circle),
|
leading: const Icon(Symbols.add_circle),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context)
|
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
|
||||||
.pushNamed('accountPublisherNew')
|
|
||||||
.then((value) {
|
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_publishers.clear();
|
_publishers.clear();
|
||||||
_fetchPublishers();
|
_fetchPublishers();
|
||||||
@@ -75,48 +77,52 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
LoadingIndicator(isActive: _isBusy),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RefreshIndicator(
|
child: MediaQuery.removePadding(
|
||||||
onRefresh: () {
|
context: context,
|
||||||
_publishers.clear();
|
removeTop: true,
|
||||||
return _fetchPublishers();
|
child: RefreshIndicator(
|
||||||
},
|
onRefresh: () {
|
||||||
child: ListView.builder(
|
_publishers.clear();
|
||||||
itemCount: _publishers.length,
|
return _fetchPublishers();
|
||||||
itemBuilder: (context, idx) {
|
|
||||||
final publisher = _publishers[idx];
|
|
||||||
return ListTile(
|
|
||||||
title: Text(publisher.nick),
|
|
||||||
subtitle: Text('@${publisher.name}'),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
leading: AccountImage(content: publisher.avatar),
|
|
||||||
trailing: PopupMenuButton(
|
|
||||||
itemBuilder: (BuildContext context) => [
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.edit),
|
|
||||||
const Gap(16),
|
|
||||||
Text('edit').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'accountPublisherEdit',
|
|
||||||
pathParameters: {
|
|
||||||
'name': publisher.name,
|
|
||||||
},
|
|
||||||
).then((value) {
|
|
||||||
if (value == true) {
|
|
||||||
_publishers.clear();
|
|
||||||
_fetchPublishers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _publishers.length,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final publisher = _publishers[idx];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(publisher.nick),
|
||||||
|
subtitle: Text('@${publisher.name}'),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
leading: AccountImage(content: publisher.avatar),
|
||||||
|
trailing: PopupMenuButton(
|
||||||
|
itemBuilder: (BuildContext context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit),
|
||||||
|
const Gap(16),
|
||||||
|
Text('edit').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'accountPublisherEdit',
|
||||||
|
pathParameters: {
|
||||||
|
'name': publisher.name,
|
||||||
|
},
|
||||||
|
).then((value) {
|
||||||
|
if (value == true) {
|
||||||
|
_publishers.clear();
|
||||||
|
_fetchPublishers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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_zoom.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class AlbumScreen extends StatefulWidget {
|
class AlbumScreen extends StatefulWidget {
|
||||||
@@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
|
|||||||
@@ -7,17 +7,14 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/screens/account/factor_settings.dart';
|
||||||
import 'package:surface/types/auth.dart';
|
import 'package:surface/types/auth.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
import '../../providers/websocket.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 {
|
class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({super.key});
|
const LoginScreen({super.key});
|
||||||
|
|
||||||
@@ -35,67 +32,73 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Theme(
|
return AppScaffold(
|
||||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
appBar: AppBar(
|
||||||
child: SingleChildScrollView(
|
leading: const PageBackButton(),
|
||||||
child: PageTransitionSwitcher(
|
title: Text('screenAuthLogin').tr(),
|
||||||
transitionBuilder: (
|
),
|
||||||
Widget child,
|
body: Theme(
|
||||||
Animation<double> primaryAnimation,
|
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||||
Animation<double> secondaryAnimation,
|
child: SingleChildScrollView(
|
||||||
) {
|
child: PageTransitionSwitcher(
|
||||||
return SharedAxisTransition(
|
transitionBuilder: (
|
||||||
animation: primaryAnimation,
|
Widget child,
|
||||||
secondaryAnimation: secondaryAnimation,
|
Animation<double> primaryAnimation,
|
||||||
transitionType: SharedAxisTransitionType.horizontal,
|
Animation<double> secondaryAnimation,
|
||||||
child: Container(
|
) {
|
||||||
constraints: BoxConstraints(maxWidth: 380),
|
return SharedAxisTransition(
|
||||||
child: child,
|
animation: primaryAnimation,
|
||||||
),
|
secondaryAnimation: secondaryAnimation,
|
||||||
);
|
transitionType: SharedAxisTransitionType.horizontal,
|
||||||
},
|
child: Container(
|
||||||
child: switch (_period % 3) {
|
constraints: BoxConstraints(maxWidth: 380),
|
||||||
1 => _LoginPickerScreen(
|
child: child,
|
||||||
key: const ValueKey(1),
|
),
|
||||||
ticket: _currentTicket,
|
);
|
||||||
factors: _factors,
|
},
|
||||||
onTicket: (p0) => setState(() {
|
child: switch (_period % 3) {
|
||||||
_currentTicket = p0;
|
1 => _LoginPickerScreen(
|
||||||
}),
|
key: const ValueKey(1),
|
||||||
onPickFactor: (p0) => setState(() {
|
ticket: _currentTicket,
|
||||||
_factorPicked = p0;
|
factors: _factors,
|
||||||
}),
|
onTicket: (p0) => setState(() {
|
||||||
onNext: () => setState(() {
|
_currentTicket = p0;
|
||||||
_period++;
|
}),
|
||||||
}),
|
onPickFactor: (p0) => setState(() {
|
||||||
),
|
_factorPicked = p0;
|
||||||
2 => _LoginCheckScreen(
|
}),
|
||||||
key: const ValueKey(2),
|
onNext: () => setState(() {
|
||||||
ticket: _currentTicket,
|
_period++;
|
||||||
factor: _factorPicked,
|
}),
|
||||||
onTicket: (p0) => setState(() {
|
),
|
||||||
_currentTicket = p0;
|
2 => _LoginCheckScreen(
|
||||||
}),
|
key: const ValueKey(2),
|
||||||
onNext: () => setState(() {
|
ticket: _currentTicket,
|
||||||
_period = 1;
|
factor: _factorPicked,
|
||||||
}),
|
onTicket: (p0) => setState(() {
|
||||||
),
|
_currentTicket = p0;
|
||||||
_ => _LoginLookupScreen(
|
}),
|
||||||
key: const ValueKey(0),
|
onNext: () => setState(() {
|
||||||
ticket: _currentTicket,
|
_period = 1;
|
||||||
onTicket: (p0) => setState(() {
|
}),
|
||||||
_currentTicket = p0;
|
),
|
||||||
}),
|
_ => _LoginLookupScreen(
|
||||||
onFactor: (p0) => setState(() {
|
key: const ValueKey(0),
|
||||||
_factors = p0;
|
ticket: _currentTicket,
|
||||||
}),
|
onTicket: (p0) => setState(() {
|
||||||
onNext: () => setState(() {
|
_currentTicket = p0;
|
||||||
_period++;
|
}),
|
||||||
}),
|
onFactor: (p0) => setState(() {
|
||||||
),
|
_factors = p0;
|
||||||
},
|
}),
|
||||||
).padding(all: 24),
|
onNext: () => setState(() {
|
||||||
).center(),
|
_period++;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
).padding(all: 24),
|
||||||
|
).center(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,7 +208,9 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
|
|||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
autofillHints: [
|
autofillHints: [
|
||||||
(_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode
|
widget.factor!.type == 0
|
||||||
|
? AutofillHints.password
|
||||||
|
: AutofillHints.oneTimeCode
|
||||||
],
|
],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
@@ -260,7 +265,8 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
|||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
int? _factorPicked;
|
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 {
|
void _performGetFactorCode() async {
|
||||||
if (_factorPicked == null) return;
|
if (_factorPicked == null) return;
|
||||||
@@ -321,11 +327,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
secondary: Icon(
|
secondary: Icon(
|
||||||
_factorLabelMap[x.type]?.$2 ?? Symbols.question_mark,
|
kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
|
kFactorTypes[x.type]?.$1 ?? 'unknown',
|
||||||
),
|
).tr(),
|
||||||
enabled: !widget.ticket!.factorTrail.contains(x.id),
|
enabled: !widget.ticket!.factorTrail.contains(x.id),
|
||||||
value: _factorPicked == x.id,
|
value: _factorPicked == x.id,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -401,11 +407,14 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
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: {
|
await sn.client.post('/cgi/id/users/me/password-reset', data: {
|
||||||
'user_id': lookupResp.data['id'],
|
'user_id': lookupResp.data['id'],
|
||||||
});
|
});
|
||||||
if (mounted) context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
if (mounted) {
|
||||||
|
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (mounted) context.showErrorDialog(err);
|
if (mounted) context.showErrorDialog(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -430,7 +439,8 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
|||||||
widget.onTicket(result.ticket);
|
widget.onTicket(result.ticket);
|
||||||
|
|
||||||
// Pull factors
|
// 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(),
|
'ticketId': result.ticket!.id.toString(),
|
||||||
});
|
});
|
||||||
widget.onFactor(
|
widget.onFactor(
|
||||||
@@ -441,7 +451,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
|||||||
|
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if(mounted) context.showErrorDialog(err);
|
if (mounted) context.showErrorDialog(err);
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
@@ -524,7 +534,10 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
|||||||
'termAcceptNextWithAgree'.tr(),
|
'termAcceptNextWithAgree'.tr(),
|
||||||
textAlign: TextAlign.end,
|
textAlign: TextAlign.end,
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
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(
|
Material(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class RegisterScreen extends StatefulWidget {
|
class RegisterScreen extends StatefulWidget {
|
||||||
@@ -43,6 +44,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
'nick': nickname,
|
'nick': nickname,
|
||||||
'email': email,
|
'email': email,
|
||||||
'password': password,
|
'password': password,
|
||||||
|
'language': EasyLocalization.of(context)!.currentLocale.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@@ -54,175 +56,178 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return StyledWidget(Container(
|
return AppScaffold(
|
||||||
constraints: const BoxConstraints(maxWidth: 380),
|
appBar: AppBar(
|
||||||
child: SingleChildScrollView(
|
leading: const PageBackButton(),
|
||||||
child: Column(
|
title: Text('screenAuthRegister').tr(),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: [
|
body: StyledWidget(Container(
|
||||||
Align(
|
constraints: const BoxConstraints(maxWidth: 380),
|
||||||
alignment: Alignment.centerLeft,
|
child: SingleChildScrollView(
|
||||||
child: CircleAvatar(
|
child: Column(
|
||||||
radius: 26,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: const Icon(
|
children: [
|
||||||
Symbols.person_add,
|
Align(
|
||||||
size: 28,
|
alignment: Alignment.centerLeft,
|
||||||
),
|
child: CircleAvatar(
|
||||||
).padding(bottom: 8),
|
radius: 26,
|
||||||
),
|
child: const Icon(
|
||||||
Text(
|
Symbols.person_add,
|
||||||
'screenAuthRegister',
|
size: 28,
|
||||||
style: const TextStyle(
|
),
|
||||||
fontSize: 28,
|
).padding(bottom: 8),
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
),
|
),
|
||||||
).tr().padding(left: 4, bottom: 16),
|
Text(
|
||||||
Form(
|
'screenAuthRegister',
|
||||||
key: _formKey,
|
style: const TextStyle(
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
fontSize: 28,
|
||||||
child: Column(
|
fontWeight: FontWeight.w900,
|
||||||
children: [
|
),
|
||||||
TextFormField(
|
).tr().padding(left: 4, bottom: 16),
|
||||||
validator: (value) {
|
Form(
|
||||||
if (value == null || value.length < 4 || value.length > 32) {
|
key: _formKey,
|
||||||
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
}
|
child: Column(
|
||||||
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
children: [
|
||||||
return 'fieldUsernameAlphanumOnly'.tr();
|
TextFormField(
|
||||||
}
|
validator: (value) {
|
||||||
return null;
|
if (value == null || value.length < 4 || value.length > 32) {
|
||||||
},
|
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
||||||
autocorrect: false,
|
}
|
||||||
enableSuggestions: false,
|
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
||||||
controller: _usernameController,
|
return 'fieldUsernameAlphanumOnly'.tr();
|
||||||
autofillHints: const [AutofillHints.username],
|
}
|
||||||
decoration: InputDecoration(
|
return null;
|
||||||
isDense: true,
|
},
|
||||||
border: const UnderlineInputBorder(),
|
autocorrect: false,
|
||||||
labelText: 'fieldUsername'.tr(),
|
enableSuggestions: false,
|
||||||
),
|
controller: _usernameController,
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
autofillHints: const [AutofillHints.username],
|
||||||
),
|
decoration: InputDecoration(
|
||||||
const Gap(12),
|
isDense: true,
|
||||||
TextFormField(
|
border: const UnderlineInputBorder(),
|
||||||
validator: (value) {
|
labelText: 'fieldUsername'.tr(),
|
||||||
if (value == null || value.length < 4 || value.length > 32) {
|
|
||||||
return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
controller: _nicknameController,
|
|
||||||
autofillHints: const [AutofillHints.nickname],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'fieldNickname'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
TextFormField(
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'fieldCannotBeEmpty'.tr();
|
|
||||||
}
|
|
||||||
if (!EmailValidator.validate(value)) {
|
|
||||||
return 'fieldEmailAddressMustBeValid'.tr();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
controller: _emailController,
|
|
||||||
autofillHints: const [AutofillHints.email],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'fieldEmail'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
TextFormField(
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'fieldCannotBeEmpty'.tr();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
obscureText: true,
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autofillHints: const [AutofillHints.password],
|
|
||||||
controller: _passwordController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const UnderlineInputBorder(),
|
|
||||||
labelText: 'fieldPassword'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 7),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: StyledWidget(
|
|
||||||
Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 290),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'termAcceptNextWithAgree'.tr(),
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurface
|
|
||||||
.withAlpha((255 * 0.75).round()),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Material(
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
color: Colors.transparent,
|
),
|
||||||
child: InkWell(
|
const Gap(12),
|
||||||
child: Row(
|
TextFormField(
|
||||||
mainAxisSize: MainAxisSize.min,
|
validator: (value) {
|
||||||
children: [
|
if (value == null || value.length < 4 || value.length > 32) {
|
||||||
Text('termAcceptLink'.tr()),
|
return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
||||||
const Gap(4),
|
}
|
||||||
const Icon(Symbols.launch, size: 14),
|
return null;
|
||||||
],
|
},
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: _nicknameController,
|
||||||
|
autofillHints: const [AutofillHints.nickname],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldNickname'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
TextFormField(
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'fieldCannotBeEmpty'.tr();
|
||||||
|
}
|
||||||
|
if (!EmailValidator.validate(value)) {
|
||||||
|
return 'fieldEmailAddressMustBeValid'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: _emailController,
|
||||||
|
autofillHints: const [AutofillHints.email],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldEmail'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
TextFormField(
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'fieldCannotBeEmpty'.tr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
obscureText: true,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
controller: _passwordController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldPassword'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 7),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: StyledWidget(
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 290),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'termAcceptNextWithAgree'.tr(),
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('termAcceptLink'.tr()),
|
||||||
|
const Gap(4),
|
||||||
|
const Icon(Symbols.launch, size: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString('https://solsynth.dev/terms');
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
launchUrlString('https://solsynth.dev/terms');
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => _performAction(context),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('next').tr(),
|
||||||
|
const Icon(Symbols.chevron_right),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).padding(horizontal: 16),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () => _performAction(context),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('next').tr(),
|
|
||||||
const Icon(Symbols.chevron_right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
)).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/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.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/unauthorized_hint.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenChat').tr(),
|
title: Text('screenChat').tr(),
|
||||||
@@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenChat').tr(),
|
title: Text('screenChat').tr(),
|
||||||
@@ -195,22 +196,58 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
children: [
|
children: [
|
||||||
LoadingIndicator(isActive: _isBusy),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RefreshIndicator(
|
child: MediaQuery.removePadding(
|
||||||
onRefresh: () => Future.sync(() => _refreshChannels()),
|
context: context,
|
||||||
child: ListView.builder(
|
removeTop: true,
|
||||||
itemCount: _channels?.length ?? 0,
|
child: RefreshIndicator(
|
||||||
itemBuilder: (context, idx) {
|
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||||
final channel = _channels![idx];
|
child: ListView.builder(
|
||||||
final lastMessage = _lastMessages?[channel.id];
|
itemCount: _channels?.length ?? 0,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final channel = _channels![idx];
|
||||||
|
final lastMessage = _lastMessages?[channel.id];
|
||||||
|
|
||||||
if (channel.type == 1) {
|
if (channel.type == 1) {
|
||||||
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||||
(ele) => ele?.accountId != ua.user?.id,
|
(ele) => ele?.accountId != ua.user?.id,
|
||||||
orElse: () => null,
|
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(
|
return ListTile(
|
||||||
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
title: Text(channel.name),
|
||||||
subtitle: lastMessage != null
|
subtitle: lastMessage != null
|
||||||
? Text(
|
? Text(
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
@@ -218,15 +255,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
'channelDirectMessageDescription'.tr(args: [
|
channel.description,
|
||||||
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
|
||||||
]),
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
content: null,
|
||||||
|
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
@@ -240,39 +276,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
),
|
||||||
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: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'chatRoom',
|
|
||||||
pathParameters: {
|
|
||||||
'scope': channel.realm?.alias ?? 'global',
|
|
||||||
'alias': channel.alias,
|
|
||||||
},
|
|
||||||
).then((value) {
|
|
||||||
if (value == true) _refreshChannels();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/providers/chat_call.dart';
|
import 'package:surface/providers/chat_call.dart';
|
||||||
import 'package:surface/widgets/chat/call/call_controls.dart';
|
import 'package:surface/widgets/chat/call/call_controls.dart';
|
||||||
import 'package:surface/widgets/chat/call/call_participant.dart';
|
import 'package:surface/widgets/chat/call/call_participant.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
class CallRoomScreen extends StatefulWidget {
|
class CallRoomScreen extends StatefulWidget {
|
||||||
final String scope;
|
final String scope;
|
||||||
final String alias;
|
final String alias;
|
||||||
|
|
||||||
const CallRoomScreen({super.key, required this.scope, required this.alias});
|
const CallRoomScreen({super.key, required this.scope, required this.alias});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -35,8 +37,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
color:
|
color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
||||||
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
|
||||||
child: call.focusTrack != null
|
child: call.focusTrack != null
|
||||||
? InteractiveParticipantWidget(
|
? InteractiveParticipantWidget(
|
||||||
isFixedAvatar: false,
|
isFixedAvatar: false,
|
||||||
@@ -71,8 +72,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
color: Theme.of(context).cardColor,
|
color: Theme.of(context).cardColor,
|
||||||
participant: track,
|
participant: track,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (track.participant.sid !=
|
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
||||||
call.focusTrack?.participant.sid) {
|
|
||||||
call.setFocusTrack(track);
|
call.setFocusTrack(track);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -114,14 +114,10 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: InteractiveParticipantWidget(
|
child: InteractiveParticipantWidget(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerHigh
|
|
||||||
.withOpacity(0.75),
|
|
||||||
participant: track,
|
participant: track,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (track.participant.sid !=
|
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
||||||
call.focusTrack?.participant.sid) {
|
|
||||||
call.setFocusTrack(track);
|
call.setFocusTrack(track);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -152,157 +148,134 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: call,
|
listenable: call,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: RichText(
|
title: RichText(
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
text: TextSpan(children: [
|
text: TextSpan(children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'call'.tr(),
|
text: 'call'.tr(),
|
||||||
style: Theme.of(context)
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
|
||||||
.textTheme
|
|
||||||
.titleLarge!
|
|
||||||
.copyWith(color: Colors.white),
|
|
||||||
),
|
),
|
||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: call.lastDuration.toString(),
|
text: call.lastDuration.toString(),
|
||||||
style: Theme.of(context)
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
|
||||||
.textTheme
|
|
||||||
.bodySmall!
|
|
||||||
.copyWith(color: Colors.white),
|
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: GestureDetector(
|
||||||
child: GestureDetector(
|
behavior: HitTestBehavior.translucent,
|
||||||
behavior: HitTestBehavior.translucent,
|
child: Column(
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
height: 64,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Builder(builder: (context) {
|
||||||
|
final call = context.read<ChatCallProvider>();
|
||||||
|
final connectionQuality =
|
||||||
|
call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown;
|
||||||
|
return Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
call.channel?.name ?? 'unknown'.tr(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
Text(call.lastDuration.toString())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
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,
|
||||||
|
}[connectionQuality],
|
||||||
|
color: {
|
||||||
|
livekit.ConnectionQuality.excellent: Colors.green,
|
||||||
|
livekit.ConnectionQuality.good: Colors.orange,
|
||||||
|
livekit.ConnectionQuality.poor: Colors.red,
|
||||||
|
}[connectionQuality],
|
||||||
|
size: 16,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
).padding(all: 3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view),
|
||||||
|
onPressed: () {
|
||||||
|
_switchLayout();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(left: 20, right: 16),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Material(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
switch (_layoutMode) {
|
||||||
|
case 1:
|
||||||
|
return _buildGridLayout();
|
||||||
|
default:
|
||||||
|
return _buildListLayout();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (call.room.localParticipant != null)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
height: 64,
|
child: ControlsWidget(
|
||||||
child: Row(
|
call.room,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
call.room.localParticipant!,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Builder(builder: (context) {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
final connectionQuality =
|
|
||||||
call.room.localParticipant?.connectionQuality ??
|
|
||||||
livekit.ConnectionQuality.unknown;
|
|
||||||
return Expanded(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
call.channel?.name ?? 'unknown'.tr(),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(6),
|
|
||||||
Text(call.lastDuration.toString())
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
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,
|
|
||||||
}[connectionQuality],
|
|
||||||
color: {
|
|
||||||
livekit.ConnectionQuality.excellent:
|
|
||||||
Colors.green,
|
|
||||||
livekit.ConnectionQuality.good:
|
|
||||||
Colors.orange,
|
|
||||||
livekit.ConnectionQuality.poor:
|
|
||||||
Colors.red,
|
|
||||||
}[connectionQuality],
|
|
||||||
size: 16,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const SizedBox(
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: Colors.white,
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
).padding(all: 3),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: _layoutMode == 0
|
|
||||||
? const Icon(Icons.view_list)
|
|
||||||
: const Icon(Icons.grid_view),
|
|
||||||
onPressed: () {
|
|
||||||
_switchLayout();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(left: 20, right: 16),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Material(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
switch (_layoutMode) {
|
|
||||||
case 1:
|
|
||||||
return _buildGridLayout();
|
|
||||||
default:
|
|
||||||
return _buildListLayout();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (call.room.localParticipant != null)
|
],
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
child: ControlsWidget(
|
|
||||||
call.room,
|
|
||||||
call.room.localParticipant!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {},
|
|
||||||
),
|
),
|
||||||
|
onTap: () {},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,15 +10,19 @@ import 'package:surface/providers/channel.dart';
|
|||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/types/account.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/account/account_select.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.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 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class ChannelDetailScreen extends StatefulWidget {
|
class ChannelDetailScreen extends StatefulWidget {
|
||||||
final String scope;
|
final String scope;
|
||||||
final String alias;
|
final String alias;
|
||||||
|
|
||||||
const ChannelDetailScreen({
|
const ChannelDetailScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.scope,
|
required this.scope,
|
||||||
@@ -54,8 +58,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client
|
final resp = await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/members/me');
|
||||||
.get('/cgi/im/channels/${_channel!.keyPath}/members/me');
|
|
||||||
_profile = SnChannelMember.fromJson(resp.data);
|
_profile = SnChannelMember.fromJson(resp.data);
|
||||||
_notifyLevel = _profile!.notify;
|
_notifyLevel = _profile!.notify;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -142,6 +145,25 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _addMember(SnAccount related) async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.post(
|
||||||
|
'/cgi/im/channels/${_channel!.keyPath}/members',
|
||||||
|
data: {'related': related.name},
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('channelMemberAdded'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _showChannelProfileDetail() {
|
void _showChannelProfileDetail() {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -165,13 +187,16 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showMemberAdd() {
|
void _showMemberAdd() async {
|
||||||
showModalBottomSheet(
|
final user = await showModalBottomSheet<SnAccount?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _NewChannelMemberWidget(
|
builder: (context) => AccountSelect(
|
||||||
channel: _channel!,
|
title: 'channelMemberAdd'.tr(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (user == null) return;
|
||||||
|
_addMember(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -189,7 +214,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
|
|
||||||
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
|
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
|
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
|
||||||
),
|
),
|
||||||
@@ -220,11 +245,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('channelDetailPersonalRegion')
|
Text('channelDetailPersonalRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||||
.bold()
|
|
||||||
.fontSize(17)
|
|
||||||
.tr()
|
|
||||||
.padding(horizontal: 20, bottom: 4),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.notifications),
|
leading: const Icon(Symbols.notifications),
|
||||||
trailing: DropdownButtonHideUnderline(
|
trailing: DropdownButtonHideUnderline(
|
||||||
@@ -263,8 +284,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content:
|
content: ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
||||||
ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
|
||||||
radius: 18,
|
radius: 18,
|
||||||
),
|
),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
@@ -283,8 +303,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
title: Text('channelActionLeave').tr(),
|
title: Text('channelActionLeave').tr(),
|
||||||
subtitle: Text('channelActionLeaveDescription').tr(),
|
subtitle: Text('channelActionLeaveDescription').tr(),
|
||||||
contentPadding:
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
onTap: _leaveChannel,
|
onTap: _leaveChannel,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -292,11 +311,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('channelDetailMemberRegion')
|
Text('channelDetailMemberRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||||
.bold()
|
|
||||||
.fontSize(17)
|
|
||||||
.tr()
|
|
||||||
.padding(horizontal: 20, bottom: 4),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.group),
|
leading: const Icon(Symbols.group),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
@@ -318,11 +333,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('channelDetailAdminRegion')
|
Text('channelDetailAdminRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||||
.bold()
|
|
||||||
.fontSize(17)
|
|
||||||
.tr()
|
|
||||||
.padding(horizontal: 20, bottom: 4),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.edit),
|
leading: const Icon(Symbols.edit),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
@@ -361,18 +372,17 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
class _ChannelProfileDetailDialog extends StatefulWidget {
|
class _ChannelProfileDetailDialog extends StatefulWidget {
|
||||||
final SnChannel channel;
|
final SnChannel channel;
|
||||||
final SnChannelMember current;
|
final SnChannelMember current;
|
||||||
|
|
||||||
const _ChannelProfileDetailDialog({
|
const _ChannelProfileDetailDialog({
|
||||||
required this.channel,
|
required this.channel,
|
||||||
required this.current,
|
required this.current,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_ChannelProfileDetailDialog> createState() =>
|
State<_ChannelProfileDetailDialog> createState() => _ChannelProfileDetailDialogState();
|
||||||
_ChannelProfileDetailDialogState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelProfileDetailDialogState
|
class _ChannelProfileDetailDialogState extends State<_ChannelProfileDetailDialog> {
|
||||||
extends State<_ChannelProfileDetailDialog> {
|
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
|
|
||||||
final TextEditingController _nickController = TextEditingController();
|
final TextEditingController _nickController = TextEditingController();
|
||||||
@@ -443,11 +453,11 @@ class _ChannelProfileDetailDialogState
|
|||||||
|
|
||||||
class _ChannelMemberListWidget extends StatefulWidget {
|
class _ChannelMemberListWidget extends StatefulWidget {
|
||||||
final SnChannel channel;
|
final SnChannel channel;
|
||||||
|
|
||||||
const _ChannelMemberListWidget({required this.channel});
|
const _ChannelMemberListWidget({required this.channel});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_ChannelMemberListWidget> createState() =>
|
State<_ChannelMemberListWidget> createState() => _ChannelMemberListWidgetState();
|
||||||
_ChannelMemberListWidgetState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||||
@@ -462,12 +472,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
try {
|
try {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get(
|
final resp = await sn.client.get('/cgi/im/channels/${widget.channel.keyPath}/members', queryParameters: {
|
||||||
'/cgi/im/channels/${widget.channel.keyPath}/members',
|
'take': 10,
|
||||||
queryParameters: {
|
'offset': 0,
|
||||||
'take': 10,
|
});
|
||||||
'offset': 0,
|
|
||||||
});
|
|
||||||
final out = List<SnChannelMember>.from(
|
final out = List<SnChannelMember>.from(
|
||||||
resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
|
resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
@@ -525,9 +533,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.group, size: 24),
|
const Icon(Symbols.group, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('channelMemberManage')
|
Text('channelMemberManage').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
.tr()
|
|
||||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -538,8 +544,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
},
|
},
|
||||||
child: InfiniteList(
|
child: InfiniteList(
|
||||||
itemCount: _members.length,
|
itemCount: _members.length,
|
||||||
hasReachedMax:
|
hasReachedMax: _totalCount != null && _members.length >= _totalCount!,
|
||||||
_totalCount != null && _members.length >= _totalCount!,
|
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
onFetchData: _fetchMembers,
|
onFetchData: _fetchMembers,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -550,8 +555,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
ud.getAccountFromCache(member.accountId)?.name ??
|
ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
||||||
'unknown'.tr(),
|
|
||||||
),
|
),
|
||||||
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
@@ -561,8 +565,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed:
|
onPressed: _isUpdating ? null : () => _deleteMember(member),
|
||||||
_isUpdating ? null : () => _deleteMember(member),
|
|
||||||
icon: const Icon(Symbols.person_remove),
|
icon: const Icon(Symbols.person_remove),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -577,83 +580,3 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NewChannelMemberWidget extends StatefulWidget {
|
|
||||||
final SnChannel channel;
|
|
||||||
const _NewChannelMemberWidget({required this.channel});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_NewChannelMemberWidget> createState() =>
|
|
||||||
_NewChannelMemberWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NewChannelMemberWidgetState extends State<_NewChannelMemberWidget> {
|
|
||||||
bool _isBusy = false;
|
|
||||||
|
|
||||||
final TextEditingController _relatedController = TextEditingController();
|
|
||||||
|
|
||||||
Future<void> _performAction() async {
|
|
||||||
if (_relatedController.text.isEmpty) return;
|
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client.post(
|
|
||||||
'/cgi/im/channels/${widget.channel.keyPath}/members',
|
|
||||||
data: {
|
|
||||||
'related': _relatedController.text,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!mounted) return;
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
context.showSnackbar('channelMemberAdded'.tr());
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isBusy = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_relatedController.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return StyledWidget(Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'channelMemberAdd',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
).tr(),
|
|
||||||
const Gap(12),
|
|
||||||
TextField(
|
|
||||||
controller: _relatedController,
|
|
||||||
readOnly: _isBusy,
|
|
||||||
autocorrect: false,
|
|
||||||
autofocus: true,
|
|
||||||
textCapitalization: TextCapitalization.none,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'fieldMemberRelatedName'.tr(),
|
|
||||||
suffix: SizedBox(
|
|
||||||
height: 24,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: _isBusy ? null : () => _performAction(),
|
|
||||||
icon: Icon(Symbols.send),
|
|
||||||
visualDensity:
|
|
||||||
const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)).padding(all: 24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:surface/types/realm.dart';
|
|||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class ChatManageScreen extends StatefulWidget {
|
class ChatManageScreen extends StatefulWidget {
|
||||||
@@ -87,7 +88,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
try {
|
try {
|
||||||
final resp = await sn.client.request(
|
final resp = await sn.client.request(
|
||||||
widget.editingChannelAlias != null
|
widget.editingChannelAlias != null
|
||||||
? '/cgi/im/channels/$scope/${widget.editingChannelAlias}'
|
? '/cgi/im/channels/$scope/${_editingChannel!.id}'
|
||||||
: '/cgi/im/channels/$scope',
|
: '/cgi/im/channels/$scope',
|
||||||
data: payload,
|
data: payload,
|
||||||
options: Options(
|
options: Options(
|
||||||
@@ -121,7 +122,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: widget.editingChannelAlias != null
|
title: widget.editingChannelAlias != null
|
||||||
? Text('screenChatManage').tr()
|
? Text('screenChatManage').tr()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:easy_localization/easy_localization.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:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/chat_message_controller.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/channel.dart';
|
||||||
import 'package:surface/providers/chat_call.dart';
|
import 'package:surface/providers/chat_call.dart';
|
||||||
import 'package:surface/providers/sn_network.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/providers/websocket.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/chat/call/call_prejoin.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.dart';
|
||||||
import 'package:surface/widgets/chat/chat_message_input.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/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.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 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
import '../../providers/user_directory.dart';
|
class ChatRoomScreenExtra {
|
||||||
import '../../providers/userinfo.dart';
|
final String? initialText;
|
||||||
|
final List<PostWriteMedia>? initialAttachments;
|
||||||
|
|
||||||
|
ChatRoomScreenExtra({this.initialText, this.initialAttachments});
|
||||||
|
}
|
||||||
|
|
||||||
class ChatRoomScreen extends StatefulWidget {
|
class ChatRoomScreen extends StatefulWidget {
|
||||||
final String scope;
|
final String scope;
|
||||||
final String alias;
|
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
|
@override
|
||||||
State<ChatRoomScreen> createState() => _ChatRoomScreenState();
|
State<ChatRoomScreen> createState() => _ChatRoomScreenState();
|
||||||
@@ -175,8 +186,23 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
_messageController = ChatMessageController(context);
|
_messageController = ChatMessageController(context);
|
||||||
_fetchChannel().then((_) async {
|
_fetchChannel().then((_) async {
|
||||||
await _messageController.initialize(_channel!);
|
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>();
|
final ws = context.read<WebSocketProvider>();
|
||||||
@@ -210,7 +236,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
final call = context.watch<ChatCallProvider>();
|
final call = context.watch<ChatCallProvider>();
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
_channel?.type == 1
|
_channel?.type == 1
|
||||||
@@ -280,11 +306,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: InfiniteList(
|
child: InfiniteList(
|
||||||
reverse: true,
|
reverse: true,
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(top: 12),
|
||||||
left: 12,
|
|
||||||
right: 12,
|
|
||||||
top: 12,
|
|
||||||
),
|
|
||||||
hasReachedMax: _messageController.isAllLoaded,
|
hasReachedMax: _messageController.isAllLoaded,
|
||||||
itemCount: _messageController.messages.length,
|
itemCount: _messageController.messages.length,
|
||||||
isLoading: _messageController.isLoading,
|
isLoading: _messageController.isLoading,
|
||||||
@@ -310,23 +332,20 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Container(
|
child: ChatMessage(
|
||||||
constraints: BoxConstraints(maxWidth: 480),
|
data: message,
|
||||||
child: ChatMessage(
|
isMerged: canMerge,
|
||||||
data: message,
|
hasMerged: canMergePrevious,
|
||||||
isMerged: canMerge,
|
isPending: _messageController.unconfirmedMessages.contains(message.uuid),
|
||||||
hasMerged: canMergePrevious,
|
onReply: (value) {
|
||||||
isPending: _messageController.unconfirmedMessages.contains(message.uuid),
|
_inputGlobalKey.currentState?.setReply(value);
|
||||||
onReply: (value) {
|
},
|
||||||
_inputGlobalKey.currentState?.setReply(value);
|
onEdit: (value) {
|
||||||
},
|
_inputGlobalKey.currentState?.setEdit(value);
|
||||||
onEdit: (value) {
|
},
|
||||||
_inputGlobalKey.currentState?.setEdit(value);
|
onDelete: (value) {
|
||||||
},
|
_inputGlobalKey.currentState?.deleteMessage(value);
|
||||||
onDelete: (value) {
|
},
|
||||||
_inputGlobalKey.currentState?.deleteMessage(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -335,11 +354,17 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
if (!_messageController.isPending)
|
if (!_messageController.isPending)
|
||||||
Material(
|
Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
child: ChatMessageInput(
|
child: Column(
|
||||||
key: _inputGlobalKey,
|
children: [
|
||||||
otherMember: _otherMember,
|
ChatTypingIndicator(controller: _messageController),
|
||||||
controller: _messageController,
|
ChatMessageInput(
|
||||||
).padding(bottom: MediaQuery.of(context).padding.bottom),
|
key: _inputGlobalKey,
|
||||||
|
otherMember: _otherMember,
|
||||||
|
controller: _messageController,
|
||||||
|
),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:animations/animations.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.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:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/providers/sn_network.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/types/post.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.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_item.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
@@ -93,7 +97,9 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
floatingActionButtonLocation: ExpandableFab.location,
|
floatingActionButtonLocation: ExpandableFab.location,
|
||||||
floatingActionButton: ExpandableFab(
|
floatingActionButton: ExpandableFab(
|
||||||
key: _fabKey,
|
key: _fabKey,
|
||||||
@@ -210,6 +216,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SliverGap(12),
|
||||||
SliverInfiniteList(
|
SliverInfiniteList(
|
||||||
itemCount: _posts.length,
|
itemCount: _posts.length,
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
@@ -217,27 +224,39 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
||||||
onFetchData: _fetchPosts,
|
onFetchData: _fetchPosts,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
return GestureDetector(
|
return Center(
|
||||||
child: PostItem(
|
child: OpenContainer(
|
||||||
data: _posts[idx],
|
closedBuilder: (_, __) => Container(
|
||||||
maxWidth: 640,
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
onChanged: (data) {
|
child: PostItem(
|
||||||
setState(() => _posts[idx] = data);
|
data: _posts[idx],
|
||||||
},
|
maxWidth: 640,
|
||||||
onDeleted: () {
|
onChanged: (data) {
|
||||||
_refreshPosts();
|
setState(() => _posts[idx] = data);
|
||||||
},
|
},
|
||||||
|
onDeleted: () {
|
||||||
|
_refreshPosts();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'postDetail',
|
|
||||||
pathParameters: {'slug': _posts[idx].id.toString()},
|
|
||||||
extra: _posts[idx],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
separatorBuilder: (_, __) => const Gap(8),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,14 +6,15 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/relationship.dart';
|
import 'package:surface/providers/relationship.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/account/account_select.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import '../providers/userinfo.dart';
|
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||||
import '../widgets/unauthorized_hint.dart';
|
|
||||||
|
|
||||||
const kFriendStatus = {
|
const kFriendStatus = {
|
||||||
0: 'friendStatusPending',
|
0: 'friendStatusPending',
|
||||||
@@ -167,6 +168,24 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _sendRequest(SnAccount user) async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.post('/cgi/id/users/me/relations', data: {
|
||||||
|
'related': user.name,
|
||||||
|
});
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('friendRequestSent'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -180,7 +199,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenFriend').tr(),
|
title: Text('screenFriend').tr(),
|
||||||
@@ -191,18 +210,23 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenFriend').tr(),
|
title: Text('screenFriend').tr(),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Icon(Symbols.add),
|
child: const Icon(Symbols.add),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
showModalBottomSheet(
|
final user = await showModalBottomSheet<SnAccount?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _NewFriendWidget(),
|
builder: (context) => AccountSelect(
|
||||||
|
title: 'friendNew'.tr(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (user == null) return;
|
||||||
|
_sendRequest(user);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
@@ -230,55 +254,54 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: _showBlocks,
|
onTap: _showBlocks,
|
||||||
),
|
),
|
||||||
if (_requests.isNotEmpty || _blocks.isNotEmpty)
|
if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1),
|
||||||
const Divider(height: 1),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RefreshIndicator(
|
child: MediaQuery.removePadding(
|
||||||
onRefresh: () => Future.wait([
|
context: context,
|
||||||
_fetchRelations(),
|
removeTop: true,
|
||||||
_fetchRequests(),
|
child: RefreshIndicator(
|
||||||
]),
|
onRefresh: () => Future.wait([
|
||||||
child: ListView.builder(
|
_fetchRelations(),
|
||||||
itemCount: _relations.length,
|
_fetchRequests(),
|
||||||
itemBuilder: (context, index) {
|
]),
|
||||||
final relation = _relations[index];
|
child: ListView.builder(
|
||||||
final other = relation.related;
|
itemCount: _relations.length,
|
||||||
return ListTile(
|
itemBuilder: (context, index) {
|
||||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
final relation = _relations[index];
|
||||||
leading: AccountImage(content: other?.avatar),
|
final other = relation.related;
|
||||||
title: Text(other?.nick ?? 'unknown'),
|
return ListTile(
|
||||||
subtitle: Text(other?.nick ?? 'unknown'),
|
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||||
trailing: SizedBox(
|
leading: AccountImage(content: other?.avatar),
|
||||||
height: 48,
|
title: Text(other?.nick ?? 'unknown'),
|
||||||
width: 120,
|
subtitle: Text(other?.nick ?? 'unknown'),
|
||||||
child: Column(
|
trailing: SizedBox(
|
||||||
mainAxisSize: MainAxisSize.min,
|
height: 48,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
width: 120,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Row(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
Row(
|
||||||
onTap: _isUpdating
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
? null
|
children: [
|
||||||
: () => _changeRelation(relation, 2),
|
InkWell(
|
||||||
child: Text('friendBlock').tr(),
|
onTap: _isUpdating ? null : () => _changeRelation(relation, 2),
|
||||||
),
|
child: Text('friendBlock').tr(),
|
||||||
const Gap(8),
|
),
|
||||||
InkWell(
|
const Gap(8),
|
||||||
onTap: _isUpdating
|
InkWell(
|
||||||
? null
|
onTap: _isUpdating ? null : () => _deleteRelation(relation),
|
||||||
: () => _deleteRelation(relation),
|
child: Text('friendDeleteAction').tr(),
|
||||||
child: Text('friendDeleteAction').tr(),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -288,83 +311,9 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NewFriendWidget extends StatefulWidget {
|
|
||||||
const _NewFriendWidget();
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_NewFriendWidget> createState() => _NewFriendWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NewFriendWidgetState extends State<_NewFriendWidget> {
|
|
||||||
bool _isBusy = false;
|
|
||||||
|
|
||||||
final TextEditingController _relatedController = TextEditingController();
|
|
||||||
|
|
||||||
Future<void> _sendRequest() async {
|
|
||||||
if (_relatedController.text.isEmpty) return;
|
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client.post('/cgi/id/users/me/relations', data: {
|
|
||||||
'related': _relatedController.text,
|
|
||||||
});
|
|
||||||
if (!mounted) return;
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
context.showSnackbar('friendRequestSent'.tr());
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isBusy = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_relatedController.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return StyledWidget(Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'friendNew',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
).tr(),
|
|
||||||
const Gap(12),
|
|
||||||
TextField(
|
|
||||||
controller: _relatedController,
|
|
||||||
readOnly: _isBusy,
|
|
||||||
autocorrect: false,
|
|
||||||
autofocus: true,
|
|
||||||
textCapitalization: TextCapitalization.none,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'fieldFriendRelatedName'.tr(),
|
|
||||||
suffix: SizedBox(
|
|
||||||
height: 24,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: _isBusy ? null : () => _sendRequest(),
|
|
||||||
icon: Icon(Symbols.send),
|
|
||||||
visualDensity:
|
|
||||||
const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)).padding(all: 24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FriendshipListWidget extends StatefulWidget {
|
class _FriendshipListWidget extends StatefulWidget {
|
||||||
final List<SnRelationship> relations;
|
final List<SnRelationship> relations;
|
||||||
|
|
||||||
const _FriendshipListWidget({required this.relations});
|
const _FriendshipListWidget({required this.relations});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -471,9 +420,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(kFriendStatus[relation.status] ?? 'unknown')
|
Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75),
|
||||||
.tr()
|
|
||||||
.opacity(0.75),
|
|
||||||
if (relation.status == 0)
|
if (relation.status == 0)
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
@@ -494,8 +441,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap:
|
onTap: _isBusy ? null : () => _changeRelation(relation, 1),
|
||||||
_isBusy ? null : () => _changeRelation(relation, 1),
|
|
||||||
child: Text('friendUnblock').tr(),
|
child: Text('friendUnblock').tr(),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:relative_time/relative_time.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/userinfo.dart';
|
||||||
import 'package:surface/providers/widget.dart';
|
import 'package:surface/providers/widget.dart';
|
||||||
import 'package:surface/types/check_in.dart';
|
import 'package:surface/types/check_in.dart';
|
||||||
|
import 'package:surface/types/news.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.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_item.dart';
|
||||||
|
|
||||||
class HomeScreenDashEntry {
|
class HomeScreenDashEntry {
|
||||||
@@ -48,12 +51,12 @@ class HomeScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
class _HomeScreenState extends State<HomeScreen> {
|
||||||
static const List<HomeScreenDashEntry> kCards = [
|
late final List<HomeScreenDashEntry> kCards = [
|
||||||
HomeScreenDashEntry(
|
HomeScreenDashEntry(
|
||||||
name: 'dashEntryRecommendation',
|
name: 'dashEntryRecommendation',
|
||||||
cols: 2,
|
|
||||||
rows: 2,
|
|
||||||
child: _HomeDashRecommendationPostWidget(),
|
child: _HomeDashRecommendationPostWidget(),
|
||||||
|
rows: 2,
|
||||||
|
cols: 2,
|
||||||
),
|
),
|
||||||
HomeScreenDashEntry(
|
HomeScreenDashEntry(
|
||||||
name: 'dashEntryCheckIn',
|
name: 'dashEntryCheckIn',
|
||||||
@@ -63,11 +66,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
name: 'dashEntryNotification',
|
name: 'dashEntryNotification',
|
||||||
child: _HomeDashNotificationWidget(),
|
child: _HomeDashNotificationWidget(),
|
||||||
),
|
),
|
||||||
|
HomeScreenDashEntry(
|
||||||
|
name: 'dashEntryTodayNews',
|
||||||
|
child: _HomeDashTodayNews(),
|
||||||
|
cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text("screenHome").tr(),
|
title: Text("screenHome").tr(),
|
||||||
@@ -153,9 +161,14 @@ class _HomeDashUpdateWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashSpecialDayWidget extends StatelessWidget {
|
class _HomeDashSpecialDayWidget extends StatefulWidget {
|
||||||
const _HomeDashSpecialDayWidget();
|
const _HomeDashSpecialDayWidget();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_HomeDashSpecialDayWidget> createState() => _HomeDashSpecialDayWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
@@ -165,21 +178,20 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
|
|
||||||
if (days.isNotEmpty) {
|
if (days.isNotEmpty) {
|
||||||
return Column(
|
return Column(
|
||||||
spacing: 8,
|
|
||||||
children: days.map((ele) {
|
children: days.map((ele) {
|
||||||
return Card(
|
return Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
|
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
|
||||||
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
DateFormat('y/M/d').format(DateTime.now().copyWith(
|
DateFormat('y/M/d').format(DateTime.now().copyWith(
|
||||||
month: kSpecialDays[ele]!.$1,
|
month: kSpecialDays[ele]?.$1,
|
||||||
day: kSpecialDays[ele]!.$2,
|
day: kSpecialDays[ele]?.$2,
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).padding(bottom: 8);
|
).padding(bottom: 8);
|
||||||
}).toList());
|
}).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
final nextOne = dayz.getNextSpecialDay();
|
final nextOne = dayz.getNextSpecialDay();
|
||||||
@@ -193,7 +205,7 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
return Card(
|
return Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
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(
|
subtitle: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -204,6 +216,9 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
separatorType: SeparatorType.symbol,
|
separatorType: SeparatorType.symbol,
|
||||||
decoration: BoxDecoration(),
|
decoration: BoxDecoration(),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
|
onDone: () {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Expanded(
|
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 {
|
class _HomeDashCheckInWidget extends StatefulWidget {
|
||||||
const _HomeDashCheckInWidget();
|
const _HomeDashCheckInWidget();
|
||||||
|
|
||||||
@@ -380,6 +495,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
Text(
|
Text(
|
||||||
'dailyCheckInNone',
|
'dailyCheckInNone',
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
).tr(),
|
).tr(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -397,6 +514,11 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
'+${_todayRecord!.resultExperience} EXP',
|
'+${_todayRecord!.resultExperience} EXP',
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
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:provider/provider.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/notification.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/notification.dart';
|
import 'package:surface/types/notification.dart';
|
||||||
import 'package:surface/types/post.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/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
import 'package:surface/widgets/markdown_content.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:surface/widgets/post/post_item.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
import '../providers/userinfo.dart';
|
import '../providers/userinfo.dart';
|
||||||
import '../widgets/unauthorized_hint.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 {
|
class NotificationScreen extends StatefulWidget {
|
||||||
const NotificationScreen({super.key});
|
const NotificationScreen({super.key});
|
||||||
|
|
||||||
@@ -35,13 +47,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
final List<SnNotification> _notifications = List.empty(growable: true);
|
final List<SnNotification> _notifications = List.empty(growable: true);
|
||||||
int? _totalCount;
|
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 {
|
Future<void> _fetchNotifications() async {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
if (!ua.isAuthorized) return;
|
if (!ua.isAuthorized) return;
|
||||||
@@ -50,6 +55,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final nty = context.read<NotificationProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/notifications?take=10');
|
final resp = await sn.client.get('/cgi/id/notifications?take=10');
|
||||||
_totalCount = resp.data['count'];
|
_totalCount = resp.data['count'];
|
||||||
_notifications.addAll(
|
_notifications.addAll(
|
||||||
@@ -58,6 +64,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
.cast<SnNotification>() ??
|
.cast<SnNotification>() ??
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
nty.updateTray();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -82,24 +89,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _isSubmitting = true);
|
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 {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.put('/cgi/id/notifications/read', data: {
|
final nty = context.read<NotificationProvider>();
|
||||||
'messages': markList,
|
final resp = await sn.client.put('/cgi/id/notifications/read/all');
|
||||||
});
|
|
||||||
_notifications.clear();
|
_notifications.clear();
|
||||||
_fetchNotifications();
|
_fetchNotifications();
|
||||||
|
nty.clear();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar(
|
context.showSnackbar(
|
||||||
'notificationMarkAllReadPrompt'.plural(markList.length),
|
'notificationMarkAllReadPrompt'.plural(resp.data['count']),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -146,7 +146,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenNotification').tr(),
|
title: Text('screenNotification').tr(),
|
||||||
@@ -157,7 +157,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenNotification').tr(),
|
title: Text('screenNotification').tr(),
|
||||||
@@ -215,10 +215,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
),
|
),
|
||||||
if (nty.subtitle != null) const Gap(4),
|
if (nty.subtitle != null) const Gap(4),
|
||||||
MarkdownTextContent(
|
SelectionArea(
|
||||||
content: nty.body,
|
child: MarkdownTextContent(
|
||||||
isAutoWarp: true,
|
content: nty.body,
|
||||||
isSelectable: true,
|
isAutoWarp: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if ([
|
if ([
|
||||||
'interactive.feedback',
|
'interactive.feedback',
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import 'package:surface/providers/userinfo.dart';
|
|||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.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_comment_list.dart';
|
||||||
import 'package:surface/widgets/post/post_item.dart';
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
import 'package:surface/widgets/post/post_mini_editor.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 {
|
class PostDetailScreen extends StatefulWidget {
|
||||||
final String slug;
|
final String slug;
|
||||||
final SnPost? preload;
|
final SnPost? preload;
|
||||||
|
final Function? onBack;
|
||||||
|
|
||||||
const PostDetailScreen({
|
const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
|
||||||
super.key,
|
|
||||||
required this.slug,
|
|
||||||
this.preload,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
||||||
@@ -67,121 +66,129 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
return Scaffold(
|
return AppBackground(
|
||||||
appBar: AppBar(
|
isRoot: widget.onBack != null,
|
||||||
leading: BackButton(
|
child: AppScaffold(
|
||||||
onPressed: () {
|
appBar: AppBar(
|
||||||
if (GoRouter.of(context).canPop()) {
|
leading: BackButton(
|
||||||
GoRouter.of(context).pop(context);
|
onPressed: () {
|
||||||
return;
|
if (widget.onBack != null) {
|
||||||
}
|
widget.onBack!.call();
|
||||||
GoRouter.of(context).replaceNamed('explore');
|
}
|
||||||
},
|
if (GoRouter.of(context).canPop()) {
|
||||||
),
|
GoRouter.of(context).pop(context);
|
||||||
title: _data?.body['title'] != null
|
return;
|
||||||
? RichText(
|
}
|
||||||
textAlign: TextAlign.center,
|
GoRouter.of(context).replaceNamed('explore');
|
||||||
text: TextSpan(children: [
|
},
|
||||||
TextSpan(
|
|
||||||
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const TextSpan(text: '\n'),
|
|
||||||
TextSpan(
|
|
||||||
text: 'postDetail'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
: Text('postDetail').tr(),
|
|
||||||
),
|
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: LoadingIndicator(isActive: _isBusy),
|
|
||||||
),
|
),
|
||||||
if (_data != null)
|
title: _data?.body['title'] != null
|
||||||
SliverToBoxAdapter(
|
? RichText(
|
||||||
child: PostItem(
|
textAlign: TextAlign.center,
|
||||||
data: _data!,
|
text: TextSpan(children: [
|
||||||
maxWidth: 640,
|
TextSpan(
|
||||||
showComments: false,
|
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
||||||
showFullPost: true,
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
onChanged: (data) {
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
setState(() => _data = data);
|
|
||||||
},
|
|
||||||
onDeleted: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SliverToBoxAdapter(child: Divider(height: 1)),
|
|
||||||
if (_data != null)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 640),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.comment, size: 24),
|
|
||||||
const Gap(16),
|
|
||||||
Text('postCommentsDetailed')
|
|
||||||
.plural(_data!.metric.replyCount)
|
|
||||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 20, vertical: 12).center(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_data != null && ua.isAuthorized)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Container(
|
|
||||||
height: 240,
|
|
||||||
constraints: const BoxConstraints(maxWidth: 640),
|
|
||||||
margin:
|
|
||||||
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
|
||||||
? const BorderRadius.all(Radius.circular(8))
|
|
||||||
: BorderRadius.zero,
|
|
||||||
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
|
||||||
? Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
)
|
|
||||||
: Border.symmetric(
|
|
||||||
horizontal: BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const TextSpan(text: '\n'),
|
||||||
child: PostMiniEditor(
|
TextSpan(
|
||||||
postReplyId: _data!.id,
|
text: 'postDetail'.tr(),
|
||||||
onPost: () {
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
setState(() {
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
_data = _data!.copyWith(
|
),
|
||||||
metric: _data!.metric.copyWith(
|
),
|
||||||
replyCount: _data!.metric.replyCount + 1,
|
]),
|
||||||
),
|
maxLines: 2,
|
||||||
);
|
overflow: TextOverflow.ellipsis,
|
||||||
});
|
)
|
||||||
_childListKey.currentState!.refresh();
|
: Text('postDetail').tr(),
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: LoadingIndicator(isActive: _isBusy),
|
||||||
|
),
|
||||||
|
if (_data != null)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: PostItem(
|
||||||
|
data: _data!,
|
||||||
|
maxWidth: 640,
|
||||||
|
showComments: false,
|
||||||
|
showFullPost: true,
|
||||||
|
onChanged: (data) {
|
||||||
|
setState(() => _data = data);
|
||||||
|
},
|
||||||
|
onDeleted: () {
|
||||||
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).center(),
|
),
|
||||||
),
|
const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||||
if (_data != null)
|
if (_data != null)
|
||||||
PostCommentSliverList(
|
SliverToBoxAdapter(
|
||||||
key: _childListKey,
|
child: Container(
|
||||||
parentPostId: _data!.id,
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
maxWidth: 640,
|
child: Row(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
children: [
|
||||||
],
|
const Icon(Symbols.comment, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text('postCommentsDetailed')
|
||||||
|
.plural(_data!.metric.replyCount)
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, vertical: 12).center(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_data != null && ua.isAuthorized)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
height: 240,
|
||||||
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
|
margin:
|
||||||
|
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||||
|
? const BorderRadius.all(Radius.circular(8))
|
||||||
|
: BorderRadius.zero,
|
||||||
|
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||||
|
? Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / devicePixelRatio,
|
||||||
|
)
|
||||||
|
: Border.symmetric(
|
||||||
|
horizontal: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: PostMiniEditor(
|
||||||
|
postReplyId: _data!.id,
|
||||||
|
onPost: () {
|
||||||
|
setState(() {
|
||||||
|
_data = _data!.copyWith(
|
||||||
|
metric: _data!.metric.copyWith(
|
||||||
|
replyCount: _data!.metric.replyCount + 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
_childListKey.currentState!.refresh();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).center(),
|
||||||
|
),
|
||||||
|
if (_data != null)
|
||||||
|
PostCommentSliverList(
|
||||||
|
key: _childListKey,
|
||||||
|
parentPostId: _data!.id,
|
||||||
|
maxWidth: 640,
|
||||||
|
),
|
||||||
|
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:pasteboard/pasteboard.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
@@ -13,19 +19,22 @@ import 'package:surface/providers/sn_network.dart';
|
|||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.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_item.dart';
|
||||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
||||||
import 'package:surface/widgets/post/post_meta_editor.dart';
|
import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class PostEditorExtraProps {
|
import '../../types/attachment.dart';
|
||||||
|
|
||||||
|
class PostEditorExtra {
|
||||||
final String? text;
|
final String? text;
|
||||||
final String? title;
|
final String? title;
|
||||||
final String? description;
|
final String? description;
|
||||||
final List<PostWriteMedia>? attachments;
|
final List<PostWriteMedia>? attachments;
|
||||||
|
|
||||||
const PostEditorExtraProps({
|
const PostEditorExtra({
|
||||||
this.text,
|
this.text,
|
||||||
this.title,
|
this.title,
|
||||||
this.description,
|
this.description,
|
||||||
@@ -38,7 +47,7 @@ class PostEditorScreen extends StatefulWidget {
|
|||||||
final int? postEditId;
|
final int? postEditId;
|
||||||
final int? postReplyId;
|
final int? postReplyId;
|
||||||
final int? postRepostId;
|
final int? postRepostId;
|
||||||
final PostEditorExtraProps? extraProps;
|
final PostEditorExtra? extraProps;
|
||||||
|
|
||||||
const PostEditorScreen({
|
const PostEditorScreen({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -54,7 +63,9 @@ class PostEditorScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PostEditorScreenState extends State<PostEditorScreen> {
|
class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||||
final PostWriteController _writeController = PostWriteController();
|
late final PostWriteController _writeController = PostWriteController(
|
||||||
|
doLoadFromTemporary: widget.postEditId == null,
|
||||||
|
);
|
||||||
|
|
||||||
bool _isFetching = false;
|
bool _isFetching = false;
|
||||||
|
|
||||||
@@ -91,15 +102,39 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final HotKey _pasteHotKey = HotKey(
|
||||||
|
key: PhysicalKeyboardKey.keyV,
|
||||||
|
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||||
|
scope: HotKeyScope.inapp,
|
||||||
|
);
|
||||||
|
|
||||||
|
void _registerHotKey() {
|
||||||
|
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||||
|
hotKeyManager.register(_pasteHotKey, keyDownHandler: (_) async {
|
||||||
|
final imageBytes = await Pasteboard.image;
|
||||||
|
if (imageBytes == null) return;
|
||||||
|
_writeController.addAttachments([
|
||||||
|
PostWriteMedia.fromBytes(
|
||||||
|
imageBytes,
|
||||||
|
'attachmentPastedImage'.tr(),
|
||||||
|
SnMediaType.image,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_writeController.dispose();
|
_writeController.dispose();
|
||||||
|
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_registerHotKey();
|
||||||
if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) {
|
if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) {
|
||||||
context.showErrorDialog('Unknown post type');
|
context.showErrorDialog('Unknown post type');
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -126,7 +161,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: _writeController,
|
listenable: _writeController,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: BackButton(
|
leading: BackButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -150,6 +185,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
maxLines: 2,
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -247,118 +283,130 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: Stack(
|
||||||
padding: EdgeInsets.only(bottom: 8),
|
children: [
|
||||||
child: Column(
|
SingleChildScrollView(
|
||||||
children: [
|
padding: EdgeInsets.only(bottom: 160),
|
||||||
// Replying Notice
|
child: Column(
|
||||||
if (_writeController.replyingPost != null)
|
children: [
|
||||||
Column(
|
// Replying Notice
|
||||||
children: [
|
if (_writeController.replyingPost != null)
|
||||||
ExpansionTile(
|
Column(
|
||||||
minTileHeight: 48,
|
children: [
|
||||||
leading: const Icon(Symbols.reply).padding(left: 4),
|
ExpansionTile(
|
||||||
title: Text('postReplyingNotice')
|
minTileHeight: 48,
|
||||||
.fontSize(15)
|
leading: const Icon(Symbols.reply).padding(left: 4),
|
||||||
.tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
|
title: Text('postReplyingNotice')
|
||||||
children: <Widget>[PostItem(data: _writeController.replyingPost!)],
|
.fontSize(15)
|
||||||
),
|
.tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
|
||||||
const Divider(height: 1),
|
children: <Widget>[PostItem(data: _writeController.replyingPost!)],
|
||||||
],
|
),
|
||||||
),
|
const Divider(height: 1),
|
||||||
// Reposting Notice
|
|
||||||
if (_writeController.repostingPost != null)
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
ExpansionTile(
|
|
||||||
minTileHeight: 48,
|
|
||||||
leading: const Icon(Symbols.forward).padding(left: 4),
|
|
||||||
title: Text('postRepostingNotice')
|
|
||||||
.fontSize(15)
|
|
||||||
.tr(args: ['@${_writeController.repostingPost!.publisher.name}']),
|
|
||||||
children: <Widget>[
|
|
||||||
PostItem(
|
|
||||||
data: _writeController.repostingPost!,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
// Reposting Notice
|
||||||
],
|
if (_writeController.repostingPost != null)
|
||||||
),
|
Column(
|
||||||
// Editing Notice
|
children: [
|
||||||
if (_writeController.editingPost != null)
|
ExpansionTile(
|
||||||
Column(
|
minTileHeight: 48,
|
||||||
children: [
|
leading: const Icon(Symbols.forward).padding(left: 4),
|
||||||
ExpansionTile(
|
title: Text('postRepostingNotice')
|
||||||
minTileHeight: 48,
|
.fontSize(15)
|
||||||
leading: const Icon(Symbols.edit_note).padding(left: 4),
|
.tr(args: ['@${_writeController.repostingPost!.publisher.name}']),
|
||||||
title: Text('postEditingNotice')
|
children: <Widget>[
|
||||||
.fontSize(15)
|
PostItem(
|
||||||
.tr(args: ['@${_writeController.editingPost!.publisher.name}']),
|
data: _writeController.repostingPost!,
|
||||||
children: <Widget>[PostItem(data: _writeController.editingPost!)],
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// Editing Notice
|
||||||
|
if (_writeController.editingPost != null)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
ExpansionTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.edit_note).padding(left: 4),
|
||||||
|
title: Text('postEditingNotice')
|
||||||
|
.fontSize(15)
|
||||||
|
.tr(args: ['@${_writeController.editingPost!.publisher.name}']),
|
||||||
|
children: <Widget>[PostItem(data: _writeController.editingPost!)],
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// Content Input Area
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
|
child: TextField(
|
||||||
|
controller: _writeController.contentController,
|
||||||
|
maxLines: null,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'fieldPostContent'.tr(),
|
||||||
|
hintStyle: TextStyle(fontSize: 14),
|
||||||
|
isCollapsed: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// Content Input Area
|
|
||||||
TextField(
|
|
||||||
controller: _writeController.contentController,
|
|
||||||
maxLines: null,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'fieldPostContent'.tr(),
|
|
||||||
hintStyle: TextStyle(fontSize: 14),
|
|
||||||
isCollapsed: true,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
),
|
||||||
border: InputBorder.none,
|
]
|
||||||
),
|
.expandIndexed(
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
(idx, ele) => [
|
||||||
|
if (idx != 0 || _writeController.isRelatedNull) const Gap(8),
|
||||||
|
ele,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
]
|
),
|
||||||
.expandIndexed(
|
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
|
||||||
(idx, ele) => [
|
Positioned(
|
||||||
if (idx != 0 || _writeController.isRelatedNull) const Gap(8),
|
bottom: 0,
|
||||||
ele,
|
left: 0,
|
||||||
],
|
right: 0,
|
||||||
)
|
child: PostMediaPendingList(
|
||||||
.toList(),
|
thumbnail: _writeController.thumbnail,
|
||||||
),
|
attachments: _writeController.attachments,
|
||||||
|
isBusy: _writeController.isBusy,
|
||||||
|
onUpload: (int idx) async {
|
||||||
|
await _writeController.uploadSingleAttachment(context, idx);
|
||||||
|
},
|
||||||
|
onPostSetThumbnail: (int? idx) {
|
||||||
|
_writeController.setThumbnail(idx);
|
||||||
|
},
|
||||||
|
onInsertLink: (int idx) async {
|
||||||
|
_writeController.contentController.text +=
|
||||||
|
'\n';
|
||||||
|
},
|
||||||
|
onUpdate: (int idx, PostWriteMedia updatedMedia) async {
|
||||||
|
_writeController.setIsBusy(true);
|
||||||
|
try {
|
||||||
|
_writeController.setAttachmentAt(idx, updatedMedia);
|
||||||
|
} finally {
|
||||||
|
_writeController.setIsBusy(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRemove: (int idx) async {
|
||||||
|
_writeController.setIsBusy(true);
|
||||||
|
try {
|
||||||
|
_writeController.removeAttachmentAt(idx);
|
||||||
|
} finally {
|
||||||
|
_writeController.setIsBusy(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUpdateBusy: (state) => _writeController.setIsBusy(state),
|
||||||
|
).padding(bottom: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
|
|
||||||
PostMediaPendingList(
|
|
||||||
thumbnail: _writeController.thumbnail,
|
|
||||||
attachments: _writeController.attachments,
|
|
||||||
isBusy: _writeController.isBusy,
|
|
||||||
onUpload: (int idx) async {
|
|
||||||
await _writeController.uploadSingleAttachment(context, idx);
|
|
||||||
},
|
|
||||||
onPostSetThumbnail: (int? idx) {
|
|
||||||
_writeController.setThumbnail(idx);
|
|
||||||
},
|
|
||||||
onInsertLink: (int idx) async {
|
|
||||||
_writeController.contentController.text +=
|
|
||||||
'\n';
|
|
||||||
},
|
|
||||||
onUpdate: (int idx, PostWriteMedia updatedMedia) async {
|
|
||||||
_writeController.setIsBusy(true);
|
|
||||||
try {
|
|
||||||
_writeController.setAttachmentAt(idx, updatedMedia);
|
|
||||||
} finally {
|
|
||||||
_writeController.setIsBusy(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRemove: (int idx) async {
|
|
||||||
_writeController.setIsBusy(true);
|
|
||||||
try {
|
|
||||||
_writeController.removeAttachmentAt(idx);
|
|
||||||
} finally {
|
|
||||||
_writeController.setIsBusy(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUpdateBusy: (state) => _writeController.setIsBusy(state),
|
|
||||||
).padding(bottom: 8),
|
|
||||||
Material(
|
Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -373,6 +421,36 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
)
|
)
|
||||||
else if (_writeController.isBusy)
|
else if (_writeController.isBusy)
|
||||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
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(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/dialog.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_item.dart';
|
||||||
import 'package:surface/widgets/post/post_tags_field.dart';
|
import 'package:surface/widgets/post/post_tags_field.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.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(
|
appBar: AppBar(
|
||||||
title: Text('screenPostSearch').tr(),
|
title: Text('screenPostSearch').tr(),
|
||||||
actions: [
|
actions: [
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import 'package:surface/types/post.dart';
|
|||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.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_item.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.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>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
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/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.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/unauthorized_hint.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ class _RealmScreenState extends State<RealmScreen> {
|
|||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenRealm').tr(),
|
title: Text('screenRealm').tr(),
|
||||||
@@ -94,7 +95,7 @@ class _RealmScreenState extends State<RealmScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenRealm').tr(),
|
title: Text('screenRealm').tr(),
|
||||||
@@ -118,113 +119,61 @@ class _RealmScreenState extends State<RealmScreen> {
|
|||||||
children: [
|
children: [
|
||||||
LoadingIndicator(isActive: _isBusy),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RefreshIndicator(
|
child: MediaQuery.removePadding(
|
||||||
onRefresh: _fetchRealms,
|
context: context,
|
||||||
child: ListView.builder(
|
removeTop: true,
|
||||||
itemCount: _realms?.length ?? 0,
|
child: RefreshIndicator(
|
||||||
itemBuilder: (context, idx) {
|
onRefresh: _fetchRealms,
|
||||||
final realm = _realms![idx];
|
child: ListView.builder(
|
||||||
if (_isCompactView) {
|
itemCount: _realms?.length ?? 0,
|
||||||
return ListTile(
|
itemBuilder: (context, idx) {
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
final realm = _realms![idx];
|
||||||
leading: AccountImage(
|
if (_isCompactView) {
|
||||||
content: realm.avatar,
|
return ListTile(
|
||||||
fallbackWidget: const Icon(Symbols.group, size: 20),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
),
|
leading: AccountImage(
|
||||||
title: Text(realm.name),
|
content: realm.avatar,
|
||||||
subtitle: Text(
|
fallbackWidget: const Icon(Symbols.group, size: 20),
|
||||||
realm.description,
|
),
|
||||||
maxLines: 1,
|
title: Text(realm.name),
|
||||||
overflow: TextOverflow.ellipsis,
|
subtitle: Text(
|
||||||
),
|
realm.description,
|
||||||
trailing: PopupMenuButton(
|
maxLines: 1,
|
||||||
itemBuilder: (BuildContext context) => [
|
overflow: TextOverflow.ellipsis,
|
||||||
PopupMenuItem(
|
),
|
||||||
child: Row(
|
trailing: PopupMenuButton(
|
||||||
children: [
|
itemBuilder: (BuildContext context) => [
|
||||||
const Icon(Symbols.edit),
|
PopupMenuItem(
|
||||||
const Gap(16),
|
child: Row(
|
||||||
Text('edit').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'realmManage',
|
|
||||||
queryParameters: {'editing': realm.alias},
|
|
||||||
).then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
_fetchRealms();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.delete),
|
|
||||||
const Gap(16),
|
|
||||||
Text('delete').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_deleteRealm(realm);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'realmDetail',
|
|
||||||
pathParameters: {'alias': realm.alias},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
|
||||||
child: Card(
|
|
||||||
margin: const EdgeInsets.all(12),
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 16 / 7,
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
children: [
|
||||||
Container(
|
const Icon(Symbols.edit),
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
const Gap(16),
|
||||||
child: (realm.banner?.isEmpty ?? true)
|
Text('edit').tr(),
|
||||||
? const SizedBox.shrink()
|
|
||||||
: AutoResizeUniversalImage(
|
|
||||||
sn.getAttachmentUrl(realm.banner!),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: -30,
|
|
||||||
left: 18,
|
|
||||||
child: AccountImage(
|
|
||||||
content: realm.avatar,
|
|
||||||
radius: 24,
|
|
||||||
fallbackWidget: const Icon(Symbols.group, size: 24),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'realmManage',
|
||||||
|
queryParameters: {'editing': realm.alias},
|
||||||
|
).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
_fetchRealms();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete),
|
||||||
|
const Gap(16),
|
||||||
|
Text('delete').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_deleteRealm(realm);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Gap(20 + 12),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
|
|
||||||
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 24, bottom: 14),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -233,10 +182,69 @@ class _RealmScreenState extends State<RealmScreen> {
|
|||||||
pathParameters: {'alias': realm.alias},
|
pathParameters: {'alias': realm.alias},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: Card(
|
||||||
|
margin: const EdgeInsets.all(12),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 16 / 7,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: (realm.banner?.isEmpty ?? true)
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(realm.banner!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: -30,
|
||||||
|
left: 18,
|
||||||
|
child: AccountImage(
|
||||||
|
content: realm.avatar,
|
||||||
|
radius: 24,
|
||||||
|
fallbackWidget: const Icon(Symbols.group, size: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(20 + 12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
|
||||||
|
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24, bottom: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'realmDetail',
|
||||||
|
pathParameters: {'alias': realm.alias},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
).center();
|
||||||
).center();
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import 'package:surface/types/realm.dart';
|
|||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.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:surface/widgets/universal_image.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
@@ -179,7 +180,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: widget.editingRealmAlias != null
|
title: widget.editingRealmAlias != null
|
||||||
? Text('screenRealmManage').tr()
|
? Text('screenRealmManage').tr()
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/types/account.dart';
|
||||||
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/account/account_select.dart';
|
||||||
import 'package:surface/widgets/dialog.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 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
import '../../types/post.dart';
|
|
||||||
|
|
||||||
class RealmDetailScreen extends StatefulWidget {
|
class RealmDetailScreen extends StatefulWidget {
|
||||||
final String alias;
|
final String alias;
|
||||||
|
|
||||||
@@ -70,27 +72,19 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: 3,
|
length: 3,
|
||||||
child: Scaffold(
|
child: AppScaffold(
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
// These are the slivers that show up in the "outer" scroll view.
|
|
||||||
return <Widget>[
|
return <Widget>[
|
||||||
SliverOverlapAbsorber(
|
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),
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
sliver: SliverAppBar(
|
sliver: SliverAppBar(
|
||||||
title: Text(_realm?.name ?? 'loading'.tr()),
|
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(icon: const Icon(Symbols.home)),
|
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
Tab(icon: const Icon(Symbols.group)),
|
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
Tab(icon: const Icon(Symbols.settings)),
|
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -237,13 +231,35 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showMemberAdd() {
|
Future<void> _addMember(SnAccount related) async {
|
||||||
showModalBottomSheet(
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.post(
|
||||||
|
'/cgi/id/realms/${widget.realm!.alias}/members',
|
||||||
|
data: {'related': related.name},
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('realmMemberAdded'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showMemberAdd() async {
|
||||||
|
final user = await showModalBottomSheet<SnAccount?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _NewRealmMemberWidget(
|
builder: (context) => AccountSelect(
|
||||||
realm: widget.realm!,
|
title: 'realmMemberAdd'.tr(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (user == null) return;
|
||||||
|
_addMember(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -301,85 +317,6 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NewRealmMemberWidget extends StatefulWidget {
|
|
||||||
final SnRealm realm;
|
|
||||||
|
|
||||||
const _NewRealmMemberWidget({required this.realm});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_NewRealmMemberWidget> createState() => _NewRealmMemberWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NewRealmMemberWidgetState extends State<_NewRealmMemberWidget> {
|
|
||||||
bool _isBusy = false;
|
|
||||||
|
|
||||||
final TextEditingController _relatedController = TextEditingController();
|
|
||||||
|
|
||||||
Future<void> _performAction() async {
|
|
||||||
if (_relatedController.text.isEmpty) return;
|
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client.post(
|
|
||||||
'/cgi/id/realms/${widget.realm.alias}/members',
|
|
||||||
data: {
|
|
||||||
'related': _relatedController.text,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!mounted) return;
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
context.showSnackbar('channelMemberAdded'.tr());
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isBusy = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_relatedController.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return StyledWidget(Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'realmMemberAdd',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
).tr(),
|
|
||||||
const Gap(12),
|
|
||||||
TextField(
|
|
||||||
controller: _relatedController,
|
|
||||||
readOnly: _isBusy,
|
|
||||||
autocorrect: false,
|
|
||||||
autofocus: true,
|
|
||||||
textCapitalization: TextCapitalization.none,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'fieldMemberRelatedName'.tr(),
|
|
||||||
suffix: SizedBox(
|
|
||||||
height: 24,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: _isBusy ? null : () => _performAction(),
|
|
||||||
icon: Icon(Symbols.send),
|
|
||||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)).padding(all: 24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RealmSettingsWidget extends StatefulWidget {
|
class _RealmSettingsWidget extends StatefulWidget {
|
||||||
final SnRealm? realm;
|
final SnRealm? realm;
|
||||||
final Function() onUpdate;
|
final Function() onUpdate;
|
||||||
@@ -428,7 +365,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
const Gap(16),
|
const Gap(8),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.edit),
|
leading: const Icon(Symbols.edit),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
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/providers/theme.dart';
|
||||||
import 'package:surface/theme.dart';
|
import 'package:surface/theme.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
const Map<String, Color> kColorSchemes = {
|
const Map<String, Color> kColorSchemes = {
|
||||||
'colorSchemeIndigo': Colors.indigo,
|
'colorSchemeIndigo': Colors.indigo,
|
||||||
@@ -67,7 +68,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return Scaffold(
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('screenSettings').tr(),
|
||||||
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
@@ -77,6 +82,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('settingsAppearance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
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)
|
if (!kIsWeb)
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('settingsBackgroundImage').tr(),
|
title: Text('settingsBackgroundImage').tr(),
|
||||||
@@ -120,7 +167,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
subtitle: Text('settingsThemeMaterial3Description').tr(),
|
subtitle: Text('settingsThemeMaterial3Description').tr(),
|
||||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
secondary: const Icon(Symbols.new_releases),
|
secondary: const Icon(Symbols.new_releases),
|
||||||
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false,
|
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_prefs.setBool(
|
_prefs.setBool(
|
||||||
@@ -142,30 +189,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value);
|
Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value);
|
||||||
final color = await showDialog<Color?>(
|
final color = await showDialog<Color?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) =>
|
||||||
content: SingleChildScrollView(
|
AlertDialog(
|
||||||
child: ColorPicker(
|
content: SingleChildScrollView(
|
||||||
pickerColor: pickerColor,
|
child: ColorPicker(
|
||||||
onColorChanged: (color) => pickerColor = color,
|
pickerColor: pickerColor,
|
||||||
enableAlpha: false,
|
onColorChanged: (color) => pickerColor = color,
|
||||||
hexInputBar: true,
|
enableAlpha: false,
|
||||||
|
hexInputBar: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
child: const Text('dialogDismiss').tr(),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: const Text('dialogConfirm').tr(),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(pickerColor);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
actions: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
child: const Text('dialogDismiss').tr(),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
child: const Text('dialogConfirm').tr(),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop(pickerColor);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (color == null || !context.mounted) return;
|
if (color == null || !context.mounted) return;
|
||||||
@@ -201,11 +249,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
value: _prefs.getInt(kAppColorSchemeStoreKey) == null
|
value: _prefs.getInt(kAppColorSchemeStoreKey) == null
|
||||||
? 1
|
? 1
|
||||||
: kColorSchemes.values
|
: kColorSchemes.values
|
||||||
.toList()
|
.toList()
|
||||||
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
|
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
|
||||||
onChanged: (int? value) {
|
onChanged: (int? value) {
|
||||||
if (value != null && value != -1) {
|
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>();
|
final th = context.read<ThemeProvider>();
|
||||||
th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value));
|
th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value));
|
||||||
setState(() {});
|
setState(() {});
|
||||||
@@ -240,6 +290,61 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
setState(() {});
|
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(
|
Column(
|
||||||
@@ -282,7 +387,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
('Custom', _serverUrlController.text),
|
('Custom', _serverUrlController.text),
|
||||||
]
|
]
|
||||||
.map(
|
.map(
|
||||||
(item) => DropdownMenuItem<String>(
|
(item) =>
|
||||||
|
DropdownMenuItem<String>(
|
||||||
value: item.$2,
|
value: item.$2,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
@@ -294,7 +400,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
value: _serverUrlController.text,
|
value: _serverUrlController.text,
|
||||||
onChanged: (String? value) {
|
onChanged: (String? value) {
|
||||||
@@ -349,11 +455,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
items: kImageQualityLevel.entries
|
items: kImageQualityLevel.entries
|
||||||
.map(
|
.map(
|
||||||
(item) => DropdownMenuItem<FilterQuality>(
|
(item) =>
|
||||||
|
DropdownMenuItem<FilterQuality>(
|
||||||
value: item.value,
|
value: item.value,
|
||||||
child: Text(item.key).tr().fontSize(14),
|
child: Text(item.key).tr().fontSize(14),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (FilterQuality? value) {
|
onChanged: (FilterQuality? value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
|
|||||||
@@ -8,9 +8,20 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.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: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/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/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 {
|
class AppSharingListener extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@@ -51,20 +62,39 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
pathParameters: {
|
pathParameters: {
|
||||||
'mode': 'stories',
|
'mode': 'stories',
|
||||||
},
|
},
|
||||||
extra: PostEditorExtraProps(
|
extra: PostEditorExtra(
|
||||||
text: value
|
text: value
|
||||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||||
.map((e) => e.path).join('\n'),
|
.map((e) => e.path)
|
||||||
|
.join('\n'),
|
||||||
attachments: value
|
attachments: value
|
||||||
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
|
||||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(),
|
.contains(e.type))
|
||||||
|
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Navigator.pop(context);
|
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),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -103,7 +133,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||||
_initialize();
|
_initialize();
|
||||||
_initialHandle();
|
_initialHandle();
|
||||||
}
|
}
|
||||||
@@ -120,3 +150,193 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
return widget.child;
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3
|
|||||||
|
|
||||||
Future<ThemeData> createAppTheme(
|
Future<ThemeData> createAppTheme(
|
||||||
Brightness brightness, {
|
Brightness brightness, {
|
||||||
Color? seedColorOverride,
|
Color? seedColorOverride,
|
||||||
bool? useMaterial3,
|
bool? useMaterial3,
|
||||||
}) async {
|
}) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@@ -33,10 +33,11 @@ Future<ThemeData> createAppTheme(
|
|||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
);
|
);
|
||||||
|
|
||||||
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||||
|
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false),
|
useMaterial3: useM3,
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
iconTheme: IconThemeData(
|
iconTheme: IconThemeData(
|
||||||
@@ -45,12 +46,24 @@ Future<ThemeData> createAppTheme(
|
|||||||
opticalSize: 20,
|
opticalSize: 20,
|
||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
|
snackBarTheme: SnackBarThemeData(
|
||||||
|
behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
|
||||||
|
),
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
elevation: hasAppBarBlurry ? 0 : null,
|
elevation: hasAppBarTransparent ? 0 : null,
|
||||||
backgroundColor: hasAppBarBlurry ? colorScheme.surfaceContainer.withAlpha(200) : colorScheme.primary,
|
backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
|
||||||
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
|
foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
pageTransitionsTheme: PageTransitionsTheme(
|
||||||
|
builders: {
|
||||||
|
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
|
||||||
|
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||||
|
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
|
||||||
|
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
|
||||||
|
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
|
||||||
|
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
scaffoldBackgroundColor: Colors.transparent,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ class SnAccount with _$SnAccount {
|
|||||||
required DateTime? deletedAt,
|
required DateTime? deletedAt,
|
||||||
required DateTime? confirmedAt,
|
required DateTime? confirmedAt,
|
||||||
required List<SnAccountContact>? contacts,
|
required List<SnAccountContact>? contacts,
|
||||||
required String avatar,
|
@Default("") String avatar,
|
||||||
required String banner,
|
@Default("") String banner,
|
||||||
required String description,
|
required String description,
|
||||||
required String name,
|
required String name,
|
||||||
required String nick,
|
required String nick,
|
||||||
required Map<String, dynamic> permNodes,
|
required Map<String, dynamic> permNodes,
|
||||||
|
required String language,
|
||||||
required SnAccountProfile? profile,
|
required SnAccountProfile? profile,
|
||||||
@Default([]) List<SnAccountBadge> badges,
|
@Default([]) List<SnAccountBadge> badges,
|
||||||
required DateTime? suspendedAt,
|
required DateTime? suspendedAt,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ mixin _$SnAccount {
|
|||||||
String get name => throw _privateConstructorUsedError;
|
String get name => throw _privateConstructorUsedError;
|
||||||
String get nick => throw _privateConstructorUsedError;
|
String get nick => throw _privateConstructorUsedError;
|
||||||
Map<String, dynamic> get permNodes => throw _privateConstructorUsedError;
|
Map<String, dynamic> get permNodes => throw _privateConstructorUsedError;
|
||||||
|
String get language => throw _privateConstructorUsedError;
|
||||||
SnAccountProfile? get profile => throw _privateConstructorUsedError;
|
SnAccountProfile? get profile => throw _privateConstructorUsedError;
|
||||||
List<SnAccountBadge> get badges => throw _privateConstructorUsedError;
|
List<SnAccountBadge> get badges => throw _privateConstructorUsedError;
|
||||||
DateTime? get suspendedAt => throw _privateConstructorUsedError;
|
DateTime? get suspendedAt => throw _privateConstructorUsedError;
|
||||||
@@ -69,6 +70,7 @@ abstract class $SnAccountCopyWith<$Res> {
|
|||||||
String name,
|
String name,
|
||||||
String nick,
|
String nick,
|
||||||
Map<String, dynamic> permNodes,
|
Map<String, dynamic> permNodes,
|
||||||
|
String language,
|
||||||
SnAccountProfile? profile,
|
SnAccountProfile? profile,
|
||||||
List<SnAccountBadge> badges,
|
List<SnAccountBadge> badges,
|
||||||
DateTime? suspendedAt,
|
DateTime? suspendedAt,
|
||||||
@@ -107,6 +109,7 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
|
|||||||
Object? name = null,
|
Object? name = null,
|
||||||
Object? nick = null,
|
Object? nick = null,
|
||||||
Object? permNodes = null,
|
Object? permNodes = null,
|
||||||
|
Object? language = null,
|
||||||
Object? profile = freezed,
|
Object? profile = freezed,
|
||||||
Object? badges = null,
|
Object? badges = null,
|
||||||
Object? suspendedAt = freezed,
|
Object? suspendedAt = freezed,
|
||||||
@@ -164,6 +167,10 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
|
|||||||
? _value.permNodes
|
? _value.permNodes
|
||||||
: permNodes // ignore: cast_nullable_to_non_nullable
|
: permNodes // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>,
|
as Map<String, dynamic>,
|
||||||
|
language: null == language
|
||||||
|
? _value.language
|
||||||
|
: language // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
profile: freezed == profile
|
profile: freezed == profile
|
||||||
? _value.profile
|
? _value.profile
|
||||||
: profile // ignore: cast_nullable_to_non_nullable
|
: profile // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -231,6 +238,7 @@ abstract class _$$SnAccountImplCopyWith<$Res>
|
|||||||
String name,
|
String name,
|
||||||
String nick,
|
String nick,
|
||||||
Map<String, dynamic> permNodes,
|
Map<String, dynamic> permNodes,
|
||||||
|
String language,
|
||||||
SnAccountProfile? profile,
|
SnAccountProfile? profile,
|
||||||
List<SnAccountBadge> badges,
|
List<SnAccountBadge> badges,
|
||||||
DateTime? suspendedAt,
|
DateTime? suspendedAt,
|
||||||
@@ -268,6 +276,7 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
|||||||
Object? name = null,
|
Object? name = null,
|
||||||
Object? nick = null,
|
Object? nick = null,
|
||||||
Object? permNodes = null,
|
Object? permNodes = null,
|
||||||
|
Object? language = null,
|
||||||
Object? profile = freezed,
|
Object? profile = freezed,
|
||||||
Object? badges = null,
|
Object? badges = null,
|
||||||
Object? suspendedAt = freezed,
|
Object? suspendedAt = freezed,
|
||||||
@@ -325,6 +334,10 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
|||||||
? _value._permNodes
|
? _value._permNodes
|
||||||
: permNodes // ignore: cast_nullable_to_non_nullable
|
: permNodes // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>,
|
as Map<String, dynamic>,
|
||||||
|
language: null == language
|
||||||
|
? _value.language
|
||||||
|
: language // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
profile: freezed == profile
|
profile: freezed == profile
|
||||||
? _value.profile
|
? _value.profile
|
||||||
: profile // ignore: cast_nullable_to_non_nullable
|
: profile // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -367,12 +380,13 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
required this.deletedAt,
|
required this.deletedAt,
|
||||||
required this.confirmedAt,
|
required this.confirmedAt,
|
||||||
required final List<SnAccountContact>? contacts,
|
required final List<SnAccountContact>? contacts,
|
||||||
required this.avatar,
|
this.avatar = "",
|
||||||
required this.banner,
|
this.banner = "",
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.nick,
|
required this.nick,
|
||||||
required final Map<String, dynamic> permNodes,
|
required final Map<String, dynamic> permNodes,
|
||||||
|
required this.language,
|
||||||
required this.profile,
|
required this.profile,
|
||||||
final List<SnAccountBadge> badges = const [],
|
final List<SnAccountBadge> badges = const [],
|
||||||
required this.suspendedAt,
|
required this.suspendedAt,
|
||||||
@@ -410,8 +424,10 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@JsonKey()
|
||||||
final String avatar;
|
final String avatar;
|
||||||
@override
|
@override
|
||||||
|
@JsonKey()
|
||||||
final String banner;
|
final String banner;
|
||||||
@override
|
@override
|
||||||
final String description;
|
final String description;
|
||||||
@@ -427,6 +443,8 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
return EqualUnmodifiableMapView(_permNodes);
|
return EqualUnmodifiableMapView(_permNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String language;
|
||||||
@override
|
@override
|
||||||
final SnAccountProfile? profile;
|
final SnAccountProfile? profile;
|
||||||
final List<SnAccountBadge> _badges;
|
final List<SnAccountBadge> _badges;
|
||||||
@@ -451,7 +469,7 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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
|
@override
|
||||||
@@ -477,6 +495,8 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
(identical(other.nick, nick) || other.nick == nick) &&
|
(identical(other.nick, nick) || other.nick == nick) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other._permNodes, _permNodes) &&
|
.equals(other._permNodes, _permNodes) &&
|
||||||
|
(identical(other.language, language) ||
|
||||||
|
other.language == language) &&
|
||||||
(identical(other.profile, profile) || other.profile == profile) &&
|
(identical(other.profile, profile) || other.profile == profile) &&
|
||||||
const DeepCollectionEquality().equals(other._badges, _badges) &&
|
const DeepCollectionEquality().equals(other._badges, _badges) &&
|
||||||
(identical(other.suspendedAt, suspendedAt) ||
|
(identical(other.suspendedAt, suspendedAt) ||
|
||||||
@@ -507,6 +527,7 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
name,
|
name,
|
||||||
nick,
|
nick,
|
||||||
const DeepCollectionEquality().hash(_permNodes),
|
const DeepCollectionEquality().hash(_permNodes),
|
||||||
|
language,
|
||||||
profile,
|
profile,
|
||||||
const DeepCollectionEquality().hash(_badges),
|
const DeepCollectionEquality().hash(_badges),
|
||||||
suspendedAt,
|
suspendedAt,
|
||||||
@@ -540,12 +561,13 @@ abstract class _SnAccount extends SnAccount {
|
|||||||
required final DateTime? deletedAt,
|
required final DateTime? deletedAt,
|
||||||
required final DateTime? confirmedAt,
|
required final DateTime? confirmedAt,
|
||||||
required final List<SnAccountContact>? contacts,
|
required final List<SnAccountContact>? contacts,
|
||||||
required final String avatar,
|
final String avatar,
|
||||||
required final String banner,
|
final String banner,
|
||||||
required final String description,
|
required final String description,
|
||||||
required final String name,
|
required final String name,
|
||||||
required final String nick,
|
required final String nick,
|
||||||
required final Map<String, dynamic> permNodes,
|
required final Map<String, dynamic> permNodes,
|
||||||
|
required final String language,
|
||||||
required final SnAccountProfile? profile,
|
required final SnAccountProfile? profile,
|
||||||
final List<SnAccountBadge> badges,
|
final List<SnAccountBadge> badges,
|
||||||
required final DateTime? suspendedAt,
|
required final DateTime? suspendedAt,
|
||||||
@@ -584,6 +606,8 @@ abstract class _SnAccount extends SnAccount {
|
|||||||
@override
|
@override
|
||||||
Map<String, dynamic> get permNodes;
|
Map<String, dynamic> get permNodes;
|
||||||
@override
|
@override
|
||||||
|
String get language;
|
||||||
|
@override
|
||||||
SnAccountProfile? get profile;
|
SnAccountProfile? get profile;
|
||||||
@override
|
@override
|
||||||
List<SnAccountBadge> get badges;
|
List<SnAccountBadge> get badges;
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
|
|||||||
contacts: (json['contacts'] as List<dynamic>?)
|
contacts: (json['contacts'] as List<dynamic>?)
|
||||||
?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
|
?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
avatar: json['avatar'] as String,
|
avatar: json['avatar'] as String? ?? "",
|
||||||
banner: json['banner'] as String,
|
banner: json['banner'] as String? ?? "",
|
||||||
description: json['description'] as String,
|
description: json['description'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
nick: json['nick'] as String,
|
nick: json['nick'] as String,
|
||||||
permNodes: json['perm_nodes'] as Map<String, dynamic>,
|
permNodes: json['perm_nodes'] as Map<String, dynamic>,
|
||||||
|
language: json['language'] as String,
|
||||||
profile: json['profile'] == null
|
profile: json['profile'] == null
|
||||||
? null
|
? null
|
||||||
: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
|
: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
|
||||||
@@ -56,6 +57,7 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
|
|||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'nick': instance.nick,
|
'nick': instance.nick,
|
||||||
'perm_nodes': instance.permNodes,
|
'perm_nodes': instance.permNodes,
|
||||||
|
'language': instance.language,
|
||||||
'profile': instance.profile?.toJson(),
|
'profile': instance.profile?.toJson(),
|
||||||
'badges': instance.badges.map((e) => e.toJson()).toList(),
|
'badges': instance.badges.map((e) => e.toJson()).toList(),
|
||||||
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
||||||
|
|||||||
@@ -38,12 +38,13 @@ class SnAttachment with _$SnAttachment {
|
|||||||
required SnAttachment? ref,
|
required SnAttachment? ref,
|
||||||
required int? refId,
|
required int? refId,
|
||||||
required SnAttachmentPool? pool,
|
required SnAttachmentPool? pool,
|
||||||
required int poolId,
|
required int? poolId,
|
||||||
required int accountId,
|
required int accountId,
|
||||||
int? thumbnailId,
|
int? thumbnailId,
|
||||||
SnAttachment? thumbnail,
|
SnAttachment? thumbnail,
|
||||||
int? compressedId,
|
int? compressedId,
|
||||||
SnAttachment? compressed,
|
SnAttachment? compressed,
|
||||||
|
@Default([]) List<SnAttachmentBoost> boosts,
|
||||||
@Default({}) Map<String, dynamic> usermeta,
|
@Default({}) Map<String, dynamic> usermeta,
|
||||||
@Default({}) Map<String, dynamic> metadata,
|
@Default({}) Map<String, dynamic> metadata,
|
||||||
}) = _SnAttachment;
|
}) = _SnAttachment;
|
||||||
@@ -110,3 +111,69 @@ class SnAttachmentPool with _$SnAttachmentPool {
|
|||||||
|
|
||||||
factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json);
|
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
@@ -38,7 +38,7 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
|||||||
pool: json['pool'] == null
|
pool: json['pool'] == null
|
||||||
? null
|
? null
|
||||||
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
|
: 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(),
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
|
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
|
||||||
thumbnail: json['thumbnail'] == null
|
thumbnail: json['thumbnail'] == null
|
||||||
@@ -48,6 +48,11 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
|||||||
compressed: json['compressed'] == null
|
compressed: json['compressed'] == null
|
||||||
? null
|
? null
|
||||||
: SnAttachment.fromJson(json['compressed'] as Map<String, dynamic>),
|
: 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 {},
|
usermeta: json['usermeta'] as Map<String, dynamic>? ?? const {},
|
||||||
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
|
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
|
||||||
);
|
);
|
||||||
@@ -82,6 +87,7 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
|
|||||||
'thumbnail': instance.thumbnail?.toJson(),
|
'thumbnail': instance.thumbnail?.toJson(),
|
||||||
'compressed_id': instance.compressedId,
|
'compressed_id': instance.compressedId,
|
||||||
'compressed': instance.compressed?.toJson(),
|
'compressed': instance.compressed?.toJson(),
|
||||||
|
'boosts': instance.boosts.map((e) => e.toJson()).toList(),
|
||||||
'usermeta': instance.usermeta,
|
'usermeta': instance.usermeta,
|
||||||
'metadata': instance.metadata,
|
'metadata': instance.metadata,
|
||||||
};
|
};
|
||||||
@@ -161,3 +167,117 @@ Map<String, dynamic> _$$SnAttachmentPoolImplToJson(
|
|||||||
'config': instance.config,
|
'config': instance.config,
|
||||||
'account_id': instance.accountId,
|
'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 DateTime? deletedAt,
|
||||||
required int resultTier,
|
required int resultTier,
|
||||||
required int resultExperience,
|
required int resultExperience,
|
||||||
|
required double resultCoin,
|
||||||
required List<int> resultModifiers,
|
required List<int> resultModifiers,
|
||||||
required int accountId,
|
required int accountId,
|
||||||
}) = _SnCheckInRecord;
|
}) = _SnCheckInRecord;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ mixin _$SnCheckInRecord {
|
|||||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||||
int get resultTier => throw _privateConstructorUsedError;
|
int get resultTier => throw _privateConstructorUsedError;
|
||||||
int get resultExperience => throw _privateConstructorUsedError;
|
int get resultExperience => throw _privateConstructorUsedError;
|
||||||
|
double get resultCoin => throw _privateConstructorUsedError;
|
||||||
List<int> get resultModifiers => throw _privateConstructorUsedError;
|
List<int> get resultModifiers => throw _privateConstructorUsedError;
|
||||||
int get accountId => throw _privateConstructorUsedError;
|
int get accountId => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ abstract class $SnCheckInRecordCopyWith<$Res> {
|
|||||||
DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
int resultTier,
|
int resultTier,
|
||||||
int resultExperience,
|
int resultExperience,
|
||||||
|
double resultCoin,
|
||||||
List<int> resultModifiers,
|
List<int> resultModifiers,
|
||||||
int accountId});
|
int accountId});
|
||||||
}
|
}
|
||||||
@@ -77,6 +79,7 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
|||||||
Object? deletedAt = freezed,
|
Object? deletedAt = freezed,
|
||||||
Object? resultTier = null,
|
Object? resultTier = null,
|
||||||
Object? resultExperience = null,
|
Object? resultExperience = null,
|
||||||
|
Object? resultCoin = null,
|
||||||
Object? resultModifiers = null,
|
Object? resultModifiers = null,
|
||||||
Object? accountId = null,
|
Object? accountId = null,
|
||||||
}) {
|
}) {
|
||||||
@@ -105,6 +108,10 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
|||||||
? _value.resultExperience
|
? _value.resultExperience
|
||||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
resultCoin: null == resultCoin
|
||||||
|
? _value.resultCoin
|
||||||
|
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
resultModifiers: null == resultModifiers
|
resultModifiers: null == resultModifiers
|
||||||
? _value.resultModifiers
|
? _value.resultModifiers
|
||||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -132,6 +139,7 @@ abstract class _$$SnCheckInRecordImplCopyWith<$Res>
|
|||||||
DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
int resultTier,
|
int resultTier,
|
||||||
int resultExperience,
|
int resultExperience,
|
||||||
|
double resultCoin,
|
||||||
List<int> resultModifiers,
|
List<int> resultModifiers,
|
||||||
int accountId});
|
int accountId});
|
||||||
}
|
}
|
||||||
@@ -155,6 +163,7 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
|||||||
Object? deletedAt = freezed,
|
Object? deletedAt = freezed,
|
||||||
Object? resultTier = null,
|
Object? resultTier = null,
|
||||||
Object? resultExperience = null,
|
Object? resultExperience = null,
|
||||||
|
Object? resultCoin = null,
|
||||||
Object? resultModifiers = null,
|
Object? resultModifiers = null,
|
||||||
Object? accountId = null,
|
Object? accountId = null,
|
||||||
}) {
|
}) {
|
||||||
@@ -183,6 +192,10 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
|||||||
? _value.resultExperience
|
? _value.resultExperience
|
||||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
resultCoin: null == resultCoin
|
||||||
|
? _value.resultCoin
|
||||||
|
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
resultModifiers: null == resultModifiers
|
resultModifiers: null == resultModifiers
|
||||||
? _value._resultModifiers
|
? _value._resultModifiers
|
||||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -205,6 +218,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
|||||||
required this.deletedAt,
|
required this.deletedAt,
|
||||||
required this.resultTier,
|
required this.resultTier,
|
||||||
required this.resultExperience,
|
required this.resultExperience,
|
||||||
|
required this.resultCoin,
|
||||||
required final List<int> resultModifiers,
|
required final List<int> resultModifiers,
|
||||||
required this.accountId})
|
required this.accountId})
|
||||||
: _resultModifiers = resultModifiers,
|
: _resultModifiers = resultModifiers,
|
||||||
@@ -225,6 +239,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
|||||||
final int resultTier;
|
final int resultTier;
|
||||||
@override
|
@override
|
||||||
final int resultExperience;
|
final int resultExperience;
|
||||||
|
@override
|
||||||
|
final double resultCoin;
|
||||||
final List<int> _resultModifiers;
|
final List<int> _resultModifiers;
|
||||||
@override
|
@override
|
||||||
List<int> get resultModifiers {
|
List<int> get resultModifiers {
|
||||||
@@ -238,7 +254,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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
|
@override
|
||||||
@@ -257,6 +273,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
|||||||
other.resultTier == resultTier) &&
|
other.resultTier == resultTier) &&
|
||||||
(identical(other.resultExperience, resultExperience) ||
|
(identical(other.resultExperience, resultExperience) ||
|
||||||
other.resultExperience == resultExperience) &&
|
other.resultExperience == resultExperience) &&
|
||||||
|
(identical(other.resultCoin, resultCoin) ||
|
||||||
|
other.resultCoin == resultCoin) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other._resultModifiers, _resultModifiers) &&
|
.equals(other._resultModifiers, _resultModifiers) &&
|
||||||
(identical(other.accountId, accountId) ||
|
(identical(other.accountId, accountId) ||
|
||||||
@@ -273,6 +291,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
|||||||
deletedAt,
|
deletedAt,
|
||||||
resultTier,
|
resultTier,
|
||||||
resultExperience,
|
resultExperience,
|
||||||
|
resultCoin,
|
||||||
const DeepCollectionEquality().hash(_resultModifiers),
|
const DeepCollectionEquality().hash(_resultModifiers),
|
||||||
accountId);
|
accountId);
|
||||||
|
|
||||||
@@ -301,6 +320,7 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
|
|||||||
required final DateTime? deletedAt,
|
required final DateTime? deletedAt,
|
||||||
required final int resultTier,
|
required final int resultTier,
|
||||||
required final int resultExperience,
|
required final int resultExperience,
|
||||||
|
required final double resultCoin,
|
||||||
required final List<int> resultModifiers,
|
required final List<int> resultModifiers,
|
||||||
required final int accountId}) = _$SnCheckInRecordImpl;
|
required final int accountId}) = _$SnCheckInRecordImpl;
|
||||||
const _SnCheckInRecord._() : super._();
|
const _SnCheckInRecord._() : super._();
|
||||||
@@ -321,6 +341,8 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
|
|||||||
@override
|
@override
|
||||||
int get resultExperience;
|
int get resultExperience;
|
||||||
@override
|
@override
|
||||||
|
double get resultCoin;
|
||||||
|
@override
|
||||||
List<int> get resultModifiers;
|
List<int> get resultModifiers;
|
||||||
@override
|
@override
|
||||||
int get accountId;
|
int get accountId;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ _$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson(
|
|||||||
: DateTime.parse(json['deleted_at'] as String),
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
resultTier: (json['result_tier'] as num).toInt(),
|
resultTier: (json['result_tier'] as num).toInt(),
|
||||||
resultExperience: (json['result_experience'] as num).toInt(),
|
resultExperience: (json['result_experience'] as num).toInt(),
|
||||||
|
resultCoin: (json['result_coin'] as num).toDouble(),
|
||||||
resultModifiers: (json['result_modifiers'] as List<dynamic>)
|
resultModifiers: (json['result_modifiers'] as List<dynamic>)
|
||||||
.map((e) => (e as num).toInt())
|
.map((e) => (e as num).toInt())
|
||||||
.toList(),
|
.toList(),
|
||||||
@@ -32,6 +33,7 @@ Map<String, dynamic> _$$SnCheckInRecordImplToJson(
|
|||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
'result_tier': instance.resultTier,
|
'result_tier': instance.resultTier,
|
||||||
'result_experience': instance.resultExperience,
|
'result_experience': instance.resultExperience,
|
||||||
|
'result_coin': instance.resultCoin,
|
||||||
'result_modifiers': instance.resultModifiers,
|
'result_modifiers': instance.resultModifiers,
|
||||||
'account_id': instance.accountId,
|
'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:gap/gap.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class AboutScreen extends StatelessWidget {
|
class AboutScreen extends StatelessWidget {
|
||||||
@@ -12,97 +13,103 @@ class AboutScreen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
||||||
|
|
||||||
return SizedBox(
|
return AppScaffold(
|
||||||
width: double.infinity,
|
appBar: AppBar(
|
||||||
child: Column(
|
leading: const PageBackButton(),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
title: Text('screenAbout').tr(),
|
||||||
children: [
|
),
|
||||||
ClipRRect(
|
body: SizedBox(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
width: double.infinity,
|
||||||
child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
|
child: Column(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
const Gap(8),
|
children: [
|
||||||
Text(
|
ClipRRect(
|
||||||
'Solian',
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
|
child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
|
||||||
),
|
),
|
||||||
const Text(
|
const Gap(8),
|
||||||
'The Solar Network',
|
Text(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
'Solian',
|
||||||
),
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
|
||||||
const Gap(8),
|
),
|
||||||
FutureBuilder(
|
const Text(
|
||||||
future: PackageInfo.fromPlatform(),
|
'The Solar Network',
|
||||||
builder: (context, snapshot) {
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
if (!snapshot.hasData) {
|
),
|
||||||
return const SizedBox.shrink();
|
const Gap(8),
|
||||||
}
|
FutureBuilder(
|
||||||
|
future: PackageInfo.fromPlatform(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
||||||
style: const TextStyle(fontFamily: 'monospace'),
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Container(
|
Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 280),
|
constraints: const BoxConstraints(maxWidth: 280),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: denseButtonStyle,
|
style: denseButtonStyle,
|
||||||
child: Text('appDetails').tr(),
|
child: Text('appDetails').tr(),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final info = await PackageInfo.fromPlatform();
|
final info = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
showAboutDialog(
|
showAboutDialog(
|
||||||
context: context,
|
context: context,
|
||||||
applicationName: 'Solian',
|
applicationName: 'Solian',
|
||||||
applicationVersion: '${info.version}+${info.buildNumber}',
|
applicationVersion: '${info.version}+${info.buildNumber}',
|
||||||
applicationLegalese:
|
applicationLegalese:
|
||||||
'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
||||||
applicationIcon: ClipRRect(
|
applicationIcon: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/icon/icon-light-radius.png',
|
'assets/icon/icon-light-radius.png',
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
TextButton(
|
||||||
TextButton(
|
style: denseButtonStyle,
|
||||||
style: denseButtonStyle,
|
child: Text('termRelated').tr(),
|
||||||
child: Text('termRelated').tr(),
|
onPressed: () {
|
||||||
onPressed: () {
|
launchUrlString('https://solsynth.dev/terms');
|
||||||
launchUrlString('https://solsynth.dev/terms');
|
},
|
||||||
},
|
),
|
||||||
),
|
TextButton(
|
||||||
TextButton(
|
style: denseButtonStyle,
|
||||||
style: denseButtonStyle,
|
child: Text('serviceStatus').tr(),
|
||||||
child: Text('serviceStatus').tr(),
|
onPressed: () {
|
||||||
onPressed: () {
|
launchUrlString('https://status.solsynth.dev');
|
||||||
launchUrlString('https://status.solsynth.dev');
|
},
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
|
).center(),
|
||||||
|
const Gap(16),
|
||||||
|
const Text(
|
||||||
|
'Open-sourced under AGPLv3',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
).center(),
|
],
|
||||||
const Gap(16),
|
),
|
||||||
const Text(
|
|
||||||
'Open-sourced under AGPLv3',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w300,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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 {
|
class AttachmentInputDialog extends StatefulWidget {
|
||||||
final String? title;
|
final String? title;
|
||||||
|
final bool? analyzeNow;
|
||||||
const AttachmentInputDialog({super.key, required this.title});
|
const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
|
State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
|
||||||
@@ -53,6 +53,7 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
|||||||
_thumbnailFile!.path,
|
_thumbnailFile!.path,
|
||||||
'interactive',
|
'interactive',
|
||||||
null,
|
null,
|
||||||
|
analyzeNow: widget.analyzeNow ?? false,
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.pop(context, attachment);
|
Navigator.pop(context, attachment);
|
||||||
@@ -77,7 +78,8 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
|||||||
controller: _randomIdController,
|
controller: _randomIdController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'fieldAttachmentRandomId'.tr(),
|
labelText: 'fieldAttachmentRandomId'.tr(),
|
||||||
border: const OutlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ import 'package:uuid/uuid.dart';
|
|||||||
class AttachmentItem extends StatelessWidget {
|
class AttachmentItem extends StatelessWidget {
|
||||||
final SnAttachment? data;
|
final SnAttachment? data;
|
||||||
final String? heroTag;
|
final String? heroTag;
|
||||||
|
final BoxFit fit;
|
||||||
|
|
||||||
const AttachmentItem({
|
const AttachmentItem({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.heroTag,
|
required this.heroTag,
|
||||||
});
|
});
|
||||||
@@ -43,7 +45,7 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
child: AutoResizeUniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(data!.rid),
|
sn.getAttachmentUrl(data!.rid),
|
||||||
key: Key('attachment-${data!.rid}-$tag'),
|
key: Key('attachment-${data!.rid}-$tag'),
|
||||||
fit: BoxFit.cover,
|
fit: fit,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case 'video':
|
case 'video':
|
||||||
@@ -313,6 +315,7 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
|||||||
}
|
}
|
||||||
|
|
||||||
return MaterialDesktopVideoControlsTheme(
|
return MaterialDesktopVideoControlsTheme(
|
||||||
|
key: Key('material-desktop-video-controls-theme-$_showOriginal'),
|
||||||
normal: MaterialDesktopVideoControlsThemeData(
|
normal: MaterialDesktopVideoControlsThemeData(
|
||||||
buttonBarButtonSize: 24,
|
buttonBarButtonSize: 24,
|
||||||
buttonBarButtonColor: Colors.white,
|
buttonBarButtonColor: Colors.white,
|
||||||
@@ -322,12 +325,16 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
|||||||
MaterialDesktopCustomButton(
|
MaterialDesktopCustomButton(
|
||||||
iconSize: 24,
|
iconSize: 24,
|
||||||
onPressed: _toggleOriginal,
|
onPressed: _toggleOriginal,
|
||||||
icon: _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24),
|
icon: Icon(
|
||||||
|
_showOriginal ? Symbols.high_quality : Symbols.sd,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
fullscreen: const MaterialDesktopVideoControlsThemeData(),
|
fullscreen: const MaterialDesktopVideoControlsThemeData(),
|
||||||
child: MaterialVideoControlsTheme(
|
child: MaterialVideoControlsTheme(
|
||||||
|
key: Key('material-video-controls-theme-$_showOriginal'),
|
||||||
normal: MaterialVideoControlsThemeData(
|
normal: MaterialVideoControlsThemeData(
|
||||||
buttonBarButtonSize: 24,
|
buttonBarButtonSize: 24,
|
||||||
buttonBarButtonColor: Colors.white,
|
buttonBarButtonColor: Colors.white,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.dart';
|
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||||
@@ -14,19 +14,25 @@ import 'package:uuid/uuid.dart';
|
|||||||
class AttachmentList extends StatefulWidget {
|
class AttachmentList extends StatefulWidget {
|
||||||
final List<SnAttachment?> data;
|
final List<SnAttachment?> data;
|
||||||
final bool bordered;
|
final bool bordered;
|
||||||
final bool noGrow;
|
final bool gridded;
|
||||||
final bool isFlatted;
|
final bool columned;
|
||||||
|
final BoxFit fit;
|
||||||
final double? maxHeight;
|
final double? maxHeight;
|
||||||
final EdgeInsets? listPadding;
|
final double? minWidth;
|
||||||
|
final double? maxWidth;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const AttachmentList({
|
const AttachmentList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.data,
|
required this.data,
|
||||||
this.bordered = false,
|
this.bordered = false,
|
||||||
this.noGrow = false,
|
this.gridded = false,
|
||||||
this.isFlatted = false,
|
this.columned = false,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
this.maxHeight,
|
this.maxHeight,
|
||||||
this.listPadding,
|
this.minWidth,
|
||||||
|
this.maxWidth,
|
||||||
|
this.padding,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
|
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
|
||||||
@@ -41,8 +47,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
(_) => const Uuid().v4(),
|
(_) => const Uuid().v4(),
|
||||||
);
|
);
|
||||||
|
|
||||||
static const double kAttachmentMaxWidth = 640;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
@@ -51,9 +55,8 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
|
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
|
||||||
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||||
final constraints = BoxConstraints(
|
final constraints = BoxConstraints(
|
||||||
minWidth: 80,
|
minWidth: widget.minWidth ?? 80,
|
||||||
maxHeight: widget.maxHeight ?? double.infinity,
|
maxHeight: widget.maxHeight ?? MediaQuery.of(context).size.height,
|
||||||
maxWidth: layoutConstraints.maxWidth - 20,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.data.isEmpty) return const SizedBox.shrink();
|
if (widget.data.isEmpty) return const SizedBox.shrink();
|
||||||
@@ -67,112 +70,145 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
.toDouble();
|
.toDouble();
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
padding: widget.padding ?? EdgeInsets.zero,
|
||||||
? constraints.copyWith(
|
constraints: constraints,
|
||||||
maxWidth: math.min(
|
child: GestureDetector(
|
||||||
constraints.maxWidth,
|
child: AspectRatio(
|
||||||
kAttachmentMaxWidth,
|
aspectRatio: singleAspectRatio,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
border: Border.fromBorderSide(borderSide),
|
||||||
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
|
child: AttachmentItem(
|
||||||
|
data: widget.data[0],
|
||||||
|
heroTag: heroTags[0],
|
||||||
|
fit: widget.fit,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: null,
|
),
|
||||||
child: AspectRatio(
|
),
|
||||||
aspectRatio: singleAspectRatio,
|
onTap: () {
|
||||||
child: GestureDetector(
|
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
|
||||||
child: Builder(
|
context.pushTransparentRoute(
|
||||||
builder: (context) {
|
AttachmentZoomView(
|
||||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) || widget.noGrow) {
|
data: widget.data.where((ele) => ele != null).cast(),
|
||||||
return Padding(
|
initialIndex: 0,
|
||||||
// Single child list-like displaying
|
heroTags: heroTags,
|
||||||
padding: widget.listPadding ?? EdgeInsets.zero,
|
),
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.7),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
top: borderSide,
|
||||||
|
bottom: borderSide,
|
||||||
|
),
|
||||||
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
|
),
|
||||||
|
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(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
constraints: constraints,
|
||||||
color: backgroundColor,
|
child: AttachmentItem(
|
||||||
border: Border(top: borderSide, bottom: borderSide),
|
data: ele,
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
heroTag: heroTags[idx],
|
||||||
),
|
fit: BoxFit.cover,
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
|
||||||
child: AttachmentItem(
|
|
||||||
data: widget.data[0],
|
|
||||||
heroTag: heroTags[0],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
onTap: () {
|
||||||
}
|
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
|
||||||
|
context.pushTransparentRoute(
|
||||||
return Container(
|
AttachmentZoomView(
|
||||||
decoration: BoxDecoration(
|
data: widget.data.where((ele) => ele != null).cast(),
|
||||||
color: backgroundColor,
|
initialIndex: idx,
|
||||||
border: Border(top: borderSide, bottom: borderSide),
|
heroTags: heroTags,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.7),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
child: AttachmentItem(
|
)
|
||||||
data: widget.data[0],
|
.toList(),
|
||||||
heroTag: heroTags.first,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
|
|
||||||
context.pushTransparentRoute(
|
|
||||||
AttachmentZoomView(
|
|
||||||
data: widget.data.where((ele) => ele != null).cast(),
|
|
||||||
initialIndex: 0,
|
|
||||||
heroTags: heroTags,
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.black.withOpacity(0.7),
|
|
||||||
rootNavigator: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.isFlatted) {
|
if ((!fullOfImage && widget.gridded) || widget.columned) {
|
||||||
return Wrap(
|
return Container(
|
||||||
spacing: 4,
|
margin: widget.padding ?? EdgeInsets.zero,
|
||||||
runSpacing: 4,
|
decoration: BoxDecoration(
|
||||||
children: widget.data
|
color: backgroundColor,
|
||||||
.mapIndexed(
|
border: Border(
|
||||||
(idx, ele) => AspectRatio(
|
top: borderSide,
|
||||||
aspectRatio: (ele?.data['ratio'] ?? 1).toDouble(),
|
bottom: borderSide,
|
||||||
child: Container(
|
),
|
||||||
decoration: BoxDecoration(
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
color: backgroundColor,
|
),
|
||||||
border: Border(
|
child: ClipRRect(
|
||||||
top: borderSide,
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
bottom: borderSide,
|
child: Column(
|
||||||
),
|
children: widget.data
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
.mapIndexed(
|
||||||
),
|
(idx, ele) => GestureDetector(
|
||||||
child: ClipRRect(
|
child: AspectRatio(
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
||||||
child: AttachmentItem(
|
child: Container(
|
||||||
data: ele,
|
constraints: constraints,
|
||||||
heroTag: heroTags[idx],
|
child: AttachmentItem(
|
||||||
|
data: ele,
|
||||||
|
heroTag: heroTags[idx],
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
),
|
.expand((ele) => [ele, const Divider(height: 1)])
|
||||||
)
|
.toList()
|
||||||
.toList(),
|
..removeLast(),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AspectRatio(
|
return AspectRatio(
|
||||||
aspectRatio: (widget.data.firstOrNull?.data['ratio'] ?? 1).toDouble(),
|
aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||||
child: ScrollConfiguration(
|
child: ScrollConfiguration(
|
||||||
behavior: _AttachmentListScrollBehavior(),
|
behavior: _AttachmentListScrollBehavior(),
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
|
padding: widget.padding,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: widget.data.length,
|
itemCount: widget.data.length,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: constraints,
|
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@@ -180,8 +216,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
AttachmentZoomView(
|
AttachmentZoomView(
|
||||||
data:
|
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
||||||
widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
|
||||||
initialIndex: idx,
|
initialIndex: idx,
|
||||||
heroTags: heroTags,
|
heroTags: heroTags,
|
||||||
),
|
),
|
||||||
@@ -223,7 +258,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => const Gap(8),
|
separatorBuilder: (context, index) => const Gap(8),
|
||||||
padding: widget.listPadding,
|
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
|
|
||||||
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
|
bool _showDetail = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
@@ -144,223 +146,351 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
onDismissed: () {
|
onDismissed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
direction: DismissiblePageDismissDirection.down,
|
direction: DismissiblePageDismissDirection.none,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
isFullScreen: true,
|
isFullScreen: true,
|
||||||
child: Scaffold(
|
child: GestureDetector(
|
||||||
body: Stack(
|
behavior: HitTestBehavior.translucent,
|
||||||
children: [
|
child: Scaffold(
|
||||||
Builder(builder: (context) {
|
backgroundColor: Colors.transparent,
|
||||||
if (widget.data.length == 1) {
|
body: Stack(
|
||||||
final heroTag = widget.heroTags?.first ?? uuid.v4();
|
children: [
|
||||||
return Hero(
|
Builder(builder: (context) {
|
||||||
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
if (widget.data.length == 1) {
|
||||||
child: PhotoView(
|
final heroTag = widget.heroTags?.first ?? uuid.v4();
|
||||||
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
|
return Hero(
|
||||||
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
||||||
imageProvider: UniversalImage.provider(
|
child: PhotoView(
|
||||||
sn.getAttachmentUrl(widget.data.first.rid),
|
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
|
||||||
),
|
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
||||||
),
|
imageProvider: UniversalImage.provider(
|
||||||
);
|
sn.getAttachmentUrl(widget.data.first.rid),
|
||||||
}
|
),
|
||||||
|
|
||||||
return PhotoViewGallery.builder(
|
|
||||||
pageController: _pageController,
|
|
||||||
scrollPhysics: const BouncingScrollPhysics(),
|
|
||||||
builder: (context, idx) {
|
|
||||||
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
|
|
||||||
return PhotoViewGalleryPageOptions(
|
|
||||||
imageProvider: UniversalImage.provider(
|
|
||||||
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
|
|
||||||
),
|
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
|
||||||
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
itemCount: widget.data.length,
|
|
||||||
loadingBuilder: (context, event) => Center(
|
return PhotoViewGallery.builder(
|
||||||
child: SizedBox(
|
pageController: _pageController,
|
||||||
width: 20.0,
|
scrollPhysics: const BouncingScrollPhysics(),
|
||||||
height: 20.0,
|
builder: (context, idx) {
|
||||||
child: CircularProgressIndicator(
|
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
|
||||||
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
|
return PhotoViewGalleryPageOptions(
|
||||||
|
imageProvider: UniversalImage.provider(
|
||||||
|
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
|
||||||
|
),
|
||||||
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
|
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: widget.data.length,
|
||||||
|
loadingBuilder: (context, event) => Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20.0,
|
||||||
|
height: 20.0,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
||||||
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
Align(
|
||||||
Align(
|
alignment: Alignment.bottomCenter,
|
||||||
alignment: Alignment.bottomCenter,
|
child: IgnorePointer(
|
||||||
child: IgnorePointer(
|
child: Container(
|
||||||
child: Container(
|
height: 300,
|
||||||
height: 300,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
gradient: LinearGradient(
|
||||||
gradient: LinearGradient(
|
begin: Alignment.bottomCenter,
|
||||||
begin: Alignment.bottomCenter,
|
end: Alignment.topCenter,
|
||||||
end: Alignment.topCenter,
|
colors: [
|
||||||
colors: [
|
Theme.of(context).colorScheme.surface,
|
||||||
Theme.of(context).colorScheme.surface,
|
Colors.transparent,
|
||||||
Colors.transparent,
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Positioned(
|
||||||
Positioned(
|
left: 16,
|
||||||
left: 16,
|
right: 16,
|
||||||
right: 16,
|
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||||
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
child: Material(
|
||||||
child: Material(
|
color: Colors.transparent,
|
||||||
color: Colors.transparent,
|
child: Builder(builder: (context) {
|
||||||
child: Builder(builder: (context) {
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final item = widget.data.elementAt(
|
||||||
final item = widget.data.elementAt(
|
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
|
||||||
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
|
);
|
||||||
);
|
final account = ud.getAccountFromCache(item.accountId);
|
||||||
final account = ud.getAccountFromCache(item.accountId);
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (item.accountId > 0)
|
if (item.accountId > 0)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
child: AccountImage(
|
child: AccountImage(
|
||||||
content: account!.avatar,
|
content: account?.avatar,
|
||||||
radius: 19,
|
radius: 19,
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: IgnorePointer(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'attachmentUploadBy'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
account.nick,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const Gap(8),
|
||||||
if (widget.data.length > 1)
|
Expanded(
|
||||||
IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: Text(
|
child: Column(
|
||||||
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: GoogleFonts.robotoMono(fontSize: 13),
|
children: [
|
||||||
).padding(right: 8),
|
Text(
|
||||||
),
|
'attachmentUploadBy'.tr(),
|
||||||
InkWell(
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
|
||||||
onTap: _isDownloading
|
|
||||||
? null
|
|
||||||
: () => _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(6),
|
|
||||||
child: !_isDownloading
|
|
||||||
? !_isCompletedDownload
|
|
||||||
? const Icon(Symbols.save_alt)
|
|
||||||
: const Icon(Symbols.download_done)
|
|
||||||
: SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
value: _progressOfDownload,
|
|
||||||
strokeWidth: 3,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
account?.nick ?? 'unknown'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
if (widget.data.length > 1)
|
||||||
|
IgnorePointer(
|
||||||
|
child: Text(
|
||||||
|
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
).padding(right: 8),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
|
onTap: _isDownloading
|
||||||
|
? null
|
||||||
|
: () =>
|
||||||
|
_saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
child: !_isDownloading
|
||||||
|
? !_isCompletedDownload
|
||||||
|
? const Icon(Symbols.save_alt)
|
||||||
|
: const Icon(Symbols.download_done)
|
||||||
|
: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: _progressOfDownload,
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Text(
|
||||||
|
item.alt,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
IgnorePointer(
|
|
||||||
child: Text(
|
|
||||||
item.alt,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const Gap(2),
|
||||||
const Gap(2),
|
IgnorePointer(
|
||||||
IgnorePointer(
|
child: Wrap(
|
||||||
child: Wrap(
|
spacing: 6,
|
||||||
spacing: 6,
|
children: [
|
||||||
children: [
|
if (item.metadata['exif'] == null)
|
||||||
if (item.metadata['exif'] == null)
|
Text(
|
||||||
|
'#${item.rid}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
|
if (item.metadata['exif']?['Model'] != null)
|
||||||
|
Text(
|
||||||
|
'attachmentShotOn'.tr(args: [
|
||||||
|
item.metadata['exif']?['Model'],
|
||||||
|
]),
|
||||||
|
style: metaTextStyle,
|
||||||
|
).padding(right: 2),
|
||||||
|
if (item.metadata['exif']?['ISO'] != null)
|
||||||
|
Text(
|
||||||
|
'ISO${item.metadata['exif']?['ISO']}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
).padding(right: 2),
|
||||||
|
if (item.metadata['exif']?['Aperture'] != null)
|
||||||
|
Text(
|
||||||
|
'f/${item.metadata['exif']?['Aperture']}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
).padding(right: 2),
|
||||||
|
if (item.metadata['exif']?['Megapixels'] != null &&
|
||||||
|
item.metadata['exif']?['Model'] != null)
|
||||||
|
Text(
|
||||||
|
'${item.metadata['exif']?['Megapixels']}MP',
|
||||||
|
style: metaTextStyle,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
item.size.formatBytes(),
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
|
if (item.metadata['width'] != null && item.metadata['height'] != null)
|
||||||
|
Text(
|
||||||
|
'${item.metadata['width']}x${item.metadata['height']}',
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
|
if (item.metadata['ratio'] != null)
|
||||||
|
Text(
|
||||||
|
(item.metadata['ratio'] as num).toStringAsFixed(2),
|
||||||
|
style: metaTextStyle,
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
'#${item.rid}',
|
item.mimetype,
|
||||||
style: metaTextStyle,
|
style: metaTextStyle,
|
||||||
),
|
),
|
||||||
if (item.metadata['exif']?['Model'] != null)
|
],
|
||||||
Text(
|
),
|
||||||
'attachmentShotOn'.tr(args: [
|
|
||||||
item.metadata['exif']?['Model'],
|
|
||||||
]),
|
|
||||||
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']}',
|
|
||||||
style: metaTextStyle,
|
|
||||||
).padding(right: 2),
|
|
||||||
if (item.metadata['exif']?['Aperture'] != null)
|
|
||||||
Text(
|
|
||||||
'f/${item.metadata['exif']?['Aperture']}',
|
|
||||||
style: metaTextStyle,
|
|
||||||
).padding(right: 2),
|
|
||||||
if (item.metadata['exif']?['Megapixels'] != null && item.metadata['exif']?['Model'] != null)
|
|
||||||
Text(
|
|
||||||
'${item.metadata['exif']?['Megapixels']}MP',
|
|
||||||
style: metaTextStyle,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Text(
|
|
||||||
'${item.size} Bytes',
|
|
||||||
style: metaTextStyle,
|
|
||||||
),
|
|
||||||
if (item.metadata['width'] != null && item.metadata['height'] != null)
|
|
||||||
Text(
|
|
||||||
'${item.metadata['width']}x${item.metadata['height']}',
|
|
||||||
style: metaTextStyle,
|
|
||||||
),
|
|
||||||
if (item.metadata['ratio'] != null)
|
|
||||||
Text(
|
|
||||||
(item.metadata['ratio'] as num).toStringAsFixed(2),
|
|
||||||
style: metaTextStyle,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
item.mimetype,
|
|
||||||
style: metaTextStyle,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
}),
|
||||||
}),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:popover/popover.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.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/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/account/account_image.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/attachment/attachment_list.dart';
|
||||||
import 'package:surface/widgets/context_menu.dart';
|
import 'package:surface/widgets/context_menu.dart';
|
||||||
import 'package:surface/widgets/link_preview.dart';
|
import 'package:surface/widgets/link_preview.dart';
|
||||||
@@ -24,6 +29,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
final Function(SnChatMessage)? onReply;
|
final Function(SnChatMessage)? onReply;
|
||||||
final Function(SnChatMessage)? onEdit;
|
final Function(SnChatMessage)? onEdit;
|
||||||
final Function(SnChatMessage)? onDelete;
|
final Function(SnChatMessage)? onDelete;
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
const ChatMessage({
|
const ChatMessage({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -35,6 +41,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
this.onReply,
|
this.onReply,
|
||||||
this.onEdit,
|
this.onEdit,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
|
this.padding = const EdgeInsets.only(left: 12, right: 12),
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -47,13 +54,15 @@ class ChatMessage extends StatelessWidget {
|
|||||||
|
|
||||||
final dateFormatter = DateFormat('MM/dd HH:mm');
|
final dateFormatter = DateFormat('MM/dd HH:mm');
|
||||||
|
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
|
||||||
return SwipeTo(
|
return SwipeTo(
|
||||||
key: Key('chat-message-${data.id}'),
|
key: Key('chat-message-${data.id}'),
|
||||||
iconOnLeftSwipe: Symbols.reply,
|
iconOnLeftSwipe: Symbols.reply,
|
||||||
iconOnRightSwipe: Symbols.edit,
|
iconOnRightSwipe: Symbols.edit,
|
||||||
swipeSensitivity: 20,
|
swipeSensitivity: 20,
|
||||||
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
||||||
onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null,
|
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
|
||||||
child: ContextMenuArea(
|
child: ContextMenuArea(
|
||||||
contextMenu: ContextMenu(
|
contextMenu: ContextMenu(
|
||||||
entries: [
|
entries: [
|
||||||
@@ -87,83 +96,120 @@ class ChatMessage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: isCompact ? EdgeInsets.zero : padding,
|
||||||
children: [
|
child: Row(
|
||||||
if (!isMerged && !isCompact)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
AccountImage(
|
children: [
|
||||||
content: user?.avatar,
|
if (!isMerged && !isCompact)
|
||||||
)
|
GestureDetector(
|
||||||
else if (isMerged)
|
child: AccountImage(
|
||||||
const Gap(40),
|
content: user?.avatar,
|
||||||
const Gap(8),
|
),
|
||||||
Expanded(
|
onTap: () {
|
||||||
child: Column(
|
if (user == null) return;
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
showPopover(
|
||||||
children: [
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
if (!isMerged)
|
context: context,
|
||||||
Row(
|
transition: PopoverTransition.other,
|
||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
bodyBuilder: (context) => SizedBox(
|
||||||
textBaseline: TextBaseline.alphabetic,
|
width: math.min(400, MediaQuery.of(context).size.width - 10),
|
||||||
children: [
|
child: AccountPopoverCard(
|
||||||
if (isCompact)
|
data: user,
|
||||||
AccountImage(
|
|
||||||
content: user?.avatar,
|
|
||||||
radius: 12,
|
|
||||||
).padding(right: 6),
|
|
||||||
Text(
|
|
||||||
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
|
|
||||||
).bold(),
|
|
||||||
const Gap(6),
|
|
||||||
Text(
|
|
||||||
dateFormatter.format(data.createdAt.toLocal()),
|
|
||||||
).fontSize(13),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (isCompact) const Gap(4),
|
|
||||||
if (data.preload?.quoteEvent != null)
|
|
||||||
StyledWidget(Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.only(
|
direction: PopoverDirection.bottom,
|
||||||
left: 4,
|
arrowHeight: 5,
|
||||||
right: 4,
|
arrowWidth: 15,
|
||||||
top: 8,
|
arrowDxOffset: -190,
|
||||||
bottom: 6,
|
);
|
||||||
),
|
|
||||||
child: ChatMessage(
|
|
||||||
data: data.preload!.quoteEvent!,
|
|
||||||
isCompact: true,
|
|
||||||
onReply: onReply,
|
|
||||||
onEdit: onEdit,
|
|
||||||
onDelete: onDelete,
|
|
||||||
),
|
|
||||||
)).padding(bottom: 4, top: 4),
|
|
||||||
switch (data.type) {
|
|
||||||
'messages.new' => _ChatMessageText(data: data),
|
|
||||||
_ => _ChatMessageSystemNotify(data: data),
|
|
||||||
},
|
},
|
||||||
],
|
)
|
||||||
),
|
else if (isMerged)
|
||||||
)
|
const Gap(40),
|
||||||
],
|
const Gap(8),
|
||||||
).opacity(isPending ? 0.5 : 1),
|
Expanded(
|
||||||
if (data.body['text'] != null && (data.body['text']?.isNotEmpty ?? false))
|
child: Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 480),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (!isMerged)
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (isCompact)
|
||||||
|
AccountImage(
|
||||||
|
content: user?.avatar,
|
||||||
|
radius: 12,
|
||||||
|
).padding(right: 8),
|
||||||
|
Text(
|
||||||
|
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
|
||||||
|
).bold(),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
dateFormatter.format(data.createdAt.toLocal()),
|
||||||
|
).fontSize(13),
|
||||||
|
],
|
||||||
|
).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(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
|
top: 8,
|
||||||
|
bottom: 6,
|
||||||
|
),
|
||||||
|
child: ChatMessage(
|
||||||
|
data: data.preload!.quoteEvent!,
|
||||||
|
isCompact: true,
|
||||||
|
onReply: onReply,
|
||||||
|
onEdit: onEdit,
|
||||||
|
onDelete: onDelete,
|
||||||
|
),
|
||||||
|
)).padding(bottom: 4, top: 4),
|
||||||
|
switch (data.type) {
|
||||||
|
'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.type == 'messages.new' &&
|
||||||
|
(data.body['text']?.isNotEmpty ?? false) &&
|
||||||
|
(cfg.prefs.getBool(kAppExpandChatLink) ?? true))
|
||||||
LinkPreviewWidget(text: data.body['text']!),
|
LinkPreviewWidget(text: data.body['text']!),
|
||||||
if (data.preload?.attachments?.isNotEmpty ?? false)
|
if (data.preload?.attachments?.isNotEmpty ?? false)
|
||||||
AttachmentList(
|
AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
bordered: true,
|
bordered: true,
|
||||||
noGrow: true,
|
maxHeight: 560,
|
||||||
maxHeight: 520,
|
maxWidth: 480,
|
||||||
listPadding: const EdgeInsets.only(top: 8),
|
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,19 +219,75 @@ class ChatMessage extends StatelessWidget {
|
|||||||
|
|
||||||
class _ChatMessageText extends StatelessWidget {
|
class _ChatMessageText extends StatelessWidget {
|
||||||
final SnChatMessage data;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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) {
|
if (data.body['text'] != null && data.body['text'].isNotEmpty) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MarkdownTextContent(
|
SelectionArea(
|
||||||
content: data.body['text'],
|
contextMenuBuilder: (context, editableTextState) {
|
||||||
isSelectable: true,
|
final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
|
||||||
isAutoWarp: true,
|
|
||||||
|
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'],
|
||||||
|
isAutoWarp: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (data.updatedAt != data.createdAt)
|
if (data.updatedAt != data.createdAt)
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math' show min;
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:pasteboard/pasteboard.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/chat_message_controller.dart';
|
import 'package:surface/controllers/chat_message_controller.dart';
|
||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
import 'package:surface/providers/sn_attachment.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/providers/user_directory.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/dialog.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/post/post_media_pending_list.dart';
|
||||||
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
class ChatMessageInput extends StatefulWidget {
|
class ChatMessageInput extends StatefulWidget {
|
||||||
final ChatMessageController controller;
|
final ChatMessageController controller;
|
||||||
@@ -33,12 +43,55 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
final TextEditingController _contentController = TextEditingController();
|
final TextEditingController _contentController = TextEditingController();
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
|
||||||
|
final HotKey _pasteHotKey = HotKey(
|
||||||
|
key: PhysicalKeyboardKey.keyV,
|
||||||
|
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||||
|
scope: HotKeyScope.inapp,
|
||||||
|
);
|
||||||
|
|
||||||
|
void _registerHotKey() {
|
||||||
|
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||||
|
hotKeyManager.register(_pasteHotKey, keyDownHandler: (_) async {
|
||||||
|
final imageBytes = await Pasteboard.image;
|
||||||
|
if (imageBytes == null) return;
|
||||||
|
_attachments.add(PostWriteMedia.fromBytes(
|
||||||
|
imageBytes,
|
||||||
|
'attachmentPastedImage'.tr(),
|
||||||
|
SnMediaType.image,
|
||||||
|
));
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_registerHotKey();
|
||||||
|
_contentController.addListener(() {
|
||||||
|
if (_contentController.text.isNotEmpty) {
|
||||||
|
widget.controller.pingTypingStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void setReply(SnChatMessage? value) {
|
void setReply(SnChatMessage? value) {
|
||||||
setState(() => _replyingMessage = 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) {
|
void setEdit(SnChatMessage? value) {
|
||||||
_contentController.text = value?.body['text'] ?? '';
|
_contentController.text = value?.body['text'] ?? '';
|
||||||
|
_attachments.clear();
|
||||||
|
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
||||||
setState(() => _editingMessage = value);
|
setState(() => _editingMessage = value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +136,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
media.toFile()!,
|
media.toFile()!,
|
||||||
place.$1,
|
place.$1,
|
||||||
place.$2,
|
place.$2,
|
||||||
|
analyzeNow: media.type == SnMediaType.image,
|
||||||
onProgress: (progress) {
|
onProgress: (progress) {
|
||||||
// Calculate overall progress for attachments
|
// Calculate overall progress for attachments
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -91,7 +145,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_attachments[i] = PostWriteMedia(item);
|
setState(() {
|
||||||
|
_attachments[i] = PostWriteMedia(item);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -103,7 +159,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
// Send the message
|
// 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
|
// 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(
|
widget.controller.sendMessage(
|
||||||
'messages.new',
|
_editingMessage != null ? 'messages.edit' : 'messages.new',
|
||||||
_contentController.text,
|
_contentController.text,
|
||||||
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
||||||
relatedId: _editingMessage?.id,
|
relatedId: _editingMessage?.id,
|
||||||
@@ -120,10 +176,34 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
|
|
||||||
final List<PostWriteMedia> _attachments = List.empty(growable: true);
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_contentController.dispose();
|
_contentController.dispose();
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
|
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,75 +240,84 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: Padding(
|
child: _replyingMessage != null
|
||||||
padding: _replyingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
? Container(
|
||||||
child: _replyingMessage != null
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
? MaterialBanner(
|
decoration: BoxDecoration(
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
border: Border(
|
||||||
leading: const Icon(Symbols.reply),
|
bottom: BorderSide(
|
||||||
backgroundColor: Colors.transparent,
|
color: Theme.of(context).dividerColor,
|
||||||
content: SingleChildScrollView(
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (_replyingMessage?.body['text'] != null)
|
|
||||||
MarkdownTextContent(
|
|
||||||
content: _replyingMessage?.body['text'],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
),
|
||||||
TextButton(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.reply, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
InkWell(
|
||||||
child: Text('cancel'.tr()),
|
child: Text('cancel'.tr()),
|
||||||
onPressed: () {
|
onTap: () {
|
||||||
|
_attachments.clear();
|
||||||
setState(() => _replyingMessage = null);
|
setState(() => _replyingMessage = null);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
).padding(vertical: 8),
|
||||||
: const SizedBox.shrink(),
|
)
|
||||||
),
|
: 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),
|
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: Padding(
|
child: _editingMessage != null
|
||||||
padding: _editingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
? Container(
|
||||||
child: _editingMessage != null
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
? MaterialBanner(
|
decoration: BoxDecoration(
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
border: Border(
|
||||||
leading: const Icon(Symbols.edit),
|
bottom: BorderSide(
|
||||||
backgroundColor: Colors.transparent,
|
color: Theme.of(context).dividerColor,
|
||||||
content: SingleChildScrollView(
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (_editingMessage?.body['text'] != null)
|
|
||||||
MarkdownTextContent(
|
|
||||||
content: _editingMessage?.body['text'],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
),
|
||||||
TextButton(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
InkWell(
|
||||||
child: Text('cancel'.tr()),
|
child: Text('cancel'.tr()),
|
||||||
onPressed: () {
|
onTap: () {
|
||||||
|
_attachments.clear();
|
||||||
|
_contentController.clear();
|
||||||
setState(() => _editingMessage = null);
|
setState(() => _editingMessage = null);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
).padding(vertical: 8),
|
||||||
: const SizedBox.shrink(),
|
)
|
||||||
),
|
: 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),
|
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 56,
|
height: 56,
|
||||||
@@ -256,6 +345,19 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
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(
|
AddPostMediaButton(
|
||||||
onAdd: (items) {
|
onAdd: (items) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -281,3 +383,107 @@ 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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.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:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
|
|
||||||
@@ -11,50 +14,62 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ws = context.watch<WebSocketProvider>();
|
final ws = context.watch<WebSocketProvider>();
|
||||||
|
final cfg = context.watch<ConfigProvider>();
|
||||||
|
|
||||||
|
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: ws,
|
listenable: ws,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized;
|
||||||
|
|
||||||
return GestureDetector(
|
return IgnorePointer(
|
||||||
child: Container(
|
ignoring: !show,
|
||||||
padding: EdgeInsets.only(
|
child: Center(
|
||||||
bottom: 8,
|
child: GestureDetector(
|
||||||
top: MediaQuery.of(context).padding.top + 8,
|
child: Material(
|
||||||
left: 24,
|
elevation: 2,
|
||||||
right: 24,
|
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)
|
||||||
|
else if (!ws.isConnected)
|
||||||
|
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(),
|
||||||
|
).opacity(show ? 1 : 0, animate: true).animate(
|
||||||
|
const Duration(milliseconds: 300),
|
||||||
|
Curves.easeInOut,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (!ws.isConnected && !ws.isBusy) {
|
||||||
|
ws.connect();
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
).padding(left: marginLeft),
|
||||||
child: ua.isAuthorized
|
|
||||||
? Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (ws.isBusy)
|
|
||||||
Text('serverConnecting').tr().textColor(
|
|
||||||
Theme.of(context).colorScheme.onSecondaryContainer)
|
|
||||||
else if (!ws.isConnected)
|
|
||||||
Text('serverDisconnected').tr().textColor(
|
|
||||||
Theme.of(context).colorScheme.onSecondaryContainer),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
)
|
|
||||||
.height(
|
|
||||||
(ws.isBusy || !ws.isConnected) && ua.isAuthorized
|
|
||||||
? MediaQuery.of(context).padding.top + 36
|
|
||||||
: 0,
|
|
||||||
animate: true)
|
|
||||||
.animate(
|
|
||||||
const Duration(milliseconds: 300),
|
|
||||||
Curves.easeInOut,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
if (!ws.isConnected && !ws.isBusy) {
|
|
||||||
ws.connect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:flutter_context_menu/flutter_context_menu.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 {
|
class ContextMenuArea extends StatelessWidget {
|
||||||
final ContextMenu contextMenu;
|
final ContextMenu contextMenu;
|
||||||
@@ -22,13 +23,12 @@ class ContextMenuArea extends StatelessWidget {
|
|||||||
return Listener(
|
return Listener(
|
||||||
onPointerDown: (event) {
|
onPointerDown: (event) {
|
||||||
mousePosition = event.position;
|
mousePosition = event.position;
|
||||||
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
|
final cfg = context.read<ConfigProvider>();
|
||||||
if (!isCollapseDrawer) {
|
if (!cfg.drawerIsCollapsed) {
|
||||||
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
|
|
||||||
// Leave padding for side navigation
|
// Leave padding for side navigation
|
||||||
mousePosition = isExpandDrawer
|
mousePosition = cfg.drawerIsExpanded
|
||||||
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
||||||
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
|
: mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: GestureDetector(
|
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