Compare commits
74 Commits
Author | SHA1 | Date | |
---|---|---|---|
6e544c0b6c | |||
7d56c5ef31 | |||
c2df1af16d | |||
a8143c6453 | |||
04065061e0 | |||
226eb452e5 | |||
a6715b0872 | |||
43e3404dbb | |||
c91cf7c813 | |||
42ac12b53e | |||
63567bf708 | |||
5d3cadefef | |||
251fbb2503 | |||
0b31d32217 | |||
5ddd4fed2e | |||
48b6d5f6c1 | |||
b83b0b5efb | |||
cb24bd953d | |||
4937dee182 | |||
d612097bb1 | |||
058d668b6b | |||
8b19462c3a | |||
0a381ef09b | |||
9b84e912b2 | |||
b3254e0f2f | |||
f0a3bbe023 | |||
df81c84438 | |||
8b12395fca | |||
cb2b71d194 | |||
7ed508e2bb | |||
dad869967e | |||
2d5b3b554e | |||
74882116e3 | |||
a97c3bce3a | |||
1aa70827dc | |||
fe028860e9 | |||
a2d2ce4d38 | |||
167c11b9eb | |||
8cb3933fcc | |||
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 |
25
.github/workflows/nightly.yml
vendored
25
.github/workflows/nightly.yml
vendored
@ -38,4 +38,27 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-output-windows
|
||||
path: build/windows/x64/runner/Release
|
||||
path: build/windows/x64/runner/Release
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
cache: true
|
||||
- run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev
|
||||
sudo apt-get install libmpv-dev mpv
|
||||
sudo apt-get install libayatana-appindicator3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
- run: flutter pub get
|
||||
- run: flutter build linux
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-output-linux
|
||||
path: build/linux/x64/release/bundle
|
@ -26,7 +26,7 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:launchMode="singleInstance"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
|
@ -12,9 +12,9 @@ post {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"alias": "AteChip",
|
||||
"name": "Cat ate chips",
|
||||
"attachment_id": "d0b692cc64054463",
|
||||
"pack_id": 2
|
||||
"alias": "BaLoading",
|
||||
"name": "BaLoading",
|
||||
"attachment_id": "2JCI2uh21mKkfk9P",
|
||||
"pack_id": 3
|
||||
}
|
||||
}
|
||||
|
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
|
||||
}
|
@ -15,11 +15,11 @@ body:json {
|
||||
"client_id": "{{third_client_id}}",
|
||||
"client_secret":"{{third_client_tk}}",
|
||||
"type": "general",
|
||||
"subject": "Merry Christmas!",
|
||||
"subject": "新年快乐!",
|
||||
"subtitle": "一条来自 Solar Network 团队的信息",
|
||||
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
|
||||
"content": "今天是农历正月初一,小羊祝您新年快乐 🎉",
|
||||
"metadata": {
|
||||
"image": "6EqsYQwmFRCkbmhR"
|
||||
"image": "D2EDbcrsTugs3xk5"
|
||||
},
|
||||
"priority": 10
|
||||
}
|
||||
|
23
api/Passport/Developer Notify One User.bru
Normal file
23
api/Passport/Developer Notify One User.bru
Normal file
@ -0,0 +1,23 @@
|
||||
meta {
|
||||
name: Developer Notify One User
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/id/dev/notify/122
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"client_id": "{{third_client_id}}",
|
||||
"client_secret":"{{third_client_tk}}",
|
||||
"type": "general",
|
||||
"subject": "处理该帐号 @solian 的决定",
|
||||
"subtitle": "违反用户协议",
|
||||
"content": "您的帐号违反了我们用户协议中关于冒充我们官方的行为,至此做出停权的决定。还请见谅。该决定是最终决定,不接受上诉。",
|
||||
"priority": 10
|
||||
}
|
||||
}
|
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": 150,
|
||||
"payee_id": 18
|
||||
}
|
||||
}
|
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
|
||||
}
|
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,9 @@
|
||||
"screenAccountProfileEdit": "Edit Profile",
|
||||
"screenAbuseReport": "Abuse Reports",
|
||||
"screenSettings": "Settings",
|
||||
"screenAccountSettings": "Account Settings",
|
||||
"screenFactorSettings": "Auth Factors",
|
||||
"screenAccountWallet": "Wallet",
|
||||
"screenNews": "News",
|
||||
"screenAlbum": "Album",
|
||||
"screenChat": "Chat",
|
||||
@ -24,6 +27,7 @@
|
||||
"screenChatNew": "New Channel",
|
||||
"screenRealm": "Realm",
|
||||
"screenRealmManage": "Edit Realm",
|
||||
"screenRealmDiscovery": "Realm Discovery",
|
||||
"screenRealmNew": "New Realm",
|
||||
"screenNotification": "Notification",
|
||||
"screenPostSearch": "Search Posts",
|
||||
@ -104,8 +108,18 @@
|
||||
},
|
||||
"loginEnterPassword": "Enter the code",
|
||||
"loginSuccess": "Logged in as {}",
|
||||
"authFactorDelete": "Delete Auth Factor",
|
||||
"authFactorDeleteDescription": "Are you sure you want delete auth factor {}?",
|
||||
"authFactorPassword": "Password",
|
||||
"authFactorPasswordDescription": "The password you set when you registered.",
|
||||
"authFactorEmail": "Email verification code",
|
||||
"authFactorEmailDescription": "An one-time code sent to the email address you set when you registered.",
|
||||
"authFactorTOTP": "Time-based OTP",
|
||||
"authFactorTOTPDescription": "A one-time code generated by a TOTP authenticator such as Google Authenticator or Authy.",
|
||||
"authFactorInAppNotify": "In-app notification",
|
||||
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
|
||||
"authFactorAdd": "Add a factor",
|
||||
"authFactorAddSubtitle": "Provide another way to login your account.",
|
||||
"accountIntroTitle": "Hello there!",
|
||||
"accountIntroSubtitle": "Pick an option below to get started.",
|
||||
"accountLogout": "Logout",
|
||||
@ -114,8 +128,14 @@
|
||||
"accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.",
|
||||
"accountPublishers": "Your publishers",
|
||||
"accountPublishersSubtitle": "Manage your publish identities.",
|
||||
"accountSettings": "Account Settings",
|
||||
"accountSettingsSubtitle": "Manage your account and make it yours.",
|
||||
"accountProfileEdit": "Edit your profile",
|
||||
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
|
||||
"accountWallet": "Wallet",
|
||||
"accountWalletSubtitle": "View your balance and transactions.",
|
||||
"factorSettings": "Auth Factors",
|
||||
"factorSettingsSubtitle": "Manage your authentication factors.",
|
||||
"accountProfileEditApplied": "Profile modification applied.",
|
||||
"publishersNew": "New Publisher",
|
||||
"publisherNewSubtitle": "Create a new publisher identity.",
|
||||
@ -135,9 +155,12 @@
|
||||
"fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
|
||||
"writePostTypeStory": "Post a story",
|
||||
"writePostTypeArticle": "Write an article",
|
||||
"writePostTypeQuestion": "Ask a question",
|
||||
"writePostTypeVideo": "Post a video",
|
||||
"fieldPostPublisher": "Post publisher",
|
||||
"fieldPostContent": "What happened?!",
|
||||
"fieldPostTitle": "Title",
|
||||
"fieldPostQuestionReward": "Answer Rewards (Source Points)",
|
||||
"fieldPostDescription": "Description",
|
||||
"fieldPostTags": "Tags",
|
||||
"fieldPostCategories": "Categories",
|
||||
@ -147,9 +170,9 @@
|
||||
"postPosted": "Post has been posted.",
|
||||
"postPublishedAt": "Published At",
|
||||
"postPublishedUntil": "Published Until",
|
||||
"postEditingNotice": "You're about to editing a post that posted {}.",
|
||||
"postReplyingNotice": "You're about to reply to a post that posted {}.",
|
||||
"postRepostingNotice": "You're about to repost a post that posted {}.",
|
||||
"postEditingNotice": "You're about to editing a post that posted by {}.",
|
||||
"postReplyingNotice": "You're about to reply to a post that posted by {}.",
|
||||
"postRepostingNotice": "You're about to repost a post that posted by {}.",
|
||||
"postReact": "React",
|
||||
"postReactions": "Reactions of Post",
|
||||
"postReactionUpvote": {
|
||||
@ -180,6 +203,9 @@
|
||||
"other": "{} comments"
|
||||
},
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsDisplayLanguage": "Display Language",
|
||||
"settingsDisplayLanguageDescription": "Set the application language.",
|
||||
"settingsDisplayLanguageSystem": "Follow System",
|
||||
"settingsAppBarTransparent": "Transparent App Bar",
|
||||
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
|
||||
"settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
|
||||
@ -219,6 +245,8 @@
|
||||
"settingsMisc": "Misc",
|
||||
"settingsMiscAbout": "About",
|
||||
"settingsMiscAboutDescription": "View the version information of Solian.",
|
||||
"settingsAccountLanguage": "Account Language",
|
||||
"settingsAccountLanguageDescription": "Set the language for email, notification, and other account-related content.",
|
||||
"sensitiveContent": "Sensitive Content",
|
||||
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
||||
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
||||
@ -532,11 +560,15 @@
|
||||
"postImageShareAds": "Explore posts on the Solar Network",
|
||||
"postShare": "Share",
|
||||
"postShareImage": "Share via Image",
|
||||
"postGetInsight": "Get Insight",
|
||||
"postGetInsightTitle": "AI Insight",
|
||||
"postGetInsightDescription": "AI may make mistakes, check important information.",
|
||||
"appInitializing": "Initializing",
|
||||
"poweredBy": "Powered by {}",
|
||||
"shareIntent": "Share",
|
||||
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
||||
"shareIntentPostStory": "Post a Story",
|
||||
"shareIntentSendChannel": "Share to Channel",
|
||||
"updateAvailable": "Update Available",
|
||||
"updateOngoing": "Updating, please wait...",
|
||||
"custom": "Custom",
|
||||
@ -549,6 +581,7 @@
|
||||
"colorSchemeWhite": "White",
|
||||
"colorSchemeBlack": "Black",
|
||||
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
||||
"postFeaturedComment": "Featured Comment",
|
||||
"postCategoryTechnology": "Technology",
|
||||
"postCategoryGaming": "Gaming",
|
||||
"postCategoryLife": "Life",
|
||||
@ -565,5 +598,57 @@
|
||||
"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"
|
||||
"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",
|
||||
"postQuestionUnanswered": "Unanswered Question",
|
||||
"postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
|
||||
"postQuestionAnswered": "Answered Question",
|
||||
"postQuestionAnswerSelect": "Select as Answer",
|
||||
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
|
||||
"postVideoUpload": "Upload Video",
|
||||
"realmJoin": "Join Realm",
|
||||
"realmCommunityHint": "This realm is a community realm, you can freely join.",
|
||||
"realmCommunityPublicChannelsHint": "The public channels in this realm",
|
||||
"realmJoined": "Joined realm {}.",
|
||||
"join": "Join",
|
||||
"pollEditorNew": "New Poll",
|
||||
"pollEditorEdit": "Edit Poll",
|
||||
"pollEditorDelete": "Delete Poll",
|
||||
"pollEditorDeleteDescription": "Are you sure you want to delete this poll? This operation is irreversible.",
|
||||
"pollEditorUnlink": "Unlink Poll",
|
||||
"pollOptionAdd": "Add Option",
|
||||
"pollOptionName": "Option Name",
|
||||
"pollLinkExisting": "Link existing poll",
|
||||
"pollAnswered": "Answered the poll.",
|
||||
"pollVotes": {
|
||||
"one": "{} vote",
|
||||
"other": "{} votes"
|
||||
},
|
||||
"publisherDelete": "Delete Publisher {}",
|
||||
"publisherDeleteDescription": "Are you sure you want to delete this publisher? This operation is irreversible.",
|
||||
"channelIsPublic": "Public Channel",
|
||||
"channelIsPublicDescription": "The channel is public, anyone can join.",
|
||||
"channelIsCommunity": "Community Channel",
|
||||
"channelIsCommunityDescription": "Currently, community channel has nothing special yet.",
|
||||
"realmIsPublic": "Public Realm",
|
||||
"realmIsPublicDescription": "The realm is public, anyone can join.",
|
||||
"realmIsCommunity": "Community Realm",
|
||||
"realmIsCommunityDescription": "Community realm will be displayed on the discover page.",
|
||||
"realmLeave": "Leave Realm",
|
||||
"realmLeaveDescription": "Leave the current realm and delete the realm's identity."
|
||||
}
|
||||
|
@ -15,6 +15,9 @@
|
||||
"screenAccountProfileEdit": "编辑资料",
|
||||
"screenAbuseReport": "滥用检举",
|
||||
"screenSettings": "设置",
|
||||
"screenAccountSettings": "账号设置",
|
||||
"screenFactorSettings": "验证因子",
|
||||
"screenAccountWallet": "钱包",
|
||||
"screenNews": "新闻",
|
||||
"screenAlbum": "相册",
|
||||
"screenChat": "聊天",
|
||||
@ -22,6 +25,7 @@
|
||||
"screenChatNew": "新建聊天频道",
|
||||
"screenRealm": "领域",
|
||||
"screenRealmManage": "编辑领域",
|
||||
"screenRealmDiscovery": "发现领域",
|
||||
"screenRealmNew": "新建领域",
|
||||
"screenNotification": "通知",
|
||||
"screenPostSearch": "搜索帖子",
|
||||
@ -88,8 +92,18 @@
|
||||
},
|
||||
"loginEnterPassword": "验证代码",
|
||||
"loginSuccess": "登录为 {}",
|
||||
"authFactorDelete": "删除验证因子",
|
||||
"authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
|
||||
"authFactorPassword": "密码",
|
||||
"authFactorPasswordDescription": "注册时选择设置的密码。",
|
||||
"authFactorEmail": "电邮一次性验证码",
|
||||
"authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
|
||||
"authFactorTOTP": "时序验证码",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
|
||||
"authFactorInAppNotify": "应用内通知验证码",
|
||||
"authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
|
||||
"authFactorAdd": "添加新验证因子",
|
||||
"authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
|
||||
"accountIntroTitle": "喜欢您来!",
|
||||
"accountIntroSubtitle": "登陆以探索更广大的世界。",
|
||||
"accountLogout": "退出登录",
|
||||
@ -98,8 +112,14 @@
|
||||
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
|
||||
"accountPublishers": "你的发布者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帐号设置",
|
||||
"accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
|
||||
"accountProfileEdit": "编辑资料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
|
||||
"accountWallet": "钱包",
|
||||
"accountWalletSubtitle": "查看你的余额和交易记录。",
|
||||
"factorSettings": "验证因子",
|
||||
"factorSettingsSubtitle": "管理你的登陆验证方式。",
|
||||
"accountProfileEditApplied": "个人资料修改已被应用。",
|
||||
"publishersNew": "新发布者",
|
||||
"publisherNewSubtitle": "创建一个新的公共身份。",
|
||||
@ -119,9 +139,12 @@
|
||||
"fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
|
||||
"writePostTypeStory": "发动态",
|
||||
"writePostTypeArticle": "写文章",
|
||||
"writePostTypeQuestion": "提问题",
|
||||
"writePostTypeVideo": "发视频",
|
||||
"fieldPostPublisher": "帖子发布者",
|
||||
"fieldPostContent": "发生什么事了?!",
|
||||
"fieldPostTitle": "标题",
|
||||
"fieldPostQuestionReward": "回答奖励源点",
|
||||
"fieldPostDescription": "描述",
|
||||
"fieldPostTags": "标签",
|
||||
"fieldPostCategories": "分类",
|
||||
@ -178,6 +201,9 @@
|
||||
"other": "{} 条评论"
|
||||
},
|
||||
"settingsAppearance": "外观",
|
||||
"settingsDisplayLanguage": "显示语言",
|
||||
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
|
||||
"settingsDisplayLanguageSystem": "跟随系统",
|
||||
"settingsBackgroundImage": "背景图片",
|
||||
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
|
||||
"settingsBackgroundImageClear": "清除现存背景图",
|
||||
@ -217,6 +243,8 @@
|
||||
"settingsMisc": "杂项",
|
||||
"settingsMiscAbout": "关于",
|
||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||
"settingsAccountLanguage": "帐号偏好语言",
|
||||
"settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。",
|
||||
"sensitiveContent": "敏感内容",
|
||||
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
||||
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
||||
@ -530,11 +558,15 @@
|
||||
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖图",
|
||||
"postGetInsight": "获取见解",
|
||||
"postGetInsightTitle": "AI 见解",
|
||||
"postGetInsightDescription": "AI 可能会出错,检查信息真实性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
"shareIntentDescription": "您想对您分享的内容做些什么?",
|
||||
"shareIntentPostStory": "发布动态",
|
||||
"shareIntentSendChannel": "分享到聊天频道",
|
||||
"updateAvailable": "检测到更新可用",
|
||||
"updateOngoing": "正在更新,请稍后……",
|
||||
"custom": "自定义",
|
||||
@ -547,6 +579,7 @@
|
||||
"colorSchemeWhite": "白色",
|
||||
"colorSchemeBlack": "黑色",
|
||||
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
||||
"postFeaturedComment": "精选评论",
|
||||
"postCategoryTechnology": "技术",
|
||||
"postCategoryGaming": "游戏",
|
||||
"postCategoryLife": "生活",
|
||||
@ -563,5 +596,58 @@
|
||||
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
|
||||
"newsReadingFromOriginal": "你正在阅读原始文章",
|
||||
"newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。",
|
||||
"newsToday": "快讯"
|
||||
"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": "退出",
|
||||
"postQuestionUnanswered": "未解答的问题",
|
||||
"postQuestionUnansweredWithReward": "未解答的问题,悬赏源点 {}",
|
||||
"postQuestionAnswered": "已解答的问题",
|
||||
"postQuestionAnswerTitle": "精选解答",
|
||||
"postQuestionAnswerSelect": "选择解答",
|
||||
"postQuestionAnswerSelected": "解答已选择,奖励已发放。",
|
||||
"postVideoUpload": "上传视频",
|
||||
"realmJoin": "加入领域",
|
||||
"realmCommunityHint": "该领域是一个社区领域,你可以自由加入。",
|
||||
"realmCommunityPublicChannelsHint": "该领域包含的公共频道",
|
||||
"realmJoined": "已加入领域 {}。",
|
||||
"join": "加入",
|
||||
"pollEditorNew": "新投票",
|
||||
"pollEditorEdit": "编辑投票",
|
||||
"pollEditorDelete": "删除投票",
|
||||
"pollEditorDeleteDescription": "你确定要删除这个投票吗?该操作不可撤销。",
|
||||
"pollEditorUnlink": "解除链接",
|
||||
"pollOptionAdd": "添加选项",
|
||||
"pollOptionName": "选项名称",
|
||||
"pollLinkExisting": "链接现有投票",
|
||||
"pollAnswered": "答案已经反馈。",
|
||||
"pollVotes": {
|
||||
"one": "{} 票",
|
||||
"other": "{} 票"
|
||||
},
|
||||
"publisherDelete": "删除发布者 {}",
|
||||
"publisherDeleteDescription": "你确定要删除这个发布者吗?该操作不可撤销。",
|
||||
"channelIsPublic": "公开频道",
|
||||
"channelIsPublicDescription": "该频道是公开的,任何人都可以加入。",
|
||||
"channelIsCommunity": "社区频道",
|
||||
"channelIsCommunityDescription": "目前来说,社区频道还没有什么特别之处。",
|
||||
"realmIsPublic": "公开领域",
|
||||
"realmIsPublicDescription": "该领域是公开的,任何人都可以加入。",
|
||||
"realmIsCommunity": "社区领域",
|
||||
"realmIsCommunityDescription": "社区领域会显示在发现页面上。",
|
||||
"realmLeave": "离开领域",
|
||||
"realmLeaveDescription": "离开当前领域,并且删除领域中的身份。"
|
||||
}
|
||||
|
@ -15,6 +15,9 @@
|
||||
"screenAccountProfileEdit": "編輯資料",
|
||||
"screenAbuseReport": "濫用檢舉",
|
||||
"screenSettings": "設置",
|
||||
"screenAccountSettings": "賬號設置",
|
||||
"screenFactorSettings": "驗證因子",
|
||||
"screenAccountWallet": "錢包",
|
||||
"screenNews": "新聞",
|
||||
"screenAlbum": "相冊",
|
||||
"screenChat": "聊天",
|
||||
@ -22,6 +25,7 @@
|
||||
"screenChatNew": "新建聊天頻道",
|
||||
"screenRealm": "領域",
|
||||
"screenRealmManage": "編輯領域",
|
||||
"screenRealmDiscovery": "發現領域",
|
||||
"screenRealmNew": "新建領域",
|
||||
"screenNotification": "通知",
|
||||
"screenPostSearch": "搜索帖子",
|
||||
@ -88,8 +92,18 @@
|
||||
},
|
||||
"loginEnterPassword": "驗證代碼",
|
||||
"loginSuccess": "登錄為 {}",
|
||||
"authFactorDelete": "刪除驗證因子",
|
||||
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||
"authFactorPassword": "密碼",
|
||||
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||
"authFactorEmail": "電郵一次性驗證碼",
|
||||
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||
"authFactorTOTP": "時序驗證碼",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||
"authFactorAdd": "添加新驗證因子",
|
||||
"authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。",
|
||||
"accountIntroTitle": "喜歡您來!",
|
||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||
"accountLogout": "退出登錄",
|
||||
@ -98,8 +112,14 @@
|
||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||
"accountPublishers": "你的發佈者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帳號設置",
|
||||
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||
"accountProfileEdit": "編輯資料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
|
||||
"accountWallet": "錢包",
|
||||
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||
"factorSettings": "驗證因子",
|
||||
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||
"publishersNew": "新發布者",
|
||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||
@ -119,9 +139,12 @@
|
||||
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
||||
"writePostTypeStory": "發動態",
|
||||
"writePostTypeArticle": "寫文章",
|
||||
"writePostTypeQuestion": "提問題",
|
||||
"writePostTypeVideo": "發視頻",
|
||||
"fieldPostPublisher": "帖子發佈者",
|
||||
"fieldPostContent": "發生什麼事了?!",
|
||||
"fieldPostTitle": "標題",
|
||||
"fieldPostQuestionReward": "回答獎勵源點",
|
||||
"fieldPostDescription": "描述",
|
||||
"fieldPostTags": "標籤",
|
||||
"fieldPostCategories": "分類",
|
||||
@ -178,6 +201,9 @@
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsDisplayLanguage": "顯示語言",
|
||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||
@ -217,6 +243,8 @@
|
||||
"settingsMisc": "雜項",
|
||||
"settingsMiscAbout": "關於",
|
||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||
"settingsAccountLanguage": "帳號偏好語言",
|
||||
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
|
||||
"sensitiveContent": "敏感內容",
|
||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||
@ -530,11 +558,15 @@
|
||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖圖",
|
||||
"postGetInsight": "獲取見解",
|
||||
"postGetInsightTitle": "AI 見解",
|
||||
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||
"shareIntentPostStory": "發佈動態",
|
||||
"shareIntentSendChannel": "分享到聊天頻道",
|
||||
"updateAvailable": "檢測到更新可用",
|
||||
"updateOngoing": "正在更新,請稍後……",
|
||||
"custom": "自定義",
|
||||
@ -547,6 +579,7 @@
|
||||
"colorSchemeWhite": "白色",
|
||||
"colorSchemeBlack": "黑色",
|
||||
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
||||
"postFeaturedComment": "精選評論",
|
||||
"postCategoryTechnology": "技術",
|
||||
"postCategoryGaming": "遊戲",
|
||||
"postCategoryLife": "生活",
|
||||
@ -563,5 +596,33 @@
|
||||
"newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
|
||||
"newsReadingFromOriginal": "你正在閲讀原始文章",
|
||||
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
|
||||
"newsToday": "快訊"
|
||||
"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": "退出",
|
||||
"postQuestionUnanswered": "未解答的問題",
|
||||
"postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
|
||||
"postQuestionAnswered": "已解答的問題",
|
||||
"postQuestionAnswerTitle": "精選解答",
|
||||
"postQuestionAnswerSelect": "選擇解答",
|
||||
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
|
||||
"postVideoUpload": "上傳視頻",
|
||||
"realmJoin": "加入領域",
|
||||
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
|
||||
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
|
||||
"realmJoined": "已加入領域 {}。",
|
||||
"join": "加入"
|
||||
}
|
||||
|
@ -15,6 +15,9 @@
|
||||
"screenAccountProfileEdit": "編輯資料",
|
||||
"screenAbuseReport": "濫用檢舉",
|
||||
"screenSettings": "設置",
|
||||
"screenAccountSettings": "賬號設置",
|
||||
"screenFactorSettings": "驗證因子",
|
||||
"screenAccountWallet": "錢包",
|
||||
"screenNews": "新聞",
|
||||
"screenAlbum": "相冊",
|
||||
"screenChat": "聊天",
|
||||
@ -22,6 +25,7 @@
|
||||
"screenChatNew": "新建聊天頻道",
|
||||
"screenRealm": "領域",
|
||||
"screenRealmManage": "編輯領域",
|
||||
"screenRealmDiscovery": "發現領域",
|
||||
"screenRealmNew": "新建領域",
|
||||
"screenNotification": "通知",
|
||||
"screenPostSearch": "搜索帖子",
|
||||
@ -88,8 +92,18 @@
|
||||
},
|
||||
"loginEnterPassword": "驗證代碼",
|
||||
"loginSuccess": "登錄為 {}",
|
||||
"authFactorDelete": "刪除驗證因子",
|
||||
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||
"authFactorPassword": "密碼",
|
||||
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||
"authFactorEmail": "電郵一次性驗證碼",
|
||||
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||
"authFactorTOTP": "時序驗證碼",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||
"authFactorAdd": "添加新驗證因子",
|
||||
"authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。",
|
||||
"accountIntroTitle": "喜歡您來!",
|
||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||
"accountLogout": "退出登錄",
|
||||
@ -98,8 +112,14 @@
|
||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||
"accountPublishers": "你的發佈者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帳號設置",
|
||||
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||
"accountProfileEdit": "編輯資料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
|
||||
"accountWallet": "錢包",
|
||||
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||
"factorSettings": "驗證因子",
|
||||
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||
"publishersNew": "新發布者",
|
||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||
@ -119,9 +139,12 @@
|
||||
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
||||
"writePostTypeStory": "發動態",
|
||||
"writePostTypeArticle": "寫文章",
|
||||
"writePostTypeQuestion": "提問題",
|
||||
"writePostTypeVideo": "發視頻",
|
||||
"fieldPostPublisher": "帖子發佈者",
|
||||
"fieldPostContent": "發生什麼事了?!",
|
||||
"fieldPostTitle": "標題",
|
||||
"fieldPostQuestionReward": "回答獎勵源點",
|
||||
"fieldPostDescription": "描述",
|
||||
"fieldPostTags": "標籤",
|
||||
"fieldPostCategories": "分類",
|
||||
@ -178,6 +201,9 @@
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsDisplayLanguage": "顯示語言",
|
||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||
@ -217,6 +243,8 @@
|
||||
"settingsMisc": "雜項",
|
||||
"settingsMiscAbout": "關於",
|
||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||
"settingsAccountLanguage": "帳號偏好語言",
|
||||
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
|
||||
"sensitiveContent": "敏感內容",
|
||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||
@ -530,11 +558,15 @@
|
||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖圖",
|
||||
"postGetInsight": "獲取見解",
|
||||
"postGetInsightTitle": "AI 見解",
|
||||
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||
"shareIntentPostStory": "發佈動態",
|
||||
"shareIntentSendChannel": "分享到聊天頻道",
|
||||
"updateAvailable": "檢測到更新可用",
|
||||
"updateOngoing": "正在更新,請稍後……",
|
||||
"custom": "自定義",
|
||||
@ -547,6 +579,7 @@
|
||||
"colorSchemeWhite": "白色",
|
||||
"colorSchemeBlack": "黑色",
|
||||
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
||||
"postFeaturedComment": "精選評論",
|
||||
"postCategoryTechnology": "技術",
|
||||
"postCategoryGaming": "遊戲",
|
||||
"postCategoryLife": "生活",
|
||||
@ -563,5 +596,33 @@
|
||||
"newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章",
|
||||
"newsReadingFromOriginal": "你正在閱讀原始文章",
|
||||
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
|
||||
"newsToday": "快訊"
|
||||
"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": "退出",
|
||||
"postQuestionUnanswered": "未解答的問題",
|
||||
"postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
|
||||
"postQuestionAnswered": "已解答的問題",
|
||||
"postQuestionAnswerTitle": "精選解答",
|
||||
"postQuestionAnswerSelect": "選擇解答",
|
||||
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
|
||||
"postVideoUpload": "上傳視頻",
|
||||
"realmJoin": "加入領域",
|
||||
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
|
||||
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
|
||||
"realmJoined": "已加入領域 {}。",
|
||||
"join": "加入"
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ PODS:
|
||||
- Alamofire (5.10.2)
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- croppy (0.0.1):
|
||||
- Flutter
|
||||
- device_info_plus (0.0.1):
|
||||
@ -43,58 +42,58 @@ PODS:
|
||||
- Flutter
|
||||
- file_saver (0.0.1):
|
||||
- Flutter
|
||||
- Firebase/Analytics (11.6.0):
|
||||
- Firebase/Analytics (11.7.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (11.6.0):
|
||||
- Firebase/Core (11.7.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 11.6.0)
|
||||
- Firebase/CoreOnly (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- Firebase/Messaging (11.6.0):
|
||||
- FirebaseAnalytics (~> 11.7.0)
|
||||
- Firebase/CoreOnly (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- Firebase/Messaging (11.7.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.6.0)
|
||||
- firebase_analytics (11.4.0):
|
||||
- Firebase/Analytics (= 11.6.0)
|
||||
- FirebaseMessaging (~> 11.7.0)
|
||||
- firebase_analytics (11.4.2):
|
||||
- Firebase/Analytics (= 11.7.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_core (3.10.0):
|
||||
- Firebase/CoreOnly (= 11.6.0)
|
||||
- firebase_core (3.11.0):
|
||||
- Firebase/CoreOnly (= 11.7.0)
|
||||
- Flutter
|
||||
- firebase_messaging (15.2.0):
|
||||
- Firebase/Messaging (= 11.6.0)
|
||||
- firebase_messaging (15.2.2):
|
||||
- Firebase/Messaging (= 11.7.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseAnalytics (11.6.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseAnalytics (11.7.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.7.0)
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleAppMeasurement (= 11.6.0)
|
||||
- GoogleAppMeasurement (= 11.7.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (11.6.0):
|
||||
- FirebaseCoreInternal (~> 11.6.0)
|
||||
- FirebaseCore (11.7.0):
|
||||
- FirebaseCoreInternal (~> 11.7.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- FirebaseCoreInternal (11.6.0):
|
||||
- FirebaseCoreInternal (11.7.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseInstallations (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseMessaging (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -123,21 +122,21 @@ PODS:
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleAppMeasurement (11.6.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
|
||||
- GoogleAppMeasurement (11.7.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.7.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.6.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.7.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.7.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
@ -179,8 +178,8 @@ PODS:
|
||||
- Flutter
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- Kingfisher (8.1.3)
|
||||
- livekit_client (2.3.5):
|
||||
- Kingfisher (8.2.0)
|
||||
- livekit_client (2.3.6):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@ -237,7 +236,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- Alamofire
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
@ -300,7 +299,7 @@ SPEC REPOS:
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
croppy:
|
||||
:path: ".symlinks/plugins/croppy/ios"
|
||||
device_info_plus:
|
||||
@ -374,22 +373,22 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
|
||||
firebase_analytics: 07bd7cfbac54bfcdccf2bb2530f9a65486f7ef3f
|
||||
firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10
|
||||
firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c
|
||||
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
|
||||
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
|
||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
||||
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
|
||||
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
|
||||
Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4
|
||||
firebase_analytics: 7236e6115c1b4e62c2270faa29c052a317e31107
|
||||
firebase_core: aa979ae726f00b3ef4ccf59dfb96170af84efbd4
|
||||
firebase_messaging: 3af84b6a90aeac4d7a67fbf4c43a91e7083bea1f
|
||||
FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec
|
||||
FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4
|
||||
FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881
|
||||
FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9
|
||||
FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
@ -397,14 +396,14 @@ SPEC CHECKSUMS:
|
||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
|
||||
GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
|
||||
livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
|
||||
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
|
||||
livekit_client: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
|
@ -71,7 +71,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
resp.data as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
_wsSubscription = _ws.stream.stream.listen((event) {
|
||||
_wsSubscription = _ws.pk.stream.listen((event) {
|
||||
switch (event.method) {
|
||||
case 'events.new':
|
||||
if (event.payload?['channel_id'] != channel?.id) break;
|
||||
|
@ -16,6 +16,7 @@ import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
@ -144,6 +145,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
static const Map<String, String> kTitleMap = {
|
||||
'stories': 'writePostTypeStory',
|
||||
'articles': 'writePostTypeArticle',
|
||||
'questions': 'writePostTypeQuestion',
|
||||
'videos': 'writePostTypeVideo',
|
||||
};
|
||||
|
||||
static const kAttachmentProgressWeight = 0.9;
|
||||
@ -153,6 +156,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
final TextEditingController titleController = TextEditingController();
|
||||
final TextEditingController descriptionController = TextEditingController();
|
||||
final TextEditingController aliasController = TextEditingController();
|
||||
final TextEditingController rewardController = TextEditingController();
|
||||
|
||||
bool _temporarySaveActive = false;
|
||||
|
||||
@ -168,6 +172,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
});
|
||||
contentController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
});
|
||||
if (doLoadFromTemporary) _temporaryLoad();
|
||||
}
|
||||
@ -194,6 +199,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
PostWriteMedia? thumbnail;
|
||||
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||
DateTime? publishedAt, publishedUntil;
|
||||
SnAttachment? videoAttachment;
|
||||
SnPoll? poll;
|
||||
|
||||
Future<void> fetchRelatedPost(
|
||||
BuildContext context, {
|
||||
@ -214,6 +221,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
descriptionController.text = post.body['description'] ?? '';
|
||||
contentController.text = post.body['content'] ?? '';
|
||||
aliasController.text = post.alias ?? '';
|
||||
rewardController.text = post.body['reward']?.toString() ?? '';
|
||||
videoAttachment = post.preload?.video;
|
||||
publishedAt = post.publishedAt;
|
||||
publishedUntil = post.publishedUntil;
|
||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||
@ -222,6 +231,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
||||
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||
poll = post.preload?.poll;
|
||||
|
||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||
thumbnail = PostWriteMedia(post.preload!.thumbnail);
|
||||
@ -347,6 +357,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
||||
if (rewardController.text.isNotEmpty) 'reward': rewardController.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),
|
||||
@ -359,6 +370,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
||||
if (poll != null) 'poll': poll!.toJson(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -375,6 +387,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
aliasController.text = data['alias'] ?? '';
|
||||
titleController.text = data['title'] ?? '';
|
||||
descriptionController.text = data['description'] ?? '';
|
||||
rewardController.text = data['reward']?.toString() ?? '';
|
||||
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||
attachments
|
||||
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
|
||||
@ -387,6 +400,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
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;
|
||||
poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null;
|
||||
temporaryRestored = true;
|
||||
notifyListeners();
|
||||
});
|
||||
@ -473,6 +487,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
progress = kAttachmentProgressWeight;
|
||||
notifyListeners();
|
||||
|
||||
final reward = double.tryParse(rewardController.text);
|
||||
|
||||
// Posting the content
|
||||
try {
|
||||
final baseProgressVal = progress!;
|
||||
@ -498,6 +514,9 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
||||
if (reward != null) 'reward': reward,
|
||||
if (videoAttachment != null) 'video': videoAttachment!.rid,
|
||||
if (poll != null) 'poll': poll!.id,
|
||||
},
|
||||
onSendProgress: (count, total) {
|
||||
progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
||||
@ -624,6 +643,16 @@ class PostWriteController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVideoAttachment(SnAttachment? value) {
|
||||
videoAttachment = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPoll(SnPoll? value) {
|
||||
poll = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
publishedAt = null;
|
||||
publishedUntil = null;
|
||||
|
119
lib/main.dart
119
lib/main.dart
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:croppy/croppy.dart';
|
||||
@ -10,8 +11,10 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.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:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
@ -40,9 +43,12 @@ import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
import 'package:image_picker_android/image_picker_android.dart';
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void appBackgroundDispatcher() {
|
||||
@ -103,6 +109,13 @@ void main() async {
|
||||
}
|
||||
}
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
final ImagePickerPlatform imagePickerImplementation = ImagePickerPlatform.instance;
|
||||
if (imagePickerImplementation is ImagePickerAndroid) {
|
||||
imagePickerImplementation.useAndroidPhotoPicker = true;
|
||||
}
|
||||
}
|
||||
|
||||
runApp(const SolianApp());
|
||||
}
|
||||
|
||||
@ -156,8 +169,8 @@ class SolianApp extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
breakpoints: [
|
||||
const Breakpoint(start: 0, end: 450, name: MOBILE),
|
||||
const Breakpoint(start: 451, end: 800, name: TABLET),
|
||||
const Breakpoint(start: 0, end: 600, name: MOBILE),
|
||||
const Breakpoint(start: 601, end: 800, name: TABLET),
|
||||
const Breakpoint(start: 801, end: 1920, name: DESKTOP),
|
||||
],
|
||||
);
|
||||
@ -206,7 +219,7 @@ class _AppSplashScreen extends StatefulWidget {
|
||||
State<_AppSplashScreen> createState() => _AppSplashScreenState();
|
||||
}
|
||||
|
||||
class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
void _tryRequestRating() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.containsKey('first_boot_time')) {
|
||||
@ -281,6 +294,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
final notify = context.read<NotificationProvider>();
|
||||
notify.listen();
|
||||
await notify.registerPushNotifications();
|
||||
if (!mounted) return;
|
||||
final sticker = context.read<SnStickerProvider>();
|
||||
await sticker.listStickerEagerly();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
await context.showErrorDialog(err);
|
||||
@ -291,9 +307,62 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
||||
_appLifecycleListener = AppLifecycleListener(
|
||||
onExitRequested: _onExitRequested,
|
||||
);
|
||||
}
|
||||
|
||||
_trayInitialization();
|
||||
_hotkeyInitialization();
|
||||
_initialize().then((_) {
|
||||
_postInitialization();
|
||||
_tryRequestRating();
|
||||
@ -301,6 +370,50 @@ 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
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
@ -12,6 +12,7 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/notification.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
|
||||
class NotificationProvider extends ChangeNotifier {
|
||||
late final SnNetworkProvider _sn;
|
||||
@ -71,22 +72,48 @@ class NotificationProvider extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
int showingCount = 0;
|
||||
int showingTrayCount = 0;
|
||||
List<SnNotification> notifications = List.empty(growable: true);
|
||||
|
||||
void listen() {
|
||||
_ws.stream.stream.listen((event) {
|
||||
_ws.pk.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.lightImpact();
|
||||
if (doHaptic) HapticFeedback.mediumImpact();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void clearTray() {
|
||||
showingTrayCount = 0;
|
||||
updateTray();
|
||||
}
|
||||
|
||||
void updateTray() {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||
if (showingTrayCount == 0) {
|
||||
trayManager.setTitle('');
|
||||
} else {
|
||||
trayManager.setTitle(' $showingTrayCount');
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
showingCount = 0;
|
||||
notifications.clear();
|
||||
updateTray();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
|
||||
class SnPostContentProvider {
|
||||
@ -16,6 +17,11 @@ class SnPostContentProvider {
|
||||
_attach = context.read<SnAttachmentProvider>();
|
||||
}
|
||||
|
||||
Future<SnPoll> _fetchPoll(int id) async {
|
||||
final resp = await _sn.client.get('/cgi/co/polls/$id');
|
||||
return SnPoll.fromJson(resp.data);
|
||||
}
|
||||
|
||||
Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
|
||||
Set<String> rids = {};
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
@ -23,6 +29,9 @@ class SnPostContentProvider {
|
||||
if (out[i].body['thumbnail'] != null) {
|
||||
rids.add(out[i].body['thumbnail']);
|
||||
}
|
||||
if (out[i].body['video'] != null) {
|
||||
rids.add(out[i].body['video']);
|
||||
}
|
||||
if (out[i].repostTo != null) {
|
||||
out[i] = out[i].copyWith(
|
||||
repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
|
||||
@ -32,10 +41,17 @@ class SnPostContentProvider {
|
||||
|
||||
final attachments = await _attach.getMultiple(rids.toList());
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
SnPoll? poll;
|
||||
if (out[i].pollId != null) {
|
||||
poll = await _fetchPoll(out[i].pollId!);
|
||||
}
|
||||
|
||||
out[i] = out[i].copyWith(
|
||||
preload: SnPostPreload(
|
||||
thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
|
||||
attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
||||
video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull,
|
||||
poll: poll,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -53,6 +69,9 @@ class SnPostContentProvider {
|
||||
if (out.body['thumbnail'] != null) {
|
||||
rids.add(out.body['thumbnail']);
|
||||
}
|
||||
if (out.body['video'] != null) {
|
||||
rids.add(out.body['video']);
|
||||
}
|
||||
if (out.repostTo != null) {
|
||||
out = out.copyWith(
|
||||
repostTo: await _preloadRelatedDataSingle(out.repostTo!),
|
||||
@ -60,10 +79,18 @@ class SnPostContentProvider {
|
||||
}
|
||||
|
||||
final attachments = await _attach.getMultiple(rids.toList());
|
||||
|
||||
SnPoll? poll;
|
||||
if (out.pollId != null) {
|
||||
poll = await _fetchPoll(out.pollId!);
|
||||
}
|
||||
|
||||
out = out.copyWith(
|
||||
preload: SnPostPreload(
|
||||
thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
|
||||
attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
||||
video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull,
|
||||
poll: poll,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -9,6 +9,10 @@ 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>();
|
||||
}
|
||||
@ -17,6 +21,12 @@ class SnStickerProvider {
|
||||
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];
|
||||
@ -25,7 +35,7 @@ class SnStickerProvider {
|
||||
try {
|
||||
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
|
||||
final sticker = SnSticker.fromJson(resp.data);
|
||||
_cache[alias] = sticker;
|
||||
_cacheSticker(sticker);
|
||||
|
||||
return sticker;
|
||||
} catch (err) {
|
||||
@ -35,4 +45,30 @@ class SnStickerProvider {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,4 +53,11 @@ class UserProvider extends ChangeNotifier {
|
||||
user = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setLanguage(String? value) {
|
||||
if (value == null) return;
|
||||
if (user == null) return;
|
||||
user = user!.copyWith(language: value);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,8 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
late final SnNetworkProvider _sn;
|
||||
late final UserProvider _ua;
|
||||
|
||||
StreamController<WebSocketPackage> stream = StreamController.broadcast();
|
||||
StreamController<WebSocketPackage> pk = StreamController.broadcast();
|
||||
Stream<dynamic>? _wsStream;
|
||||
|
||||
WebSocketProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
@ -33,23 +34,33 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
await connect();
|
||||
}
|
||||
|
||||
Completer<void>? _connectCompleter;
|
||||
|
||||
Future<void> connect({noRetry = false}) async {
|
||||
if (_connectCompleter != null) {
|
||||
await _connectCompleter!.future;
|
||||
_connectCompleter = null;
|
||||
}
|
||||
|
||||
if (!_ua.isAuthorized) return;
|
||||
if (isConnected || conn != null) {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
final atk = await _sn.getFreshAtk();
|
||||
final uri = Uri.parse(
|
||||
'${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk',
|
||||
);
|
||||
|
||||
isBusy = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_connectCompleter = Completer<void>();
|
||||
|
||||
final atk = await _sn.getFreshAtk();
|
||||
final uri = Uri.parse(
|
||||
'${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk',
|
||||
);
|
||||
|
||||
isBusy = true;
|
||||
notifyListeners();
|
||||
|
||||
conn = WebSocketChannel.connect(uri);
|
||||
await conn!.ready;
|
||||
_wsStream = conn!.stream.asBroadcastStream();
|
||||
listen();
|
||||
log('[WebSocket] Connected to server!');
|
||||
isConnected = true;
|
||||
@ -70,6 +81,8 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
} finally {
|
||||
isBusy = false;
|
||||
notifyListeners();
|
||||
_connectCompleter!.complete();
|
||||
_connectCompleter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,11 +96,12 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void listen() {
|
||||
conn?.stream.listen(
|
||||
if (_wsStream == null) return;
|
||||
_wsStream!.listen(
|
||||
(event) {
|
||||
final packet = WebSocketPackage.fromJson(jsonDecode(event));
|
||||
log('Websocket incoming message: ${packet.method} ${packet.message}');
|
||||
stream.sink.add(packet);
|
||||
pk.sink.add(packet);
|
||||
},
|
||||
onDone: () {
|
||||
isConnected = false;
|
||||
|
@ -47,6 +47,7 @@ class HomeWidgetProvider {
|
||||
}
|
||||
|
||||
Future<void> widgetUpdateRandomPost() async {
|
||||
if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) return;
|
||||
final snc = await SnNetworkProvider.createOffContextClient();
|
||||
final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1');
|
||||
final post = SnPost.fromJson(resp.data['data'][0]);
|
||||
|
123
lib/router.dart
123
lib/router.dart
@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:surface/screens/abuse_report.dart';
|
||||
import 'package:surface/screens/account.dart';
|
||||
import 'package:surface/screens/account/account_settings.dart';
|
||||
import 'package:surface/screens/account/factor_settings.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
import 'package:surface/screens/account/profile_edit.dart';
|
||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||
@ -29,8 +31,10 @@ import 'package:surface/screens/post/post_search.dart';
|
||||
import 'package:surface/screens/realm.dart';
|
||||
import 'package:surface/screens/realm/manage.dart';
|
||||
import 'package:surface/screens/realm/realm_detail.dart';
|
||||
import 'package:surface/screens/realm/realm_discovery.dart';
|
||||
import 'package:surface/screens/settings.dart';
|
||||
import 'package:surface/screens/sharing.dart';
|
||||
import 'package:surface/screens/wallet.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/about.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
@ -70,7 +74,7 @@ final _appRoutes = [
|
||||
postRepostId: int.tryParse(
|
||||
state.uri.queryParameters['reposting'] ?? '',
|
||||
),
|
||||
extraProps: state.extra as PostEditorExtraProps?,
|
||||
extraProps: state.extra as PostEditorExtra?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
@ -96,11 +100,52 @@ final _appRoutes = [
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
builder: (context, state) => const AccountScreen(),
|
||||
),
|
||||
GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
|
||||
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',
|
||||
@ -112,6 +157,7 @@ final _appRoutes = [
|
||||
builder: (context, state) => ChatRoomScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
extra: state.extra as ChatRoomScreenExtra?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
@ -147,11 +193,6 @@ final _appRoutes = [
|
||||
child: const RealmScreen(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:alias',
|
||||
name: 'realmDetail',
|
||||
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manage',
|
||||
name: 'realmManage',
|
||||
@ -159,22 +200,27 @@ final _appRoutes = [
|
||||
editingRealmAlias: state.uri.queryParameters['editing'],
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/discovery',
|
||||
name: 'realmDiscovery',
|
||||
builder: (context, state) => const RealmDiscoveryScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:alias',
|
||||
name: 'realmDetail',
|
||||
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/news',
|
||||
name: 'news',
|
||||
builder: (context, state) => const NewsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:hash',
|
||||
name: 'newsDetail',
|
||||
builder: (context, state) => NewsDetailScreen(
|
||||
hash: state.pathParameters['hash']!,
|
||||
),
|
||||
GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
|
||||
GoRoute(
|
||||
path: '/:hash',
|
||||
name: 'newsDetail',
|
||||
builder: (context, state) => NewsDetailScreen(
|
||||
hash: state.pathParameters['hash']!,
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]),
|
||||
GoRoute(
|
||||
path: '/album',
|
||||
name: 'album',
|
||||
@ -205,35 +251,6 @@ final _appRoutes = [
|
||||
name: 'abuseReport',
|
||||
builder: (context, state) => AbuseReportScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => ProfileEditScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers',
|
||||
name: 'accountPublishers',
|
||||
builder: (context, state) => PublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers/new',
|
||||
name: 'accountPublisherNew',
|
||||
builder: (context, state) => AccountPublisherNewScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers/edit/:name',
|
||||
name: 'accountPublisherEdit',
|
||||
builder: (context, state) => AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
|
@ -74,7 +74,10 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (_isBusy)
|
||||
const CircularProgressIndicator().padding(all: 24).center()
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: const CircularProgressIndicator(),
|
||||
).center()
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
@ -13,6 +15,7 @@ import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class AccountScreen extends StatelessWidget {
|
||||
const AccountScreen({super.key});
|
||||
@ -20,11 +23,51 @@ class AccountScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text("screenAccount").tr(),
|
||||
title: Text(
|
||||
"screenAccount",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 5.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
).tr(),
|
||||
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
||||
? Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 56 + MediaQuery.of(context).padding.top,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 10,
|
||||
sigmaY: 10,
|
||||
),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(
|
||||
clampDouble(10 * 0.1, 0, 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.settings, fill: 1),
|
||||
@ -83,16 +126,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
);
|
||||
}).padding(all: 20),
|
||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||
ListTile(
|
||||
title: Text('accountProfileEdit').tr(),
|
||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.contact_page),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountProfileEdit');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountPublishers').tr(),
|
||||
subtitle: Text('accountPublishersSubtitle').tr(),
|
||||
@ -113,6 +146,36 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
GoRouter.of(context).pushNamed('abuseReport');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('factorSettings').tr(),
|
||||
subtitle: Text('factorSettingsSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.lock),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('factorSettings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountWallet').tr(),
|
||||
subtitle: Text('accountWalletSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.wallet),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountWallet');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountSettings').tr(),
|
||||
subtitle: Text('accountSettingsSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.manage_accounts),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountSettings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountLogout').tr(),
|
||||
subtitle: Text('accountLogoutSubtitle').tr(),
|
||||
@ -134,33 +197,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
await Hive.initFlutter();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountDeletion'.tr()),
|
||||
subtitle: Text('accountDeletionActionDescription'.tr()),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.person_cancel),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'accountDeletion'.tr(),
|
||||
'accountDeletionDescription'.tr(),
|
||||
)
|
||||
.then((value) {
|
||||
if (!value || !context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
sn.client.post('/cgi/id/users/me/deletion').then((value) {
|
||||
if (context.mounted) {
|
||||
context.showSnackbar('accountDeletionSubmitted'.tr());
|
||||
}
|
||||
}).catchError((err) {
|
||||
if (context.mounted) {
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
126
lib/screens/account/account_settings.dart
Normal file
126
lib/screens/account/account_settings.dart
Normal file
@ -0,0 +1,126 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:intl/locale.dart';
|
||||
|
||||
class AccountSettingsScreen extends StatelessWidget {
|
||||
const AccountSettingsScreen({super.key});
|
||||
|
||||
Future<void> _setAccountLanguage(BuildContext context, Locale? value) async {
|
||||
if (value == null) return;
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client.put('/cgi/id/users/me/language', data: {
|
||||
'language': value.toString(),
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
context.showSnackbar('accountSettingsApplied'.tr());
|
||||
await ua.refreshUser();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountSettings').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text('settingsAccountLanguage').tr(),
|
||||
subtitle: Text('settingsAccountLanguageDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.translate),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<Locale?>(
|
||||
isExpanded: true,
|
||||
items: [
|
||||
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
|
||||
return DropdownMenuItem<Locale?>(
|
||||
value: Locale.parse(ele.toString()),
|
||||
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
|
||||
);
|
||||
}),
|
||||
],
|
||||
value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'),
|
||||
onChanged: (Locale? value) {
|
||||
if (value == null) return;
|
||||
_setAccountLanguage(context, value);
|
||||
ua.setLanguage(value.toString());
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 5,
|
||||
),
|
||||
height: 40,
|
||||
width: 160,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountProfileEdit').tr(),
|
||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.contact_page),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountProfileEdit');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountDeletion'.tr()),
|
||||
subtitle: Text('accountDeletionActionDescription'.tr()),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.person_cancel),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'accountDeletion'.tr(),
|
||||
'accountDeletionDescription'.tr(),
|
||||
)
|
||||
.then((value) {
|
||||
if (!value || !context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
sn.client.post('/cgi/id/users/me/deletion').then((value) {
|
||||
if (context.mounted) {
|
||||
context.showSnackbar('accountDeletionSubmitted'.tr());
|
||||
}
|
||||
}).catchError((err) {
|
||||
if (context.mounted) {
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
294
lib/screens/account/factor_settings.dart
Normal file
294
lib/screens/account/factor_settings.dart
Normal file
@ -0,0 +1,294 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/auth.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
final Map<int, (String, String, IconData)> kFactorTypes = {
|
||||
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
|
||||
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
|
||||
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
|
||||
3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active),
|
||||
};
|
||||
|
||||
class FactorSettingsScreen extends StatefulWidget {
|
||||
const FactorSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FactorSettingsScreen> createState() => _FactorSettingsScreenState();
|
||||
}
|
||||
|
||||
class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
||||
bool _isBusy = false;
|
||||
List<SnAuthFactor>? _factors;
|
||||
|
||||
Future<void> _fetchFactors() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/me/factors');
|
||||
_factors = List<SnAuthFactor>.from(
|
||||
resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchFactors();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenFactorSettings').tr(),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(
|
||||
isActive: _isBusy,
|
||||
),
|
||||
ListTile(
|
||||
title: Text('authFactorAdd').tr(),
|
||||
subtitle: Text('authFactorAddSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.add),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _FactorNewDialog(
|
||||
currentlyHave: _factors!,
|
||||
),
|
||||
).then((val) {
|
||||
if (val == true) _fetchFactors();
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchFactors,
|
||||
child: ListView.builder(
|
||||
itemCount: _factors?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final ele = _factors![idx];
|
||||
return ListTile(
|
||||
title: Text(kFactorTypes[ele.type]!.$1).tr(),
|
||||
subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 12),
|
||||
leading: Icon(kFactorTypes[ele.type]!.$3),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: ele.type > 0
|
||||
? () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'authFactorDelete'.tr(),
|
||||
'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]),
|
||||
)
|
||||
.then((val) async {
|
||||
if (!val) return;
|
||||
try {
|
||||
if (!context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/id/users/me/factors/${ele.id}');
|
||||
_fetchFactors();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FactorNewDialog extends StatefulWidget {
|
||||
final List<SnAuthFactor> currentlyHave;
|
||||
|
||||
const _FactorNewDialog({required this.currentlyHave});
|
||||
|
||||
@override
|
||||
State<_FactorNewDialog> createState() => _FactorNewDialogState();
|
||||
}
|
||||
|
||||
class _FactorNewDialogState extends State<_FactorNewDialog> {
|
||||
int? _factorType;
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _submit() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.post('/cgi/id/users/me/factors', data: {
|
||||
'type': _factorType,
|
||||
});
|
||||
final factor = SnAuthFactor.fromJson(resp.data);
|
||||
if (!mounted) return;
|
||||
if (factor.type == 2) {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _FactorTotpFactorDialog(factor: factor),
|
||||
);
|
||||
}
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(true);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('authFactorAdd').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<int>(
|
||||
hint: Text(
|
||||
'Select Item',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
value: _factorType,
|
||||
items: kFactorTypes.entries.map(
|
||||
(ele) {
|
||||
final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key);
|
||||
return DropdownMenuItem<int>(
|
||||
enabled: !contains,
|
||||
value: ele.key,
|
||||
child: Text(
|
||||
ele.value.$1.tr(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
).opacity(contains ? 0.75 : 1),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
onChanged: (val) => setState(() {
|
||||
_factorType = val;
|
||||
}),
|
||||
buttonStyleData: ButtonStyleData(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => Navigator.of(context).pop(),
|
||||
child: Text('dialogCancel').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _submit(),
|
||||
child: Text('dialogConfirm').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FactorTotpFactorDialog extends StatelessWidget {
|
||||
final SnAuthFactor factor;
|
||||
|
||||
const _FactorTotpFactorDialog({required this.factor});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
'totpPostSetup',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr().width(280),
|
||||
),
|
||||
const Gap(4),
|
||||
Center(
|
||||
child: Text(
|
||||
'totpPostSetupDescription',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
).tr().width(280),
|
||||
),
|
||||
const Gap(16),
|
||||
QrImageView(
|
||||
padding: EdgeInsets.zero,
|
||||
data: factor.config!['url'],
|
||||
errorCorrectionLevel: QrErrorCorrectLevel.H,
|
||||
version: QrVersions.auto,
|
||||
size: 160,
|
||||
gapless: true,
|
||||
eyeStyle: QrEyeStyle(
|
||||
eyeShape: QrEyeShape.circle,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
dataModuleStyle: QrDataModuleStyle(
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Center(
|
||||
child: Text(
|
||||
'totpNeverShare',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
).tr().bold().width(280),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -178,6 +178,10 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountPublisherEdit').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
|
@ -45,6 +45,33 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deletePublisher(SnPublisher publisher) async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'publisherDelete'.tr(args: ['#${publisher.name}']),
|
||||
'publisherDeleteDescription'.tr(),
|
||||
);
|
||||
if (!confirm) return;
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
await context
|
||||
.read<SnNetworkProvider>()
|
||||
.client
|
||||
.delete('/cgi/co/publishers/${publisher.name}');
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('publisherDeleted'.tr(args: ['#${publisher.name}']));
|
||||
_publishers.remove(publisher);
|
||||
_fetchPublishers();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -118,6 +145,18 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
});
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete),
|
||||
const Gap(16),
|
||||
Text('delete').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
_deletePublisher(publisher);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -123,8 +123,10 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
),
|
||||
if (_isBusy)
|
||||
SliverToBoxAdapter(
|
||||
child:
|
||||
const CircularProgressIndicator().padding(all: 24).center(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: const CircularProgressIndicator(),
|
||||
).center(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/account/factor_settings.dart';
|
||||
import 'package:surface/types/auth.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
@ -14,11 +15,6 @@ import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import '../../providers/websocket.dart';
|
||||
|
||||
final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
|
||||
0: ('authFactorPassword'.tr(), Symbols.password, false),
|
||||
1: ('authFactorEmail'.tr(), Symbols.email, true),
|
||||
};
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@ -212,7 +208,9 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
autofillHints: [
|
||||
(_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode
|
||||
widget.factor!.type == 0
|
||||
? AutofillHints.password
|
||||
: AutofillHints.oneTimeCode
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
@ -267,7 +265,8 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
||||
bool _isBusy = false;
|
||||
int? _factorPicked;
|
||||
|
||||
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
||||
|
||||
void _performGetFactorCode() async {
|
||||
if (_factorPicked == null) return;
|
||||
@ -328,11 +327,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
||||
),
|
||||
),
|
||||
secondary: Icon(
|
||||
_factorLabelMap[x.type]?.$2 ?? Symbols.question_mark,
|
||||
kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
|
||||
),
|
||||
title: Text(
|
||||
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
|
||||
),
|
||||
kFactorTypes[x.type]?.$1 ?? 'unknown',
|
||||
).tr(),
|
||||
enabled: !widget.ticket!.factorTrail.contains(x.id),
|
||||
value: _factorPicked == x.id,
|
||||
onChanged: (value) {
|
||||
@ -408,11 +407,14 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final lookupResp = await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
||||
final lookupResp =
|
||||
await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
||||
await sn.client.post('/cgi/id/users/me/password-reset', data: {
|
||||
'user_id': lookupResp.data['id'],
|
||||
});
|
||||
if (mounted) context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||
if (mounted) {
|
||||
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
@ -437,7 +439,8 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
widget.onTicket(result.ticket);
|
||||
|
||||
// Pull factors
|
||||
final factorResp = await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
||||
final factorResp =
|
||||
await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
||||
'ticketId': result.ticket!.id.toString(),
|
||||
});
|
||||
widget.onFactor(
|
||||
@ -531,7 +534,10 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
'termAcceptNextWithAgree'.tr(),
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha((255 * 0.75).round()),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
|
@ -44,6 +44,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
'nick': nickname,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'language': EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
@ -155,12 +155,16 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: 'call'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: call.lastDuration.toString(),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
@ -10,8 +10,10 @@ import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/chat.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/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
@ -20,6 +22,7 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
class ChannelDetailScreen extends StatefulWidget {
|
||||
final String scope;
|
||||
final String alias;
|
||||
|
||||
const ChannelDetailScreen({
|
||||
super.key,
|
||||
required this.scope,
|
||||
@ -55,8 +58,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client
|
||||
.get('/cgi/im/channels/${_channel!.keyPath}/members/me');
|
||||
final resp = await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/members/me');
|
||||
_profile = SnChannelMember.fromJson(resp.data);
|
||||
_notifyLevel = _profile!.notify;
|
||||
if (!mounted) return;
|
||||
@ -143,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() {
|
||||
showDialog(
|
||||
context: context,
|
||||
@ -166,13 +187,16 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showMemberAdd() {
|
||||
showModalBottomSheet(
|
||||
void _showMemberAdd() async {
|
||||
final user = await showModalBottomSheet<SnAccount?>(
|
||||
context: context,
|
||||
builder: (context) => _NewChannelMemberWidget(
|
||||
channel: _channel!,
|
||||
builder: (context) => AccountSelect(
|
||||
title: 'channelMemberAdd'.tr(),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (user == null) return;
|
||||
_addMember(user);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -221,11 +245,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('channelDetailPersonalRegion')
|
||||
.bold()
|
||||
.fontSize(17)
|
||||
.tr()
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
Text('channelDetailPersonalRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.notifications),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
@ -264,8 +284,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
),
|
||||
ListTile(
|
||||
leading: AccountImage(
|
||||
content:
|
||||
ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
||||
content: ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
||||
radius: 18,
|
||||
),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
@ -284,8 +303,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('channelActionLeave').tr(),
|
||||
subtitle: Text('channelActionLeaveDescription').tr(),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: _leaveChannel,
|
||||
),
|
||||
],
|
||||
@ -293,11 +311,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('channelDetailMemberRegion')
|
||||
.bold()
|
||||
.fontSize(17)
|
||||
.tr()
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
Text('channelDetailMemberRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.group),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
@ -319,11 +333,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('channelDetailAdminRegion')
|
||||
.bold()
|
||||
.fontSize(17)
|
||||
.tr()
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
Text('channelDetailAdminRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.edit),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
@ -362,18 +372,17 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
class _ChannelProfileDetailDialog extends StatefulWidget {
|
||||
final SnChannel channel;
|
||||
final SnChannelMember current;
|
||||
|
||||
const _ChannelProfileDetailDialog({
|
||||
required this.channel,
|
||||
required this.current,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ChannelProfileDetailDialog> createState() =>
|
||||
_ChannelProfileDetailDialogState();
|
||||
State<_ChannelProfileDetailDialog> createState() => _ChannelProfileDetailDialogState();
|
||||
}
|
||||
|
||||
class _ChannelProfileDetailDialogState
|
||||
extends State<_ChannelProfileDetailDialog> {
|
||||
class _ChannelProfileDetailDialogState extends State<_ChannelProfileDetailDialog> {
|
||||
bool _isBusy = false;
|
||||
|
||||
final TextEditingController _nickController = TextEditingController();
|
||||
@ -444,11 +453,11 @@ class _ChannelProfileDetailDialogState
|
||||
|
||||
class _ChannelMemberListWidget extends StatefulWidget {
|
||||
final SnChannel channel;
|
||||
|
||||
const _ChannelMemberListWidget({required this.channel});
|
||||
|
||||
@override
|
||||
State<_ChannelMemberListWidget> createState() =>
|
||||
_ChannelMemberListWidgetState();
|
||||
State<_ChannelMemberListWidget> createState() => _ChannelMemberListWidgetState();
|
||||
}
|
||||
|
||||
class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
@ -463,12 +472,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
try {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get(
|
||||
'/cgi/im/channels/${widget.channel.keyPath}/members',
|
||||
queryParameters: {
|
||||
'take': 10,
|
||||
'offset': 0,
|
||||
});
|
||||
final resp = await sn.client.get('/cgi/im/channels/${widget.channel.keyPath}/members', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': 0,
|
||||
});
|
||||
final out = List<SnChannelMember>.from(
|
||||
resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
|
||||
);
|
||||
@ -526,9 +533,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
children: [
|
||||
const Icon(Symbols.group, size: 24),
|
||||
const Gap(16),
|
||||
Text('channelMemberManage')
|
||||
.tr()
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
Text('channelMemberManage').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Expanded(
|
||||
@ -539,8 +544,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
},
|
||||
child: InfiniteList(
|
||||
itemCount: _members.length,
|
||||
hasReachedMax:
|
||||
_totalCount != null && _members.length >= _totalCount!,
|
||||
hasReachedMax: _totalCount != null && _members.length >= _totalCount!,
|
||||
isLoading: _isBusy,
|
||||
onFetchData: _fetchMembers,
|
||||
itemBuilder: (context, index) {
|
||||
@ -551,8 +555,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
||||
),
|
||||
title: Text(
|
||||
ud.getAccountFromCache(member.accountId)?.name ??
|
||||
'unknown'.tr(),
|
||||
ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
||||
),
|
||||
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
||||
trailing: SizedBox(
|
||||
@ -562,8 +565,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed:
|
||||
_isUpdating ? null : () => _deleteMember(member),
|
||||
onPressed: _isUpdating ? null : () => _deleteMember(member),
|
||||
icon: const Icon(Symbols.person_remove),
|
||||
),
|
||||
],
|
||||
@ -578,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);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -17,6 +18,7 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChatManageScreen extends StatefulWidget {
|
||||
final String? editingChannelAlias;
|
||||
|
||||
const ChatManageScreen({super.key, this.editingChannelAlias});
|
||||
|
||||
@override
|
||||
@ -33,6 +35,11 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
List<SnRealm>? _realms;
|
||||
SnRealm? _belongToRealm;
|
||||
|
||||
SnChannel? _editingChannel;
|
||||
|
||||
bool _isPublic = false;
|
||||
bool _isCommunity = false;
|
||||
|
||||
Future<void> _fetchRealms() async {
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
@ -41,6 +48,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
_realms = List<SnRealm>.from(
|
||||
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
||||
);
|
||||
if (_editingChannel != null) {
|
||||
_belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
@ -48,8 +58,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
SnChannel? _editingChannel;
|
||||
|
||||
Future<void> _fetchChannel() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
@ -62,6 +70,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
_aliasController.text = _editingChannel!.alias;
|
||||
_nameController.text = _editingChannel!.name;
|
||||
_descriptionController.text = _editingChannel!.description;
|
||||
_isPublic = _editingChannel!.isPublic;
|
||||
_isCommunity = _editingChannel!.isCommunity;
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -83,6 +93,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
: uuid.v4().replaceAll('-', '').substring(0, 12),
|
||||
'name': _nameController.text,
|
||||
'description': _descriptionController.text,
|
||||
'is_public': _isPublic,
|
||||
'is_community': _isCommunity,
|
||||
};
|
||||
|
||||
try {
|
||||
@ -124,9 +136,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.editingChannelAlias != null
|
||||
? Text('screenChatManage').tr()
|
||||
: Text('screenChatNew').tr(),
|
||||
title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
@ -138,8 +148,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
leadingPadding: const EdgeInsets.only(left: 10, right: 20),
|
||||
dividerColor: Colors.transparent,
|
||||
content: Text(
|
||||
'channelEditingNotice'
|
||||
.tr(args: ['#${_editingChannel!.alias}']),
|
||||
'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@ -162,6 +171,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
items: [
|
||||
...(_realms?.map(
|
||||
(SnRealm item) => DropdownMenuItem<SnRealm>(
|
||||
enabled: _editingChannel == null || _editingChannel?.realmId == item.id,
|
||||
value: item,
|
||||
child: Row(
|
||||
children: [
|
||||
@ -179,15 +189,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.name).textStyle(Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!),
|
||||
Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!),
|
||||
Text(
|
||||
item.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).textStyle(
|
||||
Theme.of(context).textTheme.bodySmall!),
|
||||
).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -197,14 +204,14 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
) ??
|
||||
[]),
|
||||
DropdownMenuItem<SnRealm>(
|
||||
enabled: _editingChannel == null,
|
||||
value: null,
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
child: const Icon(Symbols.clear),
|
||||
),
|
||||
const Gap(12),
|
||||
@ -213,9 +220,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('fieldChatBelongToRealmUnset')
|
||||
.tr()
|
||||
.textStyle(
|
||||
Text('fieldChatBelongToRealmUnset').tr().textStyle(
|
||||
Theme.of(context).textTheme.bodyMedium!,
|
||||
),
|
||||
],
|
||||
@ -231,10 +236,10 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
height: 60,
|
||||
height: 48,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 60,
|
||||
height: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -250,8 +255,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
helperText: 'fieldChatAliasHint'.tr(),
|
||||
helperMaxLines: 2,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
@ -260,8 +264,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldChatName'.tr(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
@ -272,8 +275,24 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldChatDescription'.tr(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
CheckboxListTile(
|
||||
value: _isPublic,
|
||||
title: Text('channelIsPublic'.tr()),
|
||||
subtitle: Text('channelIsPublicDescription'.tr()),
|
||||
onChanged: (value) {
|
||||
setState(() => _isPublic = value ?? false);
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
value: _isCommunity,
|
||||
title: Text('channelIsCommunity'.tr()),
|
||||
subtitle: Text('channelIsCommunityDescription'.tr()),
|
||||
onChanged: (value) {
|
||||
setState(() => _isCommunity = value ?? false);
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -9,9 +10,12 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/chat_message_controller.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
||||
@ -23,14 +27,19 @@ import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
import '../../providers/user_directory.dart';
|
||||
import '../../providers/userinfo.dart';
|
||||
class ChatRoomScreenExtra {
|
||||
final String? initialText;
|
||||
final List<PostWriteMedia>? initialAttachments;
|
||||
|
||||
ChatRoomScreenExtra({this.initialText, this.initialAttachments});
|
||||
}
|
||||
|
||||
class ChatRoomScreen extends StatefulWidget {
|
||||
final String scope;
|
||||
final String alias;
|
||||
final ChatRoomScreenExtra? extra;
|
||||
|
||||
const ChatRoomScreen({super.key, required this.scope, required this.alias});
|
||||
const ChatRoomScreen({super.key, required this.scope, required this.alias, this.extra});
|
||||
|
||||
@override
|
||||
State<ChatRoomScreen> createState() => _ChatRoomScreenState();
|
||||
@ -177,12 +186,27 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
_messageController = ChatMessageController(context);
|
||||
_fetchChannel().then((_) async {
|
||||
await _messageController.initialize(_channel!);
|
||||
await _messageController.checkUpdate();
|
||||
await _fetchOngoingCall();
|
||||
|
||||
if (widget.extra != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
log('[ChatInput] Setting initial text and attachments...');
|
||||
if (widget.extra!.initialText != null) {
|
||||
_inputGlobalKey.currentState?.setInitialText(widget.extra!.initialText!);
|
||||
}
|
||||
if (widget.extra!.initialAttachments != null) {
|
||||
_inputGlobalKey.currentState?.setInitialAttachments(widget.extra!.initialAttachments!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
_messageController.checkUpdate(),
|
||||
_fetchOngoingCall(),
|
||||
]);
|
||||
});
|
||||
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
_wsSubscription = ws.stream.stream.listen((event) {
|
||||
_wsSubscription = ws.pk.stream.listen((event) {
|
||||
switch (event.method) {
|
||||
case 'calls.new':
|
||||
final payload = SnChatCall.fromJson(event.payload!);
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||
@ -9,7 +8,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/post/post_detail.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -163,6 +161,48 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('writePostTypeQuestion').tr(),
|
||||
const Gap(20),
|
||||
FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: 'writePostTypeQuestion'.tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||
'mode': 'questions',
|
||||
}).then((value) {
|
||||
if (value == true) {
|
||||
_refreshPosts();
|
||||
}
|
||||
});
|
||||
_fabKey.currentState!.toggle();
|
||||
},
|
||||
child: const Icon(Symbols.question_answer),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('writePostTypeVideo').tr(),
|
||||
const Gap(20),
|
||||
FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: 'writePostTypeVideo'.tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||
'mode': 'videos',
|
||||
}).then((value) {
|
||||
if (value == true) {
|
||||
_refreshPosts();
|
||||
}
|
||||
});
|
||||
_fabKey.currentState!.toggle();
|
||||
},
|
||||
child: const Icon(Symbols.video_call),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
@ -221,34 +261,15 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
||||
onFetchData: _fetchPosts,
|
||||
itemBuilder: (context, idx) {
|
||||
return Center(
|
||||
child: OpenContainer(
|
||||
closedBuilder: (_, __) => Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: PostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
_refreshPosts();
|
||||
},
|
||||
),
|
||||
),
|
||||
openBuilder: (_, close) => PostDetailScreen(
|
||||
slug: _posts[idx].id.toString(),
|
||||
preload: _posts[idx],
|
||||
onBack: close,
|
||||
),
|
||||
openColor: Colors.transparent,
|
||||
openElevation: 0,
|
||||
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75),
|
||||
transitionType: ContainerTransitionType.fade,
|
||||
closedShape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
),
|
||||
return OpenablePostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
_refreshPosts();
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
|
@ -6,15 +6,15 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/relationship.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/account.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/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
import '../providers/userinfo.dart';
|
||||
import '../widgets/unauthorized_hint.dart';
|
||||
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||
|
||||
const kFriendStatus = {
|
||||
0: 'friendStatusPending',
|
||||
@ -168,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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -199,11 +217,16 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Symbols.add),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
onPressed: () async {
|
||||
final user = await showModalBottomSheet<SnAccount?>(
|
||||
context: context,
|
||||
builder: (context) => _NewFriendWidget(),
|
||||
builder: (context) => AccountSelect(
|
||||
title: 'friendNew'.tr(),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (user == null) return;
|
||||
_sendRequest(user);
|
||||
},
|
||||
),
|
||||
body: Column(
|
||||
@ -231,8 +254,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: _showBlocks,
|
||||
),
|
||||
if (_requests.isNotEmpty || _blocks.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
@ -264,16 +286,12 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _changeRelation(relation, 2),
|
||||
onTap: _isUpdating ? null : () => _changeRelation(relation, 2),
|
||||
child: Text('friendBlock').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
InkWell(
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _deleteRelation(relation),
|
||||
onTap: _isUpdating ? null : () => _deleteRelation(relation),
|
||||
child: Text('friendDeleteAction').tr(),
|
||||
),
|
||||
],
|
||||
@ -293,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 {
|
||||
final List<SnRelationship> relations;
|
||||
|
||||
const _FriendshipListWidget({required this.relations});
|
||||
|
||||
@override
|
||||
@ -476,9 +420,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(kFriendStatus[relation.status] ?? 'unknown')
|
||||
.tr()
|
||||
.opacity(0.75),
|
||||
Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75),
|
||||
if (relation.status == 0)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
@ -499,8 +441,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap:
|
||||
_isBusy ? null : () => _changeRelation(relation, 1),
|
||||
onTap: _isBusy ? null : () => _changeRelation(relation, 1),
|
||||
child: Text('friendUnblock').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
|
@ -51,7 +51,7 @@ class HomeScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
static const List<HomeScreenDashEntry> kCards = [
|
||||
late final List<HomeScreenDashEntry> kCards = [
|
||||
HomeScreenDashEntry(
|
||||
name: 'dashEntryRecommendation',
|
||||
child: _HomeDashRecommendationPostWidget(),
|
||||
@ -69,7 +69,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
HomeScreenDashEntry(
|
||||
name: 'dashEntryTodayNews',
|
||||
child: _HomeDashTodayNews(),
|
||||
cols: 2,
|
||||
cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2,
|
||||
),
|
||||
];
|
||||
|
||||
@ -131,6 +131,7 @@ class _HomeDashUpdateWidget extends StatelessWidget {
|
||||
return Container(
|
||||
padding: padding,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
leading: Icon(Symbols.update),
|
||||
title: Text('updateAvailable').tr(),
|
||||
@ -180,6 +181,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
||||
return Column(
|
||||
children: days.map((ele) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
|
||||
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
||||
@ -203,6 +205,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
||||
final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
|
||||
final diff = nextOne.$2.difference(DateTime.now());
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
||||
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
|
||||
@ -270,6 +273,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -288,12 +292,13 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
||||
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: 2,
|
||||
maxLines: MediaQuery.of(context).size.width >= 640 ? 2 : 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
@ -302,20 +307,18 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
||||
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);
|
||||
}
|
||||
),
|
||||
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: () {
|
||||
@ -470,6 +473,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -515,6 +519,11 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
'+${_todayRecord!.resultExperience} EXP',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (_todayRecord!.resultCoin >= 0)
|
||||
Text(
|
||||
'+${_todayRecord!.resultCoin} ${'walletCurrencyShort'.tr()}',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -590,6 +599,7 @@ class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -663,11 +673,13 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
|
||||
Widget build(BuildContext context) {
|
||||
if (_isBusy) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: CircularProgressIndicator().center(),
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -175,54 +175,57 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
),
|
||||
if (_articleFragment != null && _isReadingFromReader)
|
||||
Expanded(
|
||||
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),
|
||||
),
|
||||
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(
|
||||
|
@ -70,11 +70,16 @@ class _NewsScreenState extends State<NewsScreen> {
|
||||
sliver: SliverAppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNews').tr(),
|
||||
floating: true,
|
||||
snap: true,
|
||||
bottom: TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(child: Text('newsAllSources'.tr())),
|
||||
for (final source in _sources!) Tab(child: Text(source.label)),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -146,80 +151,87 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
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];
|
||||
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 baseUri = Uri.parse(article.url);
|
||||
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||
|
||||
final htmlDescription = parse(article.description);
|
||||
final date = article.publishedAt ?? article.createdAt;
|
||||
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}',
|
||||
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!),
|
||||
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),
|
||||
],
|
||||
).opacity(0.75).padding(horizontal: 16),
|
||||
Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75).padding(horizontal: 16),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/notification.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/notification.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
@ -21,6 +22,16 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
import '../providers/userinfo.dart';
|
||||
import '../widgets/unauthorized_hint.dart';
|
||||
|
||||
const Map<String, IconData> kNotificationTopicIcons = {
|
||||
'general': Symbols.notifications,
|
||||
'passport.security.alert': Symbols.gpp_maybe,
|
||||
'passport.security.otp': Symbols.password,
|
||||
'interactive.subscription': Symbols.subscriptions,
|
||||
'interactive.feedback': Symbols.add_reaction,
|
||||
'messaging.callStart': Symbols.call_received,
|
||||
'wallet.transaction.new': Symbols.receipt,
|
||||
};
|
||||
|
||||
class NotificationScreen extends StatefulWidget {
|
||||
const NotificationScreen({super.key});
|
||||
|
||||
@ -36,13 +47,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
final List<SnNotification> _notifications = List.empty(growable: true);
|
||||
int? _totalCount;
|
||||
|
||||
static const Map<String, IconData> kNotificationTopicIcons = {
|
||||
'passport.security.alert': Symbols.gpp_maybe,
|
||||
'interactive.subscription': Symbols.subscriptions,
|
||||
'interactive.feedback': Symbols.add_reaction,
|
||||
'messaging.callStart': Symbols.call_received,
|
||||
};
|
||||
|
||||
Future<void> _fetchNotifications() async {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) return;
|
||||
@ -51,6 +55,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final nty = context.read<NotificationProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/notifications?take=10');
|
||||
_totalCount = resp.data['count'];
|
||||
_notifications.addAll(
|
||||
@ -59,6 +64,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
.cast<SnNotification>() ??
|
||||
[],
|
||||
);
|
||||
nty.updateTray();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -85,9 +91,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final nty = context.read<NotificationProvider>();
|
||||
final resp = await sn.client.put('/cgi/id/notifications/read/all');
|
||||
_notifications.clear();
|
||||
_fetchNotifications();
|
||||
nty.clear();
|
||||
|
||||
if (!mounted) return;
|
||||
context.showSnackbar(
|
||||
|
@ -6,7 +6,6 @@ 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:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
@ -17,7 +16,6 @@ import 'package:surface/widgets/navigation/app_background.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
|
||||
class PostDetailScreen extends StatefulWidget {
|
||||
final String slug;
|
||||
@ -64,7 +62,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
|
||||
|
||||
return AppBackground(
|
||||
isRoot: widget.onBack != null,
|
||||
@ -114,7 +113,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
data: _data!,
|
||||
maxWidth: 640,
|
||||
maxWidth: maxWidth,
|
||||
showComments: false,
|
||||
showFullPost: true,
|
||||
onChanged: (data) {
|
||||
@ -125,11 +124,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||
if (_data != null)
|
||||
if (_data != null && _data!.type != 'video') const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||
if (_data != null && _data!.type != 'video')
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@ -142,51 +141,30 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
).padding(horizontal: 20, vertical: 12).center(),
|
||||
),
|
||||
),
|
||||
if (_data != null && ua.isAuthorized)
|
||||
if (_data != null && ua.isAuthorized && _data!.type != 'video')
|
||||
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(),
|
||||
child: PostCommentQuickAction(
|
||||
parentPost: _data!,
|
||||
maxWidth: maxWidth,
|
||||
onPosted: () {
|
||||
setState(() {
|
||||
_data = _data!.copyWith(
|
||||
metric: _data!.metric.copyWith(
|
||||
replyCount: _data!.metric.replyCount + 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_data != null)
|
||||
if (_data != null && _data!.type != 'video')
|
||||
PostCommentSliverList(
|
||||
key: _childListKey,
|
||||
parentPostId: _data!.id,
|
||||
maxWidth: 640,
|
||||
parentPost: _data!,
|
||||
maxWidth: maxWidth,
|
||||
),
|
||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
if (_data != null && _data!.type == 'video') SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,6 @@
|
||||
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:styled_widget/styled_widget.dart';
|
||||
@ -134,7 +133,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
body: Stack(
|
||||
children: [
|
||||
InfiniteList(
|
||||
padding: const EdgeInsets.only(top: 100),
|
||||
padding: const EdgeInsets.only(top: 100 + 8),
|
||||
itemCount: _posts.length,
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
||||
@ -142,27 +141,18 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
_fetchPosts();
|
||||
},
|
||||
itemBuilder: (context, idx) {
|
||||
return GestureDetector(
|
||||
child: PostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
_refreshPosts();
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {'slug': _posts[idx].id.toString()},
|
||||
extra: _posts[idx],
|
||||
);
|
||||
return OpenablePostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
_refreshPosts();
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
),
|
||||
Positioned(
|
||||
top: 16,
|
||||
|
@ -287,8 +287,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
appBarTheme: Theme.of(context).appBarTheme.copyWith(
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
child: SliverAppBar(
|
||||
expandedHeight: _appBarHeight,
|
||||
@ -597,25 +597,16 @@ class _PublisherPostList extends StatelessWidget {
|
||||
hasReachedMax: postCount != null && posts.length >= postCount!,
|
||||
onFetchData: fetchPosts,
|
||||
itemBuilder: (context, idx) {
|
||||
return GestureDetector(
|
||||
child: PostItem(
|
||||
data: posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
onChanged(idx, data);
|
||||
},
|
||||
onDeleted: onDeleted,
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {'slug': posts[idx].id.toString()},
|
||||
extra: posts[idx],
|
||||
);
|
||||
return OpenablePostItem(
|
||||
data: posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
onChanged(idx, data);
|
||||
},
|
||||
onDeleted: onDeleted,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -100,6 +100,12 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenRealm').tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.globe),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('realmDiscovery');
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: !_isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
|
||||
onPressed: () {
|
||||
|
@ -50,6 +50,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
_aliasController.text = out.alias;
|
||||
_nameController.text = out.name;
|
||||
_descriptionController.text = out.description;
|
||||
_isPublic = out.isPublic;
|
||||
_isCommunity = out.isCommunity;
|
||||
} catch (err) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
@ -67,6 +69,9 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
|
||||
final _imagePicker = ImagePicker();
|
||||
|
||||
bool _isPublic = false;
|
||||
bool _isCommunity = false;
|
||||
|
||||
Future<void> _updateImage(String place) async {
|
||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
if (image == null) return;
|
||||
@ -138,6 +143,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
'description': _descriptionController.text,
|
||||
'avatar': _avatar,
|
||||
'banner': _banner,
|
||||
'is_public': _isPublic,
|
||||
'is_community': _isCommunity,
|
||||
};
|
||||
|
||||
try {
|
||||
@ -293,6 +300,23 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
CheckboxListTile(
|
||||
value: _isPublic,
|
||||
title: Text('realmIsPublic'.tr()),
|
||||
subtitle: Text('realmIsPublicDescription'.tr()),
|
||||
onChanged: (value) {
|
||||
setState(() => _isPublic = value ?? false);
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
value: _isCommunity,
|
||||
title: Text('realmIsCommunity'.tr()),
|
||||
subtitle: Text('realmIsCommunityDescription'.tr()),
|
||||
onChanged: (value) {
|
||||
setState(() => _isCommunity = value ?? false);
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
|
@ -8,9 +8,11 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/account_select.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';
|
||||
@ -229,13 +231,35 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showMemberAdd() {
|
||||
showModalBottomSheet(
|
||||
Future<void> _addMember(SnAccount related) async {
|
||||
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,
|
||||
builder: (context) => _NewRealmMemberWidget(
|
||||
realm: widget.realm!,
|
||||
builder: (context) => AccountSelect(
|
||||
title: 'realmMemberAdd'.tr(),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (user == null) return;
|
||||
_addMember(user);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -293,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 {
|
||||
final SnRealm? realm;
|
||||
final Function() onUpdate;
|
||||
@ -412,6 +357,28 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _leaveRealm() async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'realmLeave'.tr(),
|
||||
'realmLeaveDescription'.tr(),
|
||||
);
|
||||
if (!confirm) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
try {
|
||||
await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}/members/me');
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.read<UserProvider>();
|
||||
@ -422,22 +389,31 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
||||
children: [
|
||||
const Gap(8),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.edit),
|
||||
leading: const Icon(Symbols.logout),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('realmEdit').tr(),
|
||||
subtitle: Text('realmEditDescription').tr(),
|
||||
title: Text('realmLeave').tr(),
|
||||
subtitle: Text('realmLeaveDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmManage',
|
||||
queryParameters: {'editing': widget.realm!.alias},
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
widget.onUpdate();
|
||||
}
|
||||
});
|
||||
},
|
||||
onTap: _isBusy ? null : () => _leaveRealm(),
|
||||
),
|
||||
if (isOwned)
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.edit),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('realmEdit').tr(),
|
||||
subtitle: Text('realmEditDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmManage',
|
||||
queryParameters: {'editing': widget.realm!.alias},
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
widget.onUpdate();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
if (isOwned)
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.delete),
|
||||
|
290
lib/screens/realm/realm_discovery.dart
Normal file
290
lib/screens/realm/realm_discovery.dart
Normal file
@ -0,0 +1,290 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class RealmDiscoveryScreen extends StatefulWidget {
|
||||
const RealmDiscoveryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RealmDiscoveryScreen> createState() => _RealmDiscoveryScreenState();
|
||||
}
|
||||
|
||||
class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
|
||||
List<SnRealm>? _realms;
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _fetchRealms() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/realms');
|
||||
_realms = List<SnRealm>.from(
|
||||
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
rethrow;
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchRealms();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('screenRealmDiscovery').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchRealms,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: _realms?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final realm = _realms![idx];
|
||||
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: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _RealmJoinPopup(realm: realm),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
).center();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RealmJoinPopup extends StatefulWidget {
|
||||
final SnRealm realm;
|
||||
|
||||
const _RealmJoinPopup({required this.realm});
|
||||
|
||||
@override
|
||||
State<_RealmJoinPopup> createState() => _RealmJoinPopupState();
|
||||
}
|
||||
|
||||
class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
||||
final List<String> _planJoinChannels = List.empty(growable: true);
|
||||
|
||||
List<SnChannel>? _channels;
|
||||
bool _isBusy = false;
|
||||
bool _isJoining = false;
|
||||
|
||||
Future<void> _fetchPublicChannels() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
|
||||
final out = List<SnChannel>.from(
|
||||
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
||||
);
|
||||
setState(() => _channels = out);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _joinRealm() async {
|
||||
try {
|
||||
setState(() => _isJoining = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client.post('/cgi/id/realms/${widget.realm.alias}/members', data: {
|
||||
'related': ua.user?.name,
|
||||
});
|
||||
await _joinSelectedChannels();
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('realmJoined'.tr(args: [widget.realm.name]));
|
||||
Navigator.pop(context);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isJoining = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _joinSelectedChannels() async {
|
||||
if (_planJoinChannels.isEmpty) return;
|
||||
for (final channel in _planJoinChannels) {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client.post('/cgi/im/channels/${widget.realm.alias}/$channel/members', data: {
|
||||
'related': ua.user?.name,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchPublicChannels();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.group_add, size: 24),
|
||||
const Gap(16),
|
||||
Text('realmJoin', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.realm.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Text(
|
||||
widget.realm.description,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _isJoining ? null : () => _joinRealm(),
|
||||
child: Text('join'.tr()),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, bottom: 12),
|
||||
const Divider(height: 1),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
|
||||
.padding(horizontal: 24, vertical: 8),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _channels?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final channel = _channels![index];
|
||||
return CheckboxListTile(
|
||||
value: _planJoinChannels.contains(channel.alias),
|
||||
title: Text(channel.name),
|
||||
subtitle: Text(
|
||||
channel.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
secondary: AccountImage(
|
||||
content: null,
|
||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||
),
|
||||
onChanged: (value) {
|
||||
value ??= false;
|
||||
if (value) {
|
||||
setState(() => _planJoinChannels.add(channel.alias));
|
||||
} else {
|
||||
setState(() => _planJoinChannels.remove(channel.alias));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -82,6 +82,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('settingsAppearance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
ListTile(
|
||||
title: Text('settingsDisplayLanguage').tr(),
|
||||
subtitle: Text('settingsDisplayLanguageDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.translate),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<Locale?>(
|
||||
isExpanded: true,
|
||||
items: [
|
||||
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
|
||||
return DropdownMenuItem<Locale?>(
|
||||
value: ele,
|
||||
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
|
||||
);
|
||||
}),
|
||||
DropdownMenuItem<Locale?>(
|
||||
value: null,
|
||||
child: Text('settingsDisplayLanguageSystem').tr().fontSize(14),
|
||||
),
|
||||
],
|
||||
value: EasyLocalization.of(context)!.currentLocale,
|
||||
onChanged: (Locale? value) {
|
||||
if (value != null) {
|
||||
EasyLocalization.of(context)!.setLocale(value);
|
||||
} else {
|
||||
EasyLocalization.of(context)!.resetLocale();
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 5,
|
||||
),
|
||||
height: 40,
|
||||
width: 160,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb)
|
||||
ListTile(
|
||||
title: Text('settingsBackgroundImage').tr(),
|
||||
@ -147,30 +189,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value);
|
||||
final color = await showDialog<Color?>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
pickerColor: pickerColor,
|
||||
onColorChanged: (color) => pickerColor = color,
|
||||
enableAlpha: false,
|
||||
hexInputBar: true,
|
||||
builder: (context) =>
|
||||
AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
pickerColor: pickerColor,
|
||||
onColorChanged: (color) => pickerColor = color,
|
||||
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;
|
||||
@ -206,11 +249,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
value: _prefs.getInt(kAppColorSchemeStoreKey) == null
|
||||
? 1
|
||||
: kColorSchemes.values
|
||||
.toList()
|
||||
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
|
||||
.toList()
|
||||
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
|
||||
onChanged: (int? value) {
|
||||
if (value != null && value != -1) {
|
||||
_prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values.elementAt(value).value);
|
||||
_prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values
|
||||
.elementAt(value)
|
||||
.value);
|
||||
final th = context.read<ThemeProvider>();
|
||||
th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value));
|
||||
setState(() {});
|
||||
@ -342,7 +387,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
('Custom', _serverUrlController.text),
|
||||
]
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
(item) =>
|
||||
DropdownMenuItem<String>(
|
||||
value: item.$2,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
@ -354,7 +400,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
.toList(),
|
||||
value: _serverUrlController.text,
|
||||
onChanged: (String? value) {
|
||||
@ -409,11 +455,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
isExpanded: true,
|
||||
items: kImageQualityLevel.entries
|
||||
.map(
|
||||
(item) => DropdownMenuItem<FilterQuality>(
|
||||
(item) =>
|
||||
DropdownMenuItem<FilterQuality>(
|
||||
value: item.value,
|
||||
child: Text(item.key).tr().fontSize(14),
|
||||
),
|
||||
)
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (FilterQuality? value) {
|
||||
if (value == null) return;
|
||||
|
@ -8,9 +8,20 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/chat/room.dart';
|
||||
import 'package:surface/screens/post/post_editor.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
|
||||
class AppSharingListener extends StatefulWidget {
|
||||
final Widget child;
|
||||
@ -51,20 +62,39 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
pathParameters: {
|
||||
'mode': 'stories',
|
||||
},
|
||||
extra: PostEditorExtraProps(
|
||||
extra: PostEditorExtra(
|
||||
text: value
|
||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||
.map((e) => e.path).join('\n'),
|
||||
.map((e) => e.path)
|
||||
.join('\n'),
|
||||
attachments: value
|
||||
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(),
|
||||
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
|
||||
.contains(e.type))
|
||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
leading: Icon(Icons.chat_outlined),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('shareIntentSendChannel').tr(),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _ShareIntentChannelSelect(value: value),
|
||||
).then((val) {
|
||||
if (!context.mounted) return;
|
||||
if (val == true) Navigator.pop(context);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
).width(280),
|
||||
)
|
||||
],
|
||||
),
|
||||
@ -103,7 +133,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||
_initialize();
|
||||
_initialHandle();
|
||||
}
|
||||
@ -120,3 +150,193 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
class _ShareIntentChannelSelect extends StatefulWidget {
|
||||
final Iterable<SharedMediaFile> value;
|
||||
|
||||
const _ShareIntentChannelSelect({required this.value});
|
||||
|
||||
@override
|
||||
State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState();
|
||||
}
|
||||
|
||||
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||
bool _isBusy = true;
|
||||
|
||||
List<SnChannel>? _channels;
|
||||
Map<int, SnChatMessage>? _lastMessages;
|
||||
|
||||
void _refreshChannels() {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) {
|
||||
setState(() => _isBusy = false);
|
||||
return;
|
||||
}
|
||||
|
||||
final chan = context.read<ChatChannelProvider>();
|
||||
chan.fetchChannels().listen((channels) async {
|
||||
final lastMessages = await chan.getLastMessages(channels);
|
||||
_lastMessages = {for (final val in lastMessages) val.channelId: val};
|
||||
channels.sort((a, b) {
|
||||
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) {
|
||||
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt);
|
||||
}
|
||||
if (_lastMessages!.containsKey(a.id)) return -1;
|
||||
if (_lastMessages!.containsKey(b.id)) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (!mounted) return;
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
for (final channel in channels) {
|
||||
if (channel.type == 1) {
|
||||
await ud.listAccount(
|
||||
channel.members
|
||||
?.cast<SnChannelMember?>()
|
||||
.map((ele) => ele?.accountId)
|
||||
.where((ele) => ele != null)
|
||||
.toSet() ??
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) setState(() => _channels = channels);
|
||||
})
|
||||
..onError((err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
setState(() => _isBusy = false);
|
||||
})
|
||||
..onDone(() {
|
||||
if (!mounted) return;
|
||||
setState(() => _isBusy = false);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refreshChannels();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.read<UserProvider>();
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.chat, size: 24),
|
||||
const Gap(16),
|
||||
Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||
child: ListView.builder(
|
||||
itemCount: _channels?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final channel = _channels![idx];
|
||||
final lastMessage = _lastMessages?[channel.id];
|
||||
|
||||
if (channel.type == 1) {
|
||||
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||
(ele) => ele?.accountId != ua.user?.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text(
|
||||
'channelDirectMessageDescription'.tr(args: [
|
||||
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
||||
]),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {
|
||||
'scope': channel.realm?.alias ?? 'global',
|
||||
'alias': channel.alias,
|
||||
},
|
||||
).then((value) {
|
||||
if (mounted) _refreshChannels();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
title: Text(channel.name),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text(
|
||||
channel.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: null,
|
||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, true);
|
||||
GoRouter.of(context)
|
||||
.pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {
|
||||
'scope': channel.realm?.alias ?? 'global',
|
||||
'alias': channel.alias,
|
||||
},
|
||||
extra: ChatRoomScreenExtra(
|
||||
initialText: widget.value
|
||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||
.map((e) => e.path)
|
||||
.join('\n'),
|
||||
initialAttachments: widget.value
|
||||
.where((e) =>
|
||||
[SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
.then((value) {
|
||||
if (value == true) _refreshChannels();
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
279
lib/screens/wallet.dart
Normal file
279
lib/screens/wallet.dart
Normal file
@ -0,0 +1,279 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/wallet.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class WalletScreen extends StatefulWidget {
|
||||
const WalletScreen({super.key});
|
||||
|
||||
@override
|
||||
State<WalletScreen> createState() => _WalletScreenState();
|
||||
}
|
||||
|
||||
class _WalletScreenState extends State<WalletScreen> {
|
||||
bool _isBusy = false;
|
||||
SnWallet? _wallet;
|
||||
|
||||
Future<void> _fetchWallet() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/wa/wallets/me');
|
||||
_wallet = SnWallet.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchWallet();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountWallet').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (_wallet == null)
|
||||
Expanded(
|
||||
child: _CreateWalletWidget(
|
||||
onCreate: () {
|
||||
_fetchWallet();
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
child: Icon(Symbols.wallet, size: 28),
|
||||
),
|
||||
const Gap(12),
|
||||
SizedBox(width: double.infinity),
|
||||
Text(
|
||||
NumberFormat.compactCurrency(
|
||||
locale: EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
symbol: '${'walletCurrencyShort'.tr()} ',
|
||||
decimalDigits: 2,
|
||||
).format(double.parse(_wallet!.balance)),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text('walletCurrency'.plural(double.parse(_wallet!.balance))),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 24),
|
||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||
if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletTransactionList extends StatefulWidget {
|
||||
final SnWallet myself;
|
||||
|
||||
const _WalletTransactionList({required this.myself});
|
||||
|
||||
@override
|
||||
State<_WalletTransactionList> createState() => _WalletTransactionListState();
|
||||
}
|
||||
|
||||
class _WalletTransactionListState extends State<_WalletTransactionList> {
|
||||
bool _isBusy = false;
|
||||
int? _totalCount;
|
||||
final List<SnTransaction> _transactions = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchTransactions() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/wa/transactions/me', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _transactions.length,
|
||||
});
|
||||
_totalCount = resp.data['count'];
|
||||
_transactions.addAll(
|
||||
resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchTransactions();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchTransactions,
|
||||
child: InfiniteList(
|
||||
itemCount: _transactions.length,
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!,
|
||||
onFetchData: () {
|
||||
_fetchTransactions();
|
||||
},
|
||||
itemBuilder: (context, idx) {
|
||||
final ele = _transactions[idx];
|
||||
final isIncoming = ele.payeeId == widget.myself.id;
|
||||
return ListTile(
|
||||
leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made),
|
||||
title: Text(
|
||||
'${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
|
||||
style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(ele.remark),
|
||||
const Gap(2),
|
||||
Text(
|
||||
DateFormat(
|
||||
null,
|
||||
EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
).format(ele.createdAt),
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateWalletWidget extends StatefulWidget {
|
||||
final Function()? onCreate;
|
||||
|
||||
const _CreateWalletWidget({required this.onCreate});
|
||||
|
||||
@override
|
||||
State<_CreateWalletWidget> createState() => _CreateWalletWidgetState();
|
||||
}
|
||||
|
||||
class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _createWallet() async {
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
final password = await showDialog<String?>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('walletCreate').tr(),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('walletCreatePassword').tr(),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
controller: passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPassword'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text('cancel').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop(passwordController.text);
|
||||
},
|
||||
child: Text('next').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
passwordController.dispose();
|
||||
});
|
||||
if (password == null || password.isEmpty) return;
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/wa/wallets/me', data: {
|
||||
'password': password,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
child: Icon(Symbols.add, size: 28),
|
||||
),
|
||||
const Gap(12),
|
||||
Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(),
|
||||
const Gap(8),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: _isBusy ? null : () => _createWallet(),
|
||||
child: Text('next').tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ Future<ThemeData> createAppTheme(
|
||||
brightness: brightness,
|
||||
);
|
||||
|
||||
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||
final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||
|
||||
return ThemeData(
|
||||
@ -51,9 +51,9 @@ Future<ThemeData> createAppTheme(
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: hasAppBarBlurry ? 0 : null,
|
||||
backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
|
||||
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||
elevation: hasAppBarTransparent ? 0 : null,
|
||||
backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
|
||||
foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||
),
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
|
@ -15,12 +15,13 @@ class SnAccount with _$SnAccount {
|
||||
required DateTime? deletedAt,
|
||||
required DateTime? confirmedAt,
|
||||
required List<SnAccountContact>? contacts,
|
||||
required String avatar,
|
||||
required String banner,
|
||||
@Default("") String avatar,
|
||||
@Default("") String banner,
|
||||
required String description,
|
||||
required String name,
|
||||
required String nick,
|
||||
required Map<String, dynamic> permNodes,
|
||||
required String language,
|
||||
required SnAccountProfile? profile,
|
||||
@Default([]) List<SnAccountBadge> badges,
|
||||
required DateTime? suspendedAt,
|
||||
|
@ -33,6 +33,7 @@ mixin _$SnAccount {
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
String get nick => throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> get permNodes => throw _privateConstructorUsedError;
|
||||
String get language => throw _privateConstructorUsedError;
|
||||
SnAccountProfile? get profile => throw _privateConstructorUsedError;
|
||||
List<SnAccountBadge> get badges => throw _privateConstructorUsedError;
|
||||
DateTime? get suspendedAt => throw _privateConstructorUsedError;
|
||||
@ -69,6 +70,7 @@ abstract class $SnAccountCopyWith<$Res> {
|
||||
String name,
|
||||
String nick,
|
||||
Map<String, dynamic> permNodes,
|
||||
String language,
|
||||
SnAccountProfile? profile,
|
||||
List<SnAccountBadge> badges,
|
||||
DateTime? suspendedAt,
|
||||
@ -107,6 +109,7 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
|
||||
Object? name = null,
|
||||
Object? nick = null,
|
||||
Object? permNodes = null,
|
||||
Object? language = null,
|
||||
Object? profile = freezed,
|
||||
Object? badges = null,
|
||||
Object? suspendedAt = freezed,
|
||||
@ -164,6 +167,10 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
|
||||
? _value.permNodes
|
||||
: permNodes // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
language: null == language
|
||||
? _value.language
|
||||
: language // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
profile: freezed == profile
|
||||
? _value.profile
|
||||
: profile // ignore: cast_nullable_to_non_nullable
|
||||
@ -231,6 +238,7 @@ abstract class _$$SnAccountImplCopyWith<$Res>
|
||||
String name,
|
||||
String nick,
|
||||
Map<String, dynamic> permNodes,
|
||||
String language,
|
||||
SnAccountProfile? profile,
|
||||
List<SnAccountBadge> badges,
|
||||
DateTime? suspendedAt,
|
||||
@ -268,6 +276,7 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
||||
Object? name = null,
|
||||
Object? nick = null,
|
||||
Object? permNodes = null,
|
||||
Object? language = null,
|
||||
Object? profile = freezed,
|
||||
Object? badges = null,
|
||||
Object? suspendedAt = freezed,
|
||||
@ -325,6 +334,10 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
||||
? _value._permNodes
|
||||
: permNodes // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
language: null == language
|
||||
? _value.language
|
||||
: language // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
profile: freezed == profile
|
||||
? _value.profile
|
||||
: profile // ignore: cast_nullable_to_non_nullable
|
||||
@ -367,12 +380,13 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
required this.deletedAt,
|
||||
required this.confirmedAt,
|
||||
required final List<SnAccountContact>? contacts,
|
||||
required this.avatar,
|
||||
required this.banner,
|
||||
this.avatar = "",
|
||||
this.banner = "",
|
||||
required this.description,
|
||||
required this.name,
|
||||
required this.nick,
|
||||
required final Map<String, dynamic> permNodes,
|
||||
required this.language,
|
||||
required this.profile,
|
||||
final List<SnAccountBadge> badges = const [],
|
||||
required this.suspendedAt,
|
||||
@ -410,8 +424,10 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final String avatar;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String banner;
|
||||
@override
|
||||
final String description;
|
||||
@ -427,6 +443,8 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
return EqualUnmodifiableMapView(_permNodes);
|
||||
}
|
||||
|
||||
@override
|
||||
final String language;
|
||||
@override
|
||||
final SnAccountProfile? profile;
|
||||
final List<SnAccountBadge> _badges;
|
||||
@ -451,7 +469,7 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -477,6 +495,8 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
(identical(other.nick, nick) || other.nick == nick) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._permNodes, _permNodes) &&
|
||||
(identical(other.language, language) ||
|
||||
other.language == language) &&
|
||||
(identical(other.profile, profile) || other.profile == profile) &&
|
||||
const DeepCollectionEquality().equals(other._badges, _badges) &&
|
||||
(identical(other.suspendedAt, suspendedAt) ||
|
||||
@ -507,6 +527,7 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
name,
|
||||
nick,
|
||||
const DeepCollectionEquality().hash(_permNodes),
|
||||
language,
|
||||
profile,
|
||||
const DeepCollectionEquality().hash(_badges),
|
||||
suspendedAt,
|
||||
@ -540,12 +561,13 @@ abstract class _SnAccount extends SnAccount {
|
||||
required final DateTime? deletedAt,
|
||||
required final DateTime? confirmedAt,
|
||||
required final List<SnAccountContact>? contacts,
|
||||
required final String avatar,
|
||||
required final String banner,
|
||||
final String avatar,
|
||||
final String banner,
|
||||
required final String description,
|
||||
required final String name,
|
||||
required final String nick,
|
||||
required final Map<String, dynamic> permNodes,
|
||||
required final String language,
|
||||
required final SnAccountProfile? profile,
|
||||
final List<SnAccountBadge> badges,
|
||||
required final DateTime? suspendedAt,
|
||||
@ -584,6 +606,8 @@ abstract class _SnAccount extends SnAccount {
|
||||
@override
|
||||
Map<String, dynamic> get permNodes;
|
||||
@override
|
||||
String get language;
|
||||
@override
|
||||
SnAccountProfile? get profile;
|
||||
@override
|
||||
List<SnAccountBadge> get badges;
|
||||
|
@ -20,12 +20,13 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
|
||||
contacts: (json['contacts'] as List<dynamic>?)
|
||||
?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
avatar: json['avatar'] as String,
|
||||
banner: json['banner'] as String,
|
||||
avatar: json['avatar'] as String? ?? "",
|
||||
banner: json['banner'] as String? ?? "",
|
||||
description: json['description'] as String,
|
||||
name: json['name'] as String,
|
||||
nick: json['nick'] as String,
|
||||
permNodes: json['perm_nodes'] as Map<String, dynamic>,
|
||||
language: json['language'] as String,
|
||||
profile: json['profile'] == null
|
||||
? null
|
||||
: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
|
||||
@ -56,6 +57,7 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
|
||||
'name': instance.name,
|
||||
'nick': instance.nick,
|
||||
'perm_nodes': instance.permNodes,
|
||||
'language': instance.language,
|
||||
'profile': instance.profile?.toJson(),
|
||||
'badges': instance.badges.map((e) => e.toJson()).toList(),
|
||||
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
||||
|
@ -16,6 +16,7 @@ class SnCheckInRecord with _$SnCheckInRecord {
|
||||
required DateTime? deletedAt,
|
||||
required int resultTier,
|
||||
required int resultExperience,
|
||||
required double resultCoin,
|
||||
required List<int> resultModifiers,
|
||||
required int accountId,
|
||||
}) = _SnCheckInRecord;
|
||||
|
@ -26,6 +26,7 @@ mixin _$SnCheckInRecord {
|
||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||
int get resultTier => throw _privateConstructorUsedError;
|
||||
int get resultExperience => throw _privateConstructorUsedError;
|
||||
double get resultCoin => throw _privateConstructorUsedError;
|
||||
List<int> get resultModifiers => throw _privateConstructorUsedError;
|
||||
int get accountId => throw _privateConstructorUsedError;
|
||||
|
||||
@ -52,6 +53,7 @@ abstract class $SnCheckInRecordCopyWith<$Res> {
|
||||
DateTime? deletedAt,
|
||||
int resultTier,
|
||||
int resultExperience,
|
||||
double resultCoin,
|
||||
List<int> resultModifiers,
|
||||
int accountId});
|
||||
}
|
||||
@ -77,6 +79,7 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
||||
Object? deletedAt = freezed,
|
||||
Object? resultTier = null,
|
||||
Object? resultExperience = null,
|
||||
Object? resultCoin = null,
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
@ -105,6 +108,10 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
||||
? _value.resultExperience
|
||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultCoin: null == resultCoin
|
||||
? _value.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _value.resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
@ -132,6 +139,7 @@ abstract class _$$SnCheckInRecordImplCopyWith<$Res>
|
||||
DateTime? deletedAt,
|
||||
int resultTier,
|
||||
int resultExperience,
|
||||
double resultCoin,
|
||||
List<int> resultModifiers,
|
||||
int accountId});
|
||||
}
|
||||
@ -155,6 +163,7 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
||||
Object? deletedAt = freezed,
|
||||
Object? resultTier = null,
|
||||
Object? resultExperience = null,
|
||||
Object? resultCoin = null,
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
@ -183,6 +192,10 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
||||
? _value.resultExperience
|
||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultCoin: null == resultCoin
|
||||
? _value.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _value._resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
@ -205,6 +218,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
required this.deletedAt,
|
||||
required this.resultTier,
|
||||
required this.resultExperience,
|
||||
required this.resultCoin,
|
||||
required final List<int> resultModifiers,
|
||||
required this.accountId})
|
||||
: _resultModifiers = resultModifiers,
|
||||
@ -225,6 +239,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
final int resultTier;
|
||||
@override
|
||||
final int resultExperience;
|
||||
@override
|
||||
final double resultCoin;
|
||||
final List<int> _resultModifiers;
|
||||
@override
|
||||
List<int> get resultModifiers {
|
||||
@ -238,7 +254,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -257,6 +273,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
other.resultTier == resultTier) &&
|
||||
(identical(other.resultExperience, resultExperience) ||
|
||||
other.resultExperience == resultExperience) &&
|
||||
(identical(other.resultCoin, resultCoin) ||
|
||||
other.resultCoin == resultCoin) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._resultModifiers, _resultModifiers) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
@ -273,6 +291,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
deletedAt,
|
||||
resultTier,
|
||||
resultExperience,
|
||||
resultCoin,
|
||||
const DeepCollectionEquality().hash(_resultModifiers),
|
||||
accountId);
|
||||
|
||||
@ -301,6 +320,7 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
|
||||
required final DateTime? deletedAt,
|
||||
required final int resultTier,
|
||||
required final int resultExperience,
|
||||
required final double resultCoin,
|
||||
required final List<int> resultModifiers,
|
||||
required final int accountId}) = _$SnCheckInRecordImpl;
|
||||
const _SnCheckInRecord._() : super._();
|
||||
@ -321,6 +341,8 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
|
||||
@override
|
||||
int get resultExperience;
|
||||
@override
|
||||
double get resultCoin;
|
||||
@override
|
||||
List<int> get resultModifiers;
|
||||
@override
|
||||
int get accountId;
|
||||
|
@ -17,6 +17,7 @@ _$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson(
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
resultTier: (json['result_tier'] as num).toInt(),
|
||||
resultExperience: (json['result_experience'] as num).toInt(),
|
||||
resultCoin: (json['result_coin'] as num).toDouble(),
|
||||
resultModifiers: (json['result_modifiers'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
@ -32,6 +33,7 @@ Map<String, dynamic> _$$SnCheckInRecordImplToJson(
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'result_tier': instance.resultTier,
|
||||
'result_experience': instance.resultExperience,
|
||||
'result_coin': instance.resultCoin,
|
||||
'result_modifiers': instance.resultModifiers,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
45
lib/types/poll.dart
Normal file
45
lib/types/poll.dart
Normal file
@ -0,0 +1,45 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'poll.freezed.dart';
|
||||
part 'poll.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnPoll with _$SnPoll {
|
||||
const factory SnPoll({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required dynamic deletedAt,
|
||||
required dynamic expiredAt,
|
||||
required List<SnPollOption> options,
|
||||
required int accountId,
|
||||
required SnPollMetric metric,
|
||||
}) = _SnPoll;
|
||||
|
||||
factory SnPoll.fromJson(Map<String, Object?> json) => _$SnPollFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPollMetric with _$SnPollMetric {
|
||||
const factory SnPollMetric({
|
||||
required int totalAnswer,
|
||||
@Default({}) Map<String, int> byOptions,
|
||||
@Default({}) Map<String, double> byOptionsPercentage,
|
||||
}) = _SnPollMetric;
|
||||
|
||||
factory SnPollMetric.fromJson(Map<String, Object?> json) =>
|
||||
_$SnPollMetricFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPollOption with _$SnPollOption {
|
||||
const factory SnPollOption({
|
||||
required String id,
|
||||
required String icon,
|
||||
required String name,
|
||||
required String description,
|
||||
}) = _SnPollOption;
|
||||
|
||||
factory SnPollOption.fromJson(Map<String, Object?> json) =>
|
||||
_$SnPollOptionFromJson(json);
|
||||
}
|
761
lib/types/poll.freezed.dart
Normal file
761
lib/types/poll.freezed.dart
Normal file
@ -0,0 +1,761 @@
|
||||
// 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 'poll.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');
|
||||
|
||||
SnPoll _$SnPollFromJson(Map<String, dynamic> json) {
|
||||
return _SnPoll.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnPoll {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
dynamic get deletedAt => throw _privateConstructorUsedError;
|
||||
dynamic get expiredAt => throw _privateConstructorUsedError;
|
||||
List<SnPollOption> get options => throw _privateConstructorUsedError;
|
||||
int get accountId => throw _privateConstructorUsedError;
|
||||
SnPollMetric get metric => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPoll to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnPollCopyWith<SnPoll> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnPollCopyWith<$Res> {
|
||||
factory $SnPollCopyWith(SnPoll value, $Res Function(SnPoll) then) =
|
||||
_$SnPollCopyWithImpl<$Res, SnPoll>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
dynamic expiredAt,
|
||||
List<SnPollOption> options,
|
||||
int accountId,
|
||||
SnPollMetric metric});
|
||||
|
||||
$SnPollMetricCopyWith<$Res> get metric;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnPollCopyWithImpl<$Res, $Val extends SnPoll>
|
||||
implements $SnPollCopyWith<$Res> {
|
||||
_$SnPollCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// 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? expiredAt = freezed,
|
||||
Object? options = null,
|
||||
Object? accountId = null,
|
||||
Object? metric = 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 dynamic,
|
||||
expiredAt: freezed == expiredAt
|
||||
? _value.expiredAt
|
||||
: expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
options: null == options
|
||||
? _value.options
|
||||
: options // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPollOption>,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
metric: null == metric
|
||||
? _value.metric
|
||||
: metric // ignore: cast_nullable_to_non_nullable
|
||||
as SnPollMetric,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPollMetricCopyWith<$Res> get metric {
|
||||
return $SnPollMetricCopyWith<$Res>(_value.metric, (value) {
|
||||
return _then(_value.copyWith(metric: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnPollImplCopyWith<$Res> implements $SnPollCopyWith<$Res> {
|
||||
factory _$$SnPollImplCopyWith(
|
||||
_$SnPollImpl value, $Res Function(_$SnPollImpl) then) =
|
||||
__$$SnPollImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
dynamic expiredAt,
|
||||
List<SnPollOption> options,
|
||||
int accountId,
|
||||
SnPollMetric metric});
|
||||
|
||||
@override
|
||||
$SnPollMetricCopyWith<$Res> get metric;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnPollImplCopyWithImpl<$Res>
|
||||
extends _$SnPollCopyWithImpl<$Res, _$SnPollImpl>
|
||||
implements _$$SnPollImplCopyWith<$Res> {
|
||||
__$$SnPollImplCopyWithImpl(
|
||||
_$SnPollImpl _value, $Res Function(_$SnPollImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// 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? expiredAt = freezed,
|
||||
Object? options = null,
|
||||
Object? accountId = null,
|
||||
Object? metric = null,
|
||||
}) {
|
||||
return _then(_$SnPollImpl(
|
||||
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,
|
||||
expiredAt: freezed == expiredAt
|
||||
? _value.expiredAt
|
||||
: expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
options: null == options
|
||||
? _value._options
|
||||
: options // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPollOption>,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
metric: null == metric
|
||||
? _value.metric
|
||||
: metric // ignore: cast_nullable_to_non_nullable
|
||||
as SnPollMetric,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnPollImpl implements _SnPoll {
|
||||
const _$SnPollImpl(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.expiredAt,
|
||||
required final List<SnPollOption> options,
|
||||
required this.accountId,
|
||||
required this.metric})
|
||||
: _options = options;
|
||||
|
||||
factory _$SnPollImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnPollImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final dynamic deletedAt;
|
||||
@override
|
||||
final dynamic expiredAt;
|
||||
final List<SnPollOption> _options;
|
||||
@override
|
||||
List<SnPollOption> get options {
|
||||
if (_options is EqualUnmodifiableListView) return _options;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_options);
|
||||
}
|
||||
|
||||
@override
|
||||
final int accountId;
|
||||
@override
|
||||
final SnPollMetric metric;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPoll(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, expiredAt: $expiredAt, options: $options, accountId: $accountId, metric: $metric)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnPollImpl &&
|
||||
(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) &&
|
||||
const DeepCollectionEquality().equals(other.expiredAt, expiredAt) &&
|
||||
const DeepCollectionEquality().equals(other._options, _options) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId) &&
|
||||
(identical(other.metric, metric) || other.metric == metric));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
const DeepCollectionEquality().hash(deletedAt),
|
||||
const DeepCollectionEquality().hash(expiredAt),
|
||||
const DeepCollectionEquality().hash(_options),
|
||||
accountId,
|
||||
metric);
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnPollImplCopyWith<_$SnPollImpl> get copyWith =>
|
||||
__$$SnPollImplCopyWithImpl<_$SnPollImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnPollImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnPoll implements SnPoll {
|
||||
const factory _SnPoll(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final dynamic deletedAt,
|
||||
required final dynamic expiredAt,
|
||||
required final List<SnPollOption> options,
|
||||
required final int accountId,
|
||||
required final SnPollMetric metric}) = _$SnPollImpl;
|
||||
|
||||
factory _SnPoll.fromJson(Map<String, dynamic> json) = _$SnPollImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
dynamic get deletedAt;
|
||||
@override
|
||||
dynamic get expiredAt;
|
||||
@override
|
||||
List<SnPollOption> get options;
|
||||
@override
|
||||
int get accountId;
|
||||
@override
|
||||
SnPollMetric get metric;
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnPollImplCopyWith<_$SnPollImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnPollMetric _$SnPollMetricFromJson(Map<String, dynamic> json) {
|
||||
return _SnPollMetric.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnPollMetric {
|
||||
int get totalAnswer => throw _privateConstructorUsedError;
|
||||
Map<String, int> get byOptions => throw _privateConstructorUsedError;
|
||||
Map<String, double> get byOptionsPercentage =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPollMetric to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnPollMetricCopyWith<SnPollMetric> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnPollMetricCopyWith<$Res> {
|
||||
factory $SnPollMetricCopyWith(
|
||||
SnPollMetric value, $Res Function(SnPollMetric) then) =
|
||||
_$SnPollMetricCopyWithImpl<$Res, SnPollMetric>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int totalAnswer,
|
||||
Map<String, int> byOptions,
|
||||
Map<String, double> byOptionsPercentage});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnPollMetricCopyWithImpl<$Res, $Val extends SnPollMetric>
|
||||
implements $SnPollMetricCopyWith<$Res> {
|
||||
_$SnPollMetricCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? totalAnswer = null,
|
||||
Object? byOptions = null,
|
||||
Object? byOptionsPercentage = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
totalAnswer: null == totalAnswer
|
||||
? _value.totalAnswer
|
||||
: totalAnswer // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
byOptions: null == byOptions
|
||||
? _value.byOptions
|
||||
: byOptions // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, int>,
|
||||
byOptionsPercentage: null == byOptionsPercentage
|
||||
? _value.byOptionsPercentage
|
||||
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, double>,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnPollMetricImplCopyWith<$Res>
|
||||
implements $SnPollMetricCopyWith<$Res> {
|
||||
factory _$$SnPollMetricImplCopyWith(
|
||||
_$SnPollMetricImpl value, $Res Function(_$SnPollMetricImpl) then) =
|
||||
__$$SnPollMetricImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int totalAnswer,
|
||||
Map<String, int> byOptions,
|
||||
Map<String, double> byOptionsPercentage});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnPollMetricImplCopyWithImpl<$Res>
|
||||
extends _$SnPollMetricCopyWithImpl<$Res, _$SnPollMetricImpl>
|
||||
implements _$$SnPollMetricImplCopyWith<$Res> {
|
||||
__$$SnPollMetricImplCopyWithImpl(
|
||||
_$SnPollMetricImpl _value, $Res Function(_$SnPollMetricImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? totalAnswer = null,
|
||||
Object? byOptions = null,
|
||||
Object? byOptionsPercentage = null,
|
||||
}) {
|
||||
return _then(_$SnPollMetricImpl(
|
||||
totalAnswer: null == totalAnswer
|
||||
? _value.totalAnswer
|
||||
: totalAnswer // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
byOptions: null == byOptions
|
||||
? _value._byOptions
|
||||
: byOptions // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, int>,
|
||||
byOptionsPercentage: null == byOptionsPercentage
|
||||
? _value._byOptionsPercentage
|
||||
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, double>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnPollMetricImpl implements _SnPollMetric {
|
||||
const _$SnPollMetricImpl(
|
||||
{required this.totalAnswer,
|
||||
final Map<String, int> byOptions = const {},
|
||||
final Map<String, double> byOptionsPercentage = const {}})
|
||||
: _byOptions = byOptions,
|
||||
_byOptionsPercentage = byOptionsPercentage;
|
||||
|
||||
factory _$SnPollMetricImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnPollMetricImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int totalAnswer;
|
||||
final Map<String, int> _byOptions;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<String, int> get byOptions {
|
||||
if (_byOptions is EqualUnmodifiableMapView) return _byOptions;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_byOptions);
|
||||
}
|
||||
|
||||
final Map<String, double> _byOptionsPercentage;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<String, double> get byOptionsPercentage {
|
||||
if (_byOptionsPercentage is EqualUnmodifiableMapView)
|
||||
return _byOptionsPercentage;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_byOptionsPercentage);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPollMetric(totalAnswer: $totalAnswer, byOptions: $byOptions, byOptionsPercentage: $byOptionsPercentage)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnPollMetricImpl &&
|
||||
(identical(other.totalAnswer, totalAnswer) ||
|
||||
other.totalAnswer == totalAnswer) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._byOptions, _byOptions) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._byOptionsPercentage, _byOptionsPercentage));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
totalAnswer,
|
||||
const DeepCollectionEquality().hash(_byOptions),
|
||||
const DeepCollectionEquality().hash(_byOptionsPercentage));
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnPollMetricImplCopyWith<_$SnPollMetricImpl> get copyWith =>
|
||||
__$$SnPollMetricImplCopyWithImpl<_$SnPollMetricImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnPollMetricImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnPollMetric implements SnPollMetric {
|
||||
const factory _SnPollMetric(
|
||||
{required final int totalAnswer,
|
||||
final Map<String, int> byOptions,
|
||||
final Map<String, double> byOptionsPercentage}) = _$SnPollMetricImpl;
|
||||
|
||||
factory _SnPollMetric.fromJson(Map<String, dynamic> json) =
|
||||
_$SnPollMetricImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get totalAnswer;
|
||||
@override
|
||||
Map<String, int> get byOptions;
|
||||
@override
|
||||
Map<String, double> get byOptionsPercentage;
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnPollMetricImplCopyWith<_$SnPollMetricImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnPollOption _$SnPollOptionFromJson(Map<String, dynamic> json) {
|
||||
return _SnPollOption.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnPollOption {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
String get icon => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
String get description => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPollOption to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnPollOptionCopyWith<SnPollOption> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnPollOptionCopyWith<$Res> {
|
||||
factory $SnPollOptionCopyWith(
|
||||
SnPollOption value, $Res Function(SnPollOption) then) =
|
||||
_$SnPollOptionCopyWithImpl<$Res, SnPollOption>;
|
||||
@useResult
|
||||
$Res call({String id, String icon, String name, String description});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnPollOptionCopyWithImpl<$Res, $Val extends SnPollOption>
|
||||
implements $SnPollOptionCopyWith<$Res> {
|
||||
_$SnPollOptionCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? icon = null,
|
||||
Object? name = null,
|
||||
Object? description = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
icon: null == icon
|
||||
? _value.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnPollOptionImplCopyWith<$Res>
|
||||
implements $SnPollOptionCopyWith<$Res> {
|
||||
factory _$$SnPollOptionImplCopyWith(
|
||||
_$SnPollOptionImpl value, $Res Function(_$SnPollOptionImpl) then) =
|
||||
__$$SnPollOptionImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String id, String icon, String name, String description});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnPollOptionImplCopyWithImpl<$Res>
|
||||
extends _$SnPollOptionCopyWithImpl<$Res, _$SnPollOptionImpl>
|
||||
implements _$$SnPollOptionImplCopyWith<$Res> {
|
||||
__$$SnPollOptionImplCopyWithImpl(
|
||||
_$SnPollOptionImpl _value, $Res Function(_$SnPollOptionImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? icon = null,
|
||||
Object? name = null,
|
||||
Object? description = null,
|
||||
}) {
|
||||
return _then(_$SnPollOptionImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
icon: null == icon
|
||||
? _value.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnPollOptionImpl implements _SnPollOption {
|
||||
const _$SnPollOptionImpl(
|
||||
{required this.id,
|
||||
required this.icon,
|
||||
required this.name,
|
||||
required this.description});
|
||||
|
||||
factory _$SnPollOptionImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnPollOptionImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final String icon;
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final String description;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPollOption(id: $id, icon: $icon, name: $name, description: $description)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnPollOptionImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.icon, icon) || other.icon == icon) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.description, description) ||
|
||||
other.description == description));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, icon, name, description);
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnPollOptionImplCopyWith<_$SnPollOptionImpl> get copyWith =>
|
||||
__$$SnPollOptionImplCopyWithImpl<_$SnPollOptionImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnPollOptionImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnPollOption implements SnPollOption {
|
||||
const factory _SnPollOption(
|
||||
{required final String id,
|
||||
required final String icon,
|
||||
required final String name,
|
||||
required final String description}) = _$SnPollOptionImpl;
|
||||
|
||||
factory _SnPollOption.fromJson(Map<String, dynamic> json) =
|
||||
_$SnPollOptionImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get id;
|
||||
@override
|
||||
String get icon;
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
String get description;
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnPollOptionImplCopyWith<_$SnPollOptionImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
69
lib/types/poll.g.dart
Normal file
69
lib/types/poll.g.dart
Normal file
@ -0,0 +1,69 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'poll.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnPollImpl _$$SnPollImplFromJson(Map<String, dynamic> json) => _$SnPollImpl(
|
||||
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'],
|
||||
expiredAt: json['expired_at'],
|
||||
options: (json['options'] as List<dynamic>)
|
||||
.map((e) => SnPollOption.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
metric: SnPollMetric.fromJson(json['metric'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPollImplToJson(_$SnPollImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt,
|
||||
'expired_at': instance.expiredAt,
|
||||
'options': instance.options.map((e) => e.toJson()).toList(),
|
||||
'account_id': instance.accountId,
|
||||
'metric': instance.metric.toJson(),
|
||||
};
|
||||
|
||||
_$SnPollMetricImpl _$$SnPollMetricImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPollMetricImpl(
|
||||
totalAnswer: (json['total_answer'] as num).toInt(),
|
||||
byOptions: (json['by_options'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toInt()),
|
||||
) ??
|
||||
const {},
|
||||
byOptionsPercentage:
|
||||
(json['by_options_percentage'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toDouble()),
|
||||
) ??
|
||||
const {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPollMetricImplToJson(_$SnPollMetricImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'total_answer': instance.totalAnswer,
|
||||
'by_options': instance.byOptions,
|
||||
'by_options_percentage': instance.byOptionsPercentage,
|
||||
};
|
||||
|
||||
_$SnPollOptionImpl _$$SnPollOptionImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPollOptionImpl(
|
||||
id: json['id'] as String,
|
||||
icon: json['icon'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPollOptionImplToJson(_$SnPollOptionImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'icon': instance.icon,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
|
||||
part 'post.freezed.dart';
|
||||
part 'post.g.dart';
|
||||
@ -37,6 +38,7 @@ class SnPost with _$SnPost {
|
||||
required int totalUpvote,
|
||||
required int totalDownvote,
|
||||
required int publisherId,
|
||||
required int? pollId,
|
||||
required SnPublisher publisher,
|
||||
required SnMetric metric,
|
||||
SnPostPreload? preload,
|
||||
@ -89,6 +91,8 @@ class SnPostPreload with _$SnPostPreload {
|
||||
const factory SnPostPreload({
|
||||
required SnAttachment? thumbnail,
|
||||
required List<SnAttachment?>? attachments,
|
||||
required SnAttachment? video,
|
||||
required SnPoll? poll,
|
||||
}) = _SnPostPreload;
|
||||
|
||||
factory SnPostPreload.fromJson(Map<String, Object?> json) =>
|
||||
|
@ -48,6 +48,7 @@ mixin _$SnPost {
|
||||
int get totalUpvote => throw _privateConstructorUsedError;
|
||||
int get totalDownvote => throw _privateConstructorUsedError;
|
||||
int get publisherId => throw _privateConstructorUsedError;
|
||||
int? get pollId => throw _privateConstructorUsedError;
|
||||
SnPublisher get publisher => throw _privateConstructorUsedError;
|
||||
SnMetric get metric => throw _privateConstructorUsedError;
|
||||
SnPostPreload? get preload => throw _privateConstructorUsedError;
|
||||
@ -95,6 +96,7 @@ abstract class $SnPostCopyWith<$Res> {
|
||||
int totalUpvote,
|
||||
int totalDownvote,
|
||||
int publisherId,
|
||||
int? pollId,
|
||||
SnPublisher publisher,
|
||||
SnMetric metric,
|
||||
SnPostPreload? preload});
|
||||
@ -149,6 +151,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
Object? totalUpvote = null,
|
||||
Object? totalDownvote = null,
|
||||
Object? publisherId = null,
|
||||
Object? pollId = freezed,
|
||||
Object? publisher = null,
|
||||
Object? metric = null,
|
||||
Object? preload = freezed,
|
||||
@ -266,6 +269,10 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
? _value.publisherId
|
||||
: publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
pollId: freezed == pollId
|
||||
? _value.pollId
|
||||
: pollId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
publisher: null == publisher
|
||||
? _value.publisher
|
||||
: publisher // ignore: cast_nullable_to_non_nullable
|
||||
@ -380,6 +387,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
||||
int totalUpvote,
|
||||
int totalDownvote,
|
||||
int publisherId,
|
||||
int? pollId,
|
||||
SnPublisher publisher,
|
||||
SnMetric metric,
|
||||
SnPostPreload? preload});
|
||||
@ -437,6 +445,7 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
||||
Object? totalUpvote = null,
|
||||
Object? totalDownvote = null,
|
||||
Object? publisherId = null,
|
||||
Object? pollId = freezed,
|
||||
Object? publisher = null,
|
||||
Object? metric = null,
|
||||
Object? preload = freezed,
|
||||
@ -554,6 +563,10 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
||||
? _value.publisherId
|
||||
: publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
pollId: freezed == pollId
|
||||
? _value.pollId
|
||||
: pollId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
publisher: null == publisher
|
||||
? _value.publisher
|
||||
: publisher // ignore: cast_nullable_to_non_nullable
|
||||
@ -602,6 +615,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
required this.totalUpvote,
|
||||
required this.totalDownvote,
|
||||
required this.publisherId,
|
||||
required this.pollId,
|
||||
required this.publisher,
|
||||
required this.metric,
|
||||
this.preload})
|
||||
@ -719,6 +733,8 @@ class _$SnPostImpl extends _SnPost {
|
||||
@override
|
||||
final int publisherId;
|
||||
@override
|
||||
final int? pollId;
|
||||
@override
|
||||
final SnPublisher publisher;
|
||||
@override
|
||||
final SnMetric metric;
|
||||
@ -727,7 +743,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, publisherId: $publisherId, publisher: $publisher, metric: $metric, preload: $preload)';
|
||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -782,6 +798,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
other.totalDownvote == totalDownvote) &&
|
||||
(identical(other.publisherId, publisherId) ||
|
||||
other.publisherId == publisherId) &&
|
||||
(identical(other.pollId, pollId) || other.pollId == pollId) &&
|
||||
(identical(other.publisher, publisher) ||
|
||||
other.publisher == publisher) &&
|
||||
(identical(other.metric, metric) || other.metric == metric) &&
|
||||
@ -820,6 +837,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
totalUpvote,
|
||||
totalDownvote,
|
||||
publisherId,
|
||||
pollId,
|
||||
publisher,
|
||||
metric,
|
||||
preload
|
||||
@ -871,6 +889,7 @@ abstract class _SnPost extends SnPost {
|
||||
required final int totalUpvote,
|
||||
required final int totalDownvote,
|
||||
required final int publisherId,
|
||||
required final int? pollId,
|
||||
required final SnPublisher publisher,
|
||||
required final SnMetric metric,
|
||||
final SnPostPreload? preload}) = _$SnPostImpl;
|
||||
@ -935,6 +954,8 @@ abstract class _SnPost extends SnPost {
|
||||
@override
|
||||
int get publisherId;
|
||||
@override
|
||||
int? get pollId;
|
||||
@override
|
||||
SnPublisher get publisher;
|
||||
@override
|
||||
SnMetric get metric;
|
||||
@ -1567,6 +1588,8 @@ SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) {
|
||||
mixin _$SnPostPreload {
|
||||
SnAttachment? get thumbnail => throw _privateConstructorUsedError;
|
||||
List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
|
||||
SnAttachment? get video => throw _privateConstructorUsedError;
|
||||
SnPoll? get poll => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPostPreload to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@ -1584,9 +1607,15 @@ abstract class $SnPostPreloadCopyWith<$Res> {
|
||||
SnPostPreload value, $Res Function(SnPostPreload) then) =
|
||||
_$SnPostPreloadCopyWithImpl<$Res, SnPostPreload>;
|
||||
@useResult
|
||||
$Res call({SnAttachment? thumbnail, List<SnAttachment?>? attachments});
|
||||
$Res call(
|
||||
{SnAttachment? thumbnail,
|
||||
List<SnAttachment?>? attachments,
|
||||
SnAttachment? video,
|
||||
SnPoll? poll});
|
||||
|
||||
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
||||
$SnAttachmentCopyWith<$Res>? get video;
|
||||
$SnPollCopyWith<$Res>? get poll;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -1606,6 +1635,8 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
|
||||
$Res call({
|
||||
Object? thumbnail = freezed,
|
||||
Object? attachments = freezed,
|
||||
Object? video = freezed,
|
||||
Object? poll = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
thumbnail: freezed == thumbnail
|
||||
@ -1616,6 +1647,14 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
|
||||
? _value.attachments
|
||||
: attachments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnAttachment?>?,
|
||||
video: freezed == video
|
||||
? _value.video
|
||||
: video // ignore: cast_nullable_to_non_nullable
|
||||
as SnAttachment?,
|
||||
poll: freezed == poll
|
||||
? _value.poll
|
||||
: poll // ignore: cast_nullable_to_non_nullable
|
||||
as SnPoll?,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@ -1632,6 +1671,34 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
|
||||
return _then(_value.copyWith(thumbnail: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAttachmentCopyWith<$Res>? get video {
|
||||
if (_value.video == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnAttachmentCopyWith<$Res>(_value.video!, (value) {
|
||||
return _then(_value.copyWith(video: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPollCopyWith<$Res>? get poll {
|
||||
if (_value.poll == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPollCopyWith<$Res>(_value.poll!, (value) {
|
||||
return _then(_value.copyWith(poll: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -1642,10 +1709,18 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res>
|
||||
__$$SnPostPreloadImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({SnAttachment? thumbnail, List<SnAttachment?>? attachments});
|
||||
$Res call(
|
||||
{SnAttachment? thumbnail,
|
||||
List<SnAttachment?>? attachments,
|
||||
SnAttachment? video,
|
||||
SnPoll? poll});
|
||||
|
||||
@override
|
||||
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
||||
@override
|
||||
$SnAttachmentCopyWith<$Res>? get video;
|
||||
@override
|
||||
$SnPollCopyWith<$Res>? get poll;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -1663,6 +1738,8 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
|
||||
$Res call({
|
||||
Object? thumbnail = freezed,
|
||||
Object? attachments = freezed,
|
||||
Object? video = freezed,
|
||||
Object? poll = freezed,
|
||||
}) {
|
||||
return _then(_$SnPostPreloadImpl(
|
||||
thumbnail: freezed == thumbnail
|
||||
@ -1673,6 +1750,14 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
|
||||
? _value._attachments
|
||||
: attachments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnAttachment?>?,
|
||||
video: freezed == video
|
||||
? _value.video
|
||||
: video // ignore: cast_nullable_to_non_nullable
|
||||
as SnAttachment?,
|
||||
poll: freezed == poll
|
||||
? _value.poll
|
||||
: poll // ignore: cast_nullable_to_non_nullable
|
||||
as SnPoll?,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -1682,7 +1767,9 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
|
||||
class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||
const _$SnPostPreloadImpl(
|
||||
{required this.thumbnail,
|
||||
required final List<SnAttachment?>? attachments})
|
||||
required final List<SnAttachment?>? attachments,
|
||||
required this.video,
|
||||
required this.poll})
|
||||
: _attachments = attachments;
|
||||
|
||||
factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) =>
|
||||
@ -1700,9 +1787,14 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
final SnAttachment? video;
|
||||
@override
|
||||
final SnPoll? poll;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments)';
|
||||
return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video, poll: $poll)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1713,13 +1805,15 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||
(identical(other.thumbnail, thumbnail) ||
|
||||
other.thumbnail == thumbnail) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._attachments, _attachments));
|
||||
.equals(other._attachments, _attachments) &&
|
||||
(identical(other.video, video) || other.video == video) &&
|
||||
(identical(other.poll, poll) || other.poll == poll));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, thumbnail,
|
||||
const DeepCollectionEquality().hash(_attachments));
|
||||
const DeepCollectionEquality().hash(_attachments), video, poll);
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -1740,7 +1834,9 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||
abstract class _SnPostPreload implements SnPostPreload {
|
||||
const factory _SnPostPreload(
|
||||
{required final SnAttachment? thumbnail,
|
||||
required final List<SnAttachment?>? attachments}) = _$SnPostPreloadImpl;
|
||||
required final List<SnAttachment?>? attachments,
|
||||
required final SnAttachment? video,
|
||||
required final SnPoll? poll}) = _$SnPostPreloadImpl;
|
||||
|
||||
factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
|
||||
_$SnPostPreloadImpl.fromJson;
|
||||
@ -1749,6 +1845,10 @@ abstract class _SnPostPreload implements SnPostPreload {
|
||||
SnAttachment? get thumbnail;
|
||||
@override
|
||||
List<SnAttachment?>? get attachments;
|
||||
@override
|
||||
SnAttachment? get video;
|
||||
@override
|
||||
SnPoll? get poll;
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
@ -63,6 +63,7 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
||||
totalUpvote: (json['total_upvote'] as num).toInt(),
|
||||
totalDownvote: (json['total_downvote'] as num).toInt(),
|
||||
publisherId: (json['publisher_id'] as num).toInt(),
|
||||
pollId: (json['poll_id'] as num?)?.toInt(),
|
||||
publisher:
|
||||
SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
||||
metric: SnMetric.fromJson(json['metric'] as Map<String, dynamic>),
|
||||
@ -101,6 +102,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
||||
'total_upvote': instance.totalUpvote,
|
||||
'total_downvote': instance.totalDownvote,
|
||||
'publisher_id': instance.publisherId,
|
||||
'poll_id': instance.pollId,
|
||||
'publisher': instance.publisher.toJson(),
|
||||
'metric': instance.metric.toJson(),
|
||||
'preload': instance.preload?.toJson(),
|
||||
@ -165,12 +167,20 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
|
||||
? null
|
||||
: SnAttachment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
video: json['video'] == null
|
||||
? null
|
||||
: SnAttachment.fromJson(json['video'] as Map<String, dynamic>),
|
||||
poll: json['poll'] == null
|
||||
? null
|
||||
: SnPoll.fromJson(json['poll'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'thumbnail': instance.thumbnail?.toJson(),
|
||||
'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
|
||||
'video': instance.video?.toJson(),
|
||||
'poll': instance.poll?.toJson(),
|
||||
};
|
||||
|
||||
_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
|
||||
|
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,
|
||||
};
|
@ -1,9 +1,12 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
|
||||
@ -47,10 +50,19 @@ class _AccountSelectState extends State<AccountSelect> {
|
||||
Future<void> _getFriends() async {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
|
||||
if (!mounted) return;
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
setState(() {
|
||||
_relativeUsers.addAll(
|
||||
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
|
||||
resp.data?.map((e) {
|
||||
final rel = SnRelationship.fromJson(e);
|
||||
if (rel.relatedId == ua.user?.id) {
|
||||
return rel.account!;
|
||||
} else {
|
||||
return rel.related!;
|
||||
}
|
||||
}).cast<SnAccount>(),
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -96,10 +108,14 @@ class _AccountSelectState extends State<AccountSelect> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.title,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).padding(left: 24, right: 24, top: 16, bottom: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.group, size: 24),
|
||||
const Gap(16),
|
||||
Text(widget.title, style: Theme.of(context).textTheme.titleLarge),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
@ -117,13 +133,9 @@ class _AccountSelectState extends State<AccountSelect> {
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _pendingUsers.isEmpty
|
||||
? _relativeUsers.length
|
||||
: _pendingUsers.length,
|
||||
itemCount: _pendingUsers.isEmpty ? _relativeUsers.length : _pendingUsers.length,
|
||||
itemBuilder: (context, index) {
|
||||
var user = _pendingUsers.isEmpty
|
||||
? _relativeUsers[index]
|
||||
: _pendingUsers[index];
|
||||
var user = _pendingUsers.isEmpty ? _relativeUsers[index] : _pendingUsers[index];
|
||||
return ListTile(
|
||||
title: Text(user.nick),
|
||||
subtitle: Text(user.name),
|
||||
@ -142,8 +154,7 @@ class _AccountSelectState extends State<AccountSelect> {
|
||||
}
|
||||
|
||||
setState(() {
|
||||
final idx = _selectedUsers
|
||||
.indexWhere((x) => x.id == user.id);
|
||||
final idx = _selectedUsers.indexWhere((x) => x.id == user.id);
|
||||
if (idx != -1) {
|
||||
_selectedUsers.removeAt(idx);
|
||||
} else {
|
||||
|
@ -6,12 +6,22 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class AttachmentInputDialog extends StatefulWidget {
|
||||
final String? title;
|
||||
final bool? analyzeNow;
|
||||
const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false});
|
||||
final bool? analyzeNow;
|
||||
final SnMediaType? mediaType;
|
||||
final String pool;
|
||||
|
||||
const AttachmentInputDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.pool,
|
||||
this.analyzeNow = false,
|
||||
this.mediaType = SnMediaType.image,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
|
||||
@ -20,13 +30,18 @@ final bool? analyzeNow;
|
||||
class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
final _randomIdController = TextEditingController();
|
||||
|
||||
XFile? _thumbnailFile;
|
||||
XFile? _file;
|
||||
double? _progress;
|
||||
|
||||
void _pickImage() async {
|
||||
void _pickMedia() async {
|
||||
final picker = ImagePicker();
|
||||
final result = await picker.pickImage(source: ImageSource.gallery);
|
||||
final result = switch (widget.mediaType) {
|
||||
SnMediaType.image => await picker.pickImage(source: ImageSource.gallery),
|
||||
SnMediaType.video => await picker.pickVideo(source: ImageSource.gallery),
|
||||
_ => await picker.pickMedia(),
|
||||
};
|
||||
if (result == null) return;
|
||||
setState(() => _thumbnailFile = result);
|
||||
setState(() => _file = result);
|
||||
}
|
||||
|
||||
bool _isBusy = false;
|
||||
@ -46,15 +61,20 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
} else if (_thumbnailFile != null) {
|
||||
} else if (_file != null) {
|
||||
try {
|
||||
final attachment = await attach.directUploadOne(
|
||||
(await _thumbnailFile!.readAsBytes()).buffer.asUint8List(),
|
||||
_thumbnailFile!.path,
|
||||
'interactive',
|
||||
null,
|
||||
final place = await attach.chunkedUploadInitialize(await _file!.length(), _file!.name, widget.pool, null);
|
||||
|
||||
final attachment = await attach.chunkedUploadParts(
|
||||
_file!,
|
||||
place.$1,
|
||||
place.$2,
|
||||
analyzeNow: widget.analyzeNow ?? false,
|
||||
onProgress: (value) {
|
||||
setState(() => _progress = value);
|
||||
},
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, attachment);
|
||||
} catch (err) {
|
||||
@ -67,7 +87,7 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.title ?? 'attachmentInputDialog').tr(),
|
||||
title: Text(widget.title ?? 'attachmentInputDialog'.tr()),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -86,24 +106,35 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
const Gap(24),
|
||||
Text('attachmentInputNew').tr().fontSize(14),
|
||||
Card(
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: const Icon(Symbols.add_photo_alternate),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('addAttachmentFromAlbum').tr(),
|
||||
subtitle: _thumbnailFile == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
|
||||
onTap: () {
|
||||
_pickImage();
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: const Icon(Symbols.add_photo_alternate),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('addAttachmentFromAlbum').tr(),
|
||||
subtitle: _file == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
|
||||
onTap: () {
|
||||
_pickMedia();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_isBusy)
|
||||
LinearProgressIndicator(
|
||||
value: _progress,
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
).padding(top: 16),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('dialogDismiss').tr(),
|
||||
),
|
||||
TextButton(
|
||||
|
@ -196,68 +196,71 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||
child: ScrollConfiguration(
|
||||
behavior: _AttachmentListScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
padding: widget.padding,
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.data.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
|
||||
child: AspectRatio(
|
||||
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
return AspectRatio(
|
||||
aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||
child: ScrollConfiguration(
|
||||
behavior: _AttachmentListScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
padding: widget.padding,
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.data.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
|
||||
child: AspectRatio(
|
||||
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AttachmentItem(
|
||||
data: widget.data[idx],
|
||||
heroTag: heroTags[idx],
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AttachmentItem(
|
||||
data: widget.data[idx],
|
||||
heroTag: heroTags[idx],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(
|
||||
label: Text('${idx + 1}/${widget.data.length}'),
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(
|
||||
label: Text('${idx + 1}/${widget.data.length}'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -152,6 +152,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Stack(
|
||||
children: [
|
||||
Builder(builder: (context) {
|
||||
|
@ -1,17 +1,28 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' show min;
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/chat_message_controller.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_sticker.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class ChatMessageInput extends StatefulWidget {
|
||||
final ChatMessageController controller;
|
||||
@ -32,9 +43,39 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
final TextEditingController _contentController = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
final HotKey _pasteHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.keyV,
|
||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
final HotKey _newLineHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.enter,
|
||||
modifiers: [(!kIsWeb && 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(() {});
|
||||
});
|
||||
hotKeyManager.register(_newLineHotKey, keyDownHandler: (_) async {
|
||||
if (_contentController.text.isEmpty) return;
|
||||
_contentController.text += '\n';
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registerHotKey();
|
||||
_contentController.addListener(() {
|
||||
if (_contentController.text.isNotEmpty) {
|
||||
widget.controller.pingTypingStatus();
|
||||
@ -46,6 +87,16 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
setState(() => _replyingMessage = value);
|
||||
}
|
||||
|
||||
void setInitialText(String? value) {
|
||||
_contentController.text = value ?? '';
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setInitialAttachments(List<PostWriteMedia>? value) {
|
||||
_attachments.addAll(value ?? []);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setEdit(SnChatMessage? value) {
|
||||
_contentController.text = value?.body['text'] ?? '';
|
||||
_attachments.clear();
|
||||
@ -70,6 +121,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
if (_contentController.text.isEmpty && _attachments.isEmpty) return;
|
||||
if (_isBusy) return;
|
||||
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
@ -134,10 +186,38 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
|
||||
final List<PostWriteMedia> _attachments = List.empty(growable: true);
|
||||
|
||||
OverlayEntry? _overlayEntry;
|
||||
|
||||
void _showEmojiPicker(BuildContext context) {
|
||||
final overlay = Overlay.of(context);
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||
right: 16,
|
||||
child: _StickerPicker(
|
||||
originalText: _contentController.text,
|
||||
onDismiss: () => _dismissEmojiPicker(),
|
||||
onInsert: (str) => _contentController.text = str,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
overlay.insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
void _dismissEmojiPicker() {
|
||||
_overlayEntry?.remove();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contentController.dispose();
|
||||
_focusNode.dispose();
|
||||
_dismissEmojiPicker();
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
|
||||
hotKeyManager.unregister(_pasteHotKey);
|
||||
hotKeyManager.unregister(_newLineHotKey);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -270,15 +350,29 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
: 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
textInputAction: TextInputAction.send,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onSubmitted: (_) {
|
||||
if (_isBusy) return;
|
||||
_sendMessage();
|
||||
_focusNode.requestFocus();
|
||||
},
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Symbols.mood,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
_showEmojiPicker(context);
|
||||
},
|
||||
),
|
||||
AddPostMediaButton(
|
||||
onAdd: (items) {
|
||||
setState(() {
|
||||
@ -292,10 +386,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
Symbols.send,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -304,3 +397,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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
|
||||
@ -13,6 +14,9 @@ class ConnectionIndicator extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ws = context.watch<WebSocketProvider>();
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
|
||||
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: ws,
|
||||
@ -22,45 +26,50 @@ class ConnectionIndicator extends StatelessWidget {
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: !show,
|
||||
child: GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
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();
|
||||
}
|
||||
},
|
||||
),
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (ws.isBusy)
|
||||
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
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();
|
||||
}
|
||||
},
|
||||
),
|
||||
).padding(left: marginLeft),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -28,7 +28,7 @@ class ContextMenuArea extends StatelessWidget {
|
||||
// Leave padding for side navigation
|
||||
mousePosition = cfg.drawerIsExpanded
|
||||
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
||||
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
|
||||
: mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
|
||||
}
|
||||
},
|
||||
child: GestureDetector(
|
||||
|
@ -2,7 +2,9 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
extension AppPromptExtension on BuildContext {
|
||||
void showSnackbar(String content, {SnackBarAction? action}) {
|
||||
@ -111,7 +113,34 @@ extension AppPromptExtension on BuildContext {
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('dialogError').tr(),
|
||||
content: content,
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 20,
|
||||
children: [
|
||||
content,
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: 'needHelp'.tr(),
|
||||
children: [
|
||||
TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: 'needHelpLaunch'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(ctx).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: Theme.of(ctx).colorScheme.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlString('https://kb.solsynth.dev/solar-network');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
@ -128,17 +157,7 @@ extension ByteFormatter on int {
|
||||
if (this == 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
final dm = decimals < 0 ? 0 : decimals;
|
||||
final sizes = [
|
||||
'Bytes',
|
||||
'KiB',
|
||||
'MiB',
|
||||
'GiB',
|
||||
'TiB',
|
||||
'PiB',
|
||||
'EiB',
|
||||
'ZiB',
|
||||
'YiB'
|
||||
];
|
||||
final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
final i = (math.log(this) / math.log(k)).floor().toInt();
|
||||
return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
final bool isAutoWarp;
|
||||
final bool isEnlargeSticker;
|
||||
final TextScaler? textScaler;
|
||||
final Color? textColor;
|
||||
final List<SnAttachment?>? attachments;
|
||||
|
||||
const MarkdownTextContent({
|
||||
@ -28,6 +29,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
this.isAutoWarp = false,
|
||||
this.isEnlargeSticker = false,
|
||||
this.textScaler,
|
||||
this.textColor,
|
||||
this.attachments,
|
||||
});
|
||||
|
||||
@ -42,6 +44,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
Theme.of(context),
|
||||
).copyWith(
|
||||
textScaler: textScaler,
|
||||
p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null,
|
||||
blockquote: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@ -126,14 +129,27 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
future: st.lookupSticker(alias),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return UniversalImage(
|
||||
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
|
||||
fit: BoxFit.cover,
|
||||
width: size,
|
||||
height: size,
|
||||
cacheHeight: size,
|
||||
cacheWidth: size,
|
||||
);
|
||||
return GestureDetector(
|
||||
child: UniversalImage(
|
||||
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
|
||||
fit: BoxFit.contain,
|
||||
width: size,
|
||||
height: size,
|
||||
cacheHeight: size,
|
||||
cacheWidth: size,
|
||||
),
|
||||
onTap: () {
|
||||
if (snapshot.data == null) return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: [snapshot.data!.attachment],
|
||||
initialIndex: 0,
|
||||
heroTags: [const Uuid().v4()],
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
@ -142,7 +158,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
);
|
||||
case 'attachments':
|
||||
final attachment = attachments?.firstWhere(
|
||||
(ele) => ele?.rid == segments[1],
|
||||
(ele) => ele?.rid == segments[1],
|
||||
orElse: () => null,
|
||||
);
|
||||
if (attachment != null) {
|
||||
|
@ -31,34 +31,37 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
|
||||
builder: (context, _) {
|
||||
final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
|
||||
|
||||
return NavigationRail(
|
||||
selectedIndex:
|
||||
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
|
||||
destinations: [
|
||||
...destinations.where((ele) => ele.isPinned).map((ele) {
|
||||
return NavigationRailDestination(
|
||||
icon: ele.icon,
|
||||
label: Text(ele.label).tr(),
|
||||
);
|
||||
}),
|
||||
],
|
||||
trailing: Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: StyledWidget(
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.menu),
|
||||
onPressed: () {
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
),
|
||||
).padding(bottom: 16),
|
||||
return SizedBox(
|
||||
width: 80,
|
||||
child: NavigationRail(
|
||||
selectedIndex:
|
||||
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
|
||||
destinations: [
|
||||
...destinations.where((ele) => ele.isPinned).map((ele) {
|
||||
return NavigationRailDestination(
|
||||
icon: ele.icon,
|
||||
label: Text(ele.label).tr(),
|
||||
);
|
||||
}),
|
||||
],
|
||||
trailing: Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: StyledWidget(
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.menu),
|
||||
onPressed: () {
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
),
|
||||
).padding(bottom: 16),
|
||||
),
|
||||
),
|
||||
onDestinationSelected: (idx) {
|
||||
nav.setIndex(idx);
|
||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||
},
|
||||
),
|
||||
onDestinationSelected: (idx) {
|
||||
nav.setIndex(idx);
|
||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -58,6 +58,7 @@ class AppScaffold extends StatelessWidget {
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: SizedBox.expand(
|
||||
child: AppBackground(
|
||||
isRoot: true,
|
||||
child: Column(
|
||||
children: [
|
||||
IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)),
|
||||
@ -140,6 +141,7 @@ class AppRootScaffold extends StatelessWidget {
|
||||
);
|
||||
|
||||
final safeTop = MediaQuery.of(context).padding.top;
|
||||
final safeBottom = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
return Scaffold(
|
||||
key: globalRootScaffoldKey,
|
||||
@ -191,7 +193,10 @@ class AppRootScaffold extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
|
||||
Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()),
|
||||
if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE))
|
||||
Positioned(bottom: safeBottom > 0 ? safeBottom : 16, left: 0, right: 0, child: ConnectionIndicator())
|
||||
else
|
||||
Positioned(top: safeTop > 0 ? safeTop : 16, left: 0, right: 0, child: ConnectionIndicator()),
|
||||
],
|
||||
),
|
||||
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
||||
|
@ -1,60 +1,181 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.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/userinfo.dart';
|
||||
import 'package:surface/screens/notification.dart';
|
||||
import 'package:surface/types/notification.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class NotifyIndicator extends StatelessWidget {
|
||||
import 'markdown_content.dart';
|
||||
|
||||
class NotifyIndicator extends StatefulWidget {
|
||||
const NotifyIndicator({super.key});
|
||||
|
||||
@override
|
||||
State<NotifyIndicator> createState() => _NotifyIndicatorState();
|
||||
}
|
||||
|
||||
class _NotifyIndicatorState extends State<NotifyIndicator> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
|
||||
void _markOneAsRead(SnNotification notification) async {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) return;
|
||||
|
||||
if (notification.id == 0) return;
|
||||
if (notification.readAt != null) return;
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.put('/cgi/id/notifications/read/${notification.id}');
|
||||
|
||||
if (!mounted) return;
|
||||
context.showSnackbar(
|
||||
'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']),
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
final nty = context.watch<NotificationProvider>();
|
||||
|
||||
final show = nty.notifications.isNotEmpty && ua.isAuthorized;
|
||||
final isMobile = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
|
||||
|
||||
final show = nty.showingCount > 0 && ua.isAuthorized;
|
||||
|
||||
if (show) {
|
||||
_animationController.animateTo(1);
|
||||
} else {
|
||||
_animationController.animateTo(0);
|
||||
}
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: nty,
|
||||
builder: (context, _) {
|
||||
final current = nty.notifications.lastOrNull;
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: !show,
|
||||
child: GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
nty.notifications.lastOrNull?.title ??
|
||||
'notificationUnreadCount'.plural(nty.notifications.length),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (nty.notifications.lastOrNull?.body != null)
|
||||
Text(
|
||||
nty.notifications.lastOrNull!.body,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).padding(left: 4),
|
||||
const Gap(8),
|
||||
const Icon(Symbols.notifications_unread, size: 18),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4)
|
||||
: const SizedBox.shrink(),
|
||||
).opacity(show ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.easeInOut,
|
||||
child: Animate(
|
||||
autoPlay: false,
|
||||
controller: _animationController,
|
||||
effects: [
|
||||
SlideEffect(
|
||||
begin: isMobile ? Offset(0, -1) : Offset(1, 0),
|
||||
end: Offset(0, 0),
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
),
|
||||
FadeEffect(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
],
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
width: double.infinity,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: isMobile ? MediaQuery.of(context).size.width - 16 : 360,
|
||||
),
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (current?.metadata['avatar'] != null)
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundImage: UniversalImage.provider(
|
||||
sn.getAttachmentUrl(current!.metadata['avatar']),
|
||||
),
|
||||
)
|
||||
else
|
||||
Icon(kNotificationTopicIcons[current?.topic] ?? Symbols.notifications),
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
current?.title ?? 'Notification',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (current?.subtitle?.isNotEmpty ?? false)
|
||||
Text(
|
||||
current!.subtitle!,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
MarkdownTextContent(
|
||||
content: current?.body ?? '',
|
||||
isAutoWarp: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(DateFormat('HH:mm').format(current?.createdAt.toLocal() ?? DateTime.now()))
|
||||
.fontSize(12)
|
||||
.padding(right: 2),
|
||||
const Gap(6),
|
||||
if (current?.metadata['image'] != null)
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(current?.metadata['image']),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
nty.clear();
|
||||
if (current != null) {
|
||||
_markOneAsRead(current);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -4,21 +4,69 @@ 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:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class PostCommentSliverList extends StatefulWidget {
|
||||
final int parentPostId;
|
||||
import '../../providers/sn_network.dart';
|
||||
|
||||
class PostCommentQuickAction extends StatelessWidget {
|
||||
final double? maxWidth;
|
||||
final SnPost parentPost;
|
||||
final Function? onPosted;
|
||||
|
||||
const PostCommentQuickAction({super.key, this.maxWidth, required this.parentPost, this.onPosted});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
return Container(
|
||||
height: 240,
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.symmetric(vertical: 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: parentPost.id,
|
||||
onPost: () {
|
||||
onPosted?.call();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PostCommentSliverList extends StatefulWidget {
|
||||
final SnPost parentPost;
|
||||
final double? maxWidth;
|
||||
final Function(SnPost)? onSelectAnswer;
|
||||
|
||||
const PostCommentSliverList({
|
||||
super.key,
|
||||
required this.parentPostId,
|
||||
required this.parentPost,
|
||||
this.maxWidth,
|
||||
this.onSelectAnswer,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -37,7 +85,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final pt = context.read<SnPostContentProvider>();
|
||||
final result = await pt.listPostReplies(widget.parentPostId);
|
||||
final result = await pt.listPostReplies(widget.parentPost.id);
|
||||
final List<SnPost> out = result.$1;
|
||||
|
||||
if (!mounted) return;
|
||||
@ -48,8 +96,24 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
if (mounted) setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> _selectAnswer(SnPost answer) async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
|
||||
'publisher': answer.publisherId,
|
||||
'answer_id': answer.id,
|
||||
});
|
||||
if (!mounted) return;
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
_posts.clear();
|
||||
_postCount = null;
|
||||
_fetchPosts();
|
||||
}
|
||||
|
||||
@ -71,6 +135,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
child: PostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: widget.maxWidth,
|
||||
onSelectAnswer: widget.parentPost.type == 'question' ? () => _selectAnswer(_posts[idx]) : null,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
@ -94,11 +159,12 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
}
|
||||
|
||||
class PostCommentListPopup extends StatefulWidget {
|
||||
final int postId;
|
||||
final SnPost post;
|
||||
final int commentCount;
|
||||
|
||||
const PostCommentListPopup({
|
||||
super.key,
|
||||
required this.postId,
|
||||
required this.post,
|
||||
this.commentCount = 0,
|
||||
});
|
||||
|
||||
@ -122,9 +188,7 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
||||
children: [
|
||||
const Icon(Symbols.comment, size: 24),
|
||||
const Gap(16),
|
||||
Text('postCommentsDetailed')
|
||||
.plural(widget.commentCount)
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
Text('postCommentsDetailed').plural(widget.commentCount).textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Expanded(
|
||||
@ -143,7 +207,7 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
||||
),
|
||||
),
|
||||
child: PostMiniEditor(
|
||||
postReplyId: widget.postId,
|
||||
postReplyId: widget.post.id,
|
||||
onPost: () {
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
@ -151,8 +215,8 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
||||
),
|
||||
),
|
||||
PostCommentSliverList(
|
||||
parentPost: widget.post,
|
||||
key: _childListKey,
|
||||
parentPostId: widget.postId,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,10 +1,13 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@ -20,10 +23,12 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/post/post_detail.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/reaction.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/link_preview.dart';
|
||||
@ -31,9 +36,72 @@ import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||
import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||
import 'package:surface/widgets/post/post_poll.dart';
|
||||
import 'package:surface/widgets/post/post_reaction.dart';
|
||||
import 'package:surface/widgets/post/publisher_popover.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
class OpenablePostItem extends StatelessWidget {
|
||||
final SnPost data;
|
||||
final bool showReactions;
|
||||
final bool showComments;
|
||||
final bool showMenu;
|
||||
final bool showFullPost;
|
||||
final double? maxWidth;
|
||||
final Function(SnPost data)? onChanged;
|
||||
final Function()? onDeleted;
|
||||
final Function()? onSelectAnswer;
|
||||
|
||||
const OpenablePostItem({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.showReactions = true,
|
||||
this.showComments = true,
|
||||
this.showMenu = true,
|
||||
this.showFullPost = false,
|
||||
this.maxWidth,
|
||||
this.onChanged,
|
||||
this.onDeleted,
|
||||
this.onSelectAnswer,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return Center(
|
||||
child: OpenContainer(
|
||||
closedBuilder: (_, __) => Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: PostItem(
|
||||
data: data,
|
||||
maxWidth: maxWidth,
|
||||
showComments: showComments,
|
||||
showFullPost: showFullPost,
|
||||
onChanged: onChanged,
|
||||
onDeleted: onDeleted,
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
),
|
||||
),
|
||||
openBuilder: (_, close) => PostDetailScreen(
|
||||
slug: data.id.toString(),
|
||||
preload: data,
|
||||
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)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PostItem extends StatelessWidget {
|
||||
final SnPost data;
|
||||
@ -44,6 +112,7 @@ class PostItem extends StatelessWidget {
|
||||
final double? maxWidth;
|
||||
final Function(SnPost data)? onChanged;
|
||||
final Function()? onDeleted;
|
||||
final Function()? onSelectAnswer;
|
||||
|
||||
const PostItem({
|
||||
super.key,
|
||||
@ -55,6 +124,7 @@ class PostItem extends StatelessWidget {
|
||||
this.maxWidth,
|
||||
this.onChanged,
|
||||
this.onDeleted,
|
||||
this.onSelectAnswer,
|
||||
});
|
||||
|
||||
void _onChanged(SnPost data) {
|
||||
@ -126,6 +196,57 @@ class PostItem extends StatelessWidget {
|
||||
final ua = context.read<UserProvider>();
|
||||
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
|
||||
|
||||
// Video full view
|
||||
if (showFullPost && data.type == 'video' && ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PostContentHeader(
|
||||
data: data,
|
||||
isAuthor: isAuthor,
|
||||
isRelativeDate: !showFullPost,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
onDeleted: () {
|
||||
if (onDeleted != null) {}
|
||||
},
|
||||
).padding(bottom: 8),
|
||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(bottom: 8),
|
||||
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
|
||||
_PostFeaturedComment(data: data),
|
||||
_PostBottomAction(
|
||||
data: data,
|
||||
showComments: true,
|
||||
showReactions: showReactions,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onChanged: _onChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
SizedBox(
|
||||
width: 340,
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
PostCommentSliverList(
|
||||
parentPost: data,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Article headline preview
|
||||
if (!showFullPost && data.type == 'article') {
|
||||
return Container(
|
||||
@ -139,10 +260,12 @@ class PostItem extends StatelessWidget {
|
||||
isRelativeDate: !showFullPost,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
onDeleted: () {
|
||||
if (onDeleted != null) {}
|
||||
},
|
||||
).padding(horizontal: 12, top: 8, bottom: 8),
|
||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
|
||||
@ -186,6 +309,7 @@ class PostItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
|
||||
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
|
||||
_PostBottomAction(
|
||||
data: data,
|
||||
showComments: showComments,
|
||||
@ -220,10 +344,13 @@ class PostItem extends StatelessWidget {
|
||||
showMenu: showMenu,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
onDeleted: () {
|
||||
if (onDeleted != null) onDeleted!();
|
||||
},
|
||||
).padding(horizontal: 12, vertical: 8),
|
||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
|
||||
if (data.body['title'] != null || data.body['description'] != null)
|
||||
_PostHeadline(
|
||||
data: data,
|
||||
@ -263,10 +390,12 @@ class PostItem extends StatelessWidget {
|
||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!).padding(horizontal: 12, vertical: 4),
|
||||
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
||||
LinkPreviewWidget(
|
||||
text: data.body['content'],
|
||||
).padding(horizontal: 4),
|
||||
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: Column(
|
||||
@ -328,6 +457,7 @@ class PostShareImageWidget extends StatelessWidget {
|
||||
showMenu: false,
|
||||
isRelativeDate: false,
|
||||
).padding(horizontal: 16, bottom: 8),
|
||||
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
|
||||
_PostHeadline(
|
||||
data: data,
|
||||
isEnlarge: data.type == 'article',
|
||||
@ -433,6 +563,30 @@ class PostShareImageWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PostQuestionHint extends StatelessWidget {
|
||||
final SnPost data;
|
||||
|
||||
const _PostQuestionHint({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20),
|
||||
const Gap(4),
|
||||
if (data.body['answer'] == null && data.body['reward']?.toDouble() != null)
|
||||
Text('postQuestionUnansweredWithReward'.tr(args: [
|
||||
'${data.body['reward']}',
|
||||
])).opacity(0.75)
|
||||
else if (data.body['answer'] == null)
|
||||
Text('postQuestionUnanswered'.tr()).opacity(0.75)
|
||||
else
|
||||
Text('postQuestionAnswered'.tr()).opacity(0.75),
|
||||
],
|
||||
).opacity(0.75);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostBottomAction extends StatelessWidget {
|
||||
final SnPost data;
|
||||
final bool showComments;
|
||||
@ -524,7 +678,7 @@ class _PostBottomAction extends StatelessWidget {
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => PostCommentListPopup(
|
||||
postId: data.id,
|
||||
post: data,
|
||||
commentCount: data.metric.replyCount,
|
||||
),
|
||||
);
|
||||
@ -647,6 +801,7 @@ class _PostContentHeader extends StatelessWidget {
|
||||
final bool showMenu;
|
||||
final Function onDeleted;
|
||||
final Function() onShare, onShareImage;
|
||||
final Function()? onSelectAnswer;
|
||||
|
||||
const _PostContentHeader({
|
||||
required this.data,
|
||||
@ -657,6 +812,7 @@ class _PostContentHeader extends StatelessWidget {
|
||||
required this.onDeleted,
|
||||
required this.onShare,
|
||||
required this.onShareImage,
|
||||
this.onSelectAnswer,
|
||||
});
|
||||
|
||||
Future<void> _deletePost(BuildContext context) async {
|
||||
@ -755,6 +911,20 @@ class _PostContentHeader extends StatelessWidget {
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
if (isAuthor && onSelectAnswer != null)
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.check_circle),
|
||||
const Gap(16),
|
||||
Text('postQuestionAnswerSelect').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
onSelectAnswer?.call();
|
||||
},
|
||||
),
|
||||
if (isAuthor && onSelectAnswer != null) PopupMenuDivider(),
|
||||
if (isAuthor)
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
@ -795,7 +965,7 @@ class _PostContentHeader extends StatelessWidget {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': data.typePlural},
|
||||
pathParameters: {'mode': 'stories'},
|
||||
queryParameters: {'replying': data.id.toString()},
|
||||
);
|
||||
},
|
||||
@ -811,12 +981,28 @@ class _PostContentHeader extends StatelessWidget {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': data.typePlural},
|
||||
pathParameters: {'mode': 'stories'},
|
||||
queryParameters: {'reposting': data.id.toString()},
|
||||
);
|
||||
},
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.book_4_spark),
|
||||
const Gap(16),
|
||||
Text('postGetInsight').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _PostGetInsightPopup(postId: data.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
onTap: onShare,
|
||||
child: Row(
|
||||
@ -1106,6 +1292,118 @@ class _PostTruncatedHint extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PostFeaturedComment extends StatefulWidget {
|
||||
final SnPost data;
|
||||
final double? maxWidth;
|
||||
|
||||
const _PostFeaturedComment({required this.data, this.maxWidth});
|
||||
|
||||
@override
|
||||
State<_PostFeaturedComment> createState() => _PostFeaturedCommentState();
|
||||
}
|
||||
|
||||
class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
||||
SnPost? _featuredComment;
|
||||
bool _isAnswer = false;
|
||||
|
||||
Future<void> _fetchComments() async {
|
||||
// If this is a answered question, fetch the answer instead
|
||||
if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
|
||||
_isAnswer = true;
|
||||
setState(() => _featuredComment = SnPost.fromJson(resp.data));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
|
||||
'take': 1,
|
||||
});
|
||||
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.data.metric.replyCount > 0) {
|
||||
_fetchComments();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.data.metric.replyCount == 0) return const SizedBox.shrink();
|
||||
if (_featuredComment == null) return const SizedBox.shrink();
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AnimateWidgetExtensions(Container(
|
||||
constraints: BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
width: double.infinity,
|
||||
child: Material(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: _isAnswer ? Colors.green.withOpacity(0.5) : Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => PostCommentListPopup(
|
||||
post: widget.data,
|
||||
commentCount: widget.data.metric.replyCount,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Gap(2),
|
||||
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, size: 20),
|
||||
const Gap(10),
|
||||
Text(
|
||||
_isAnswer ? 'postQuestionAnswerTitle' : 'postFeaturedComment',
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 15),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
const Gap(4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 12,
|
||||
backgroundImage: UniversalImage.provider(
|
||||
sn.getAttachmentUrl(_featuredComment!.publisher.avatar),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: MarkdownTextContent(
|
||||
content: _featuredComment!.body['content'],
|
||||
isAutoWarp: true,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
)).animate().fadeIn(duration: 300.ms, curve: Curves.easeInOut);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostAbuseReportDialog extends StatefulWidget {
|
||||
final SnPost data;
|
||||
|
||||
@ -1181,3 +1479,128 @@ class _PostAbuseReportDialogState extends State<_PostAbuseReportDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostGetInsightPopup extends StatefulWidget {
|
||||
final int postId;
|
||||
|
||||
const _PostGetInsightPopup({required this.postId});
|
||||
|
||||
@override
|
||||
State<_PostGetInsightPopup> createState() => _PostGetInsightPopupState();
|
||||
}
|
||||
|
||||
class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
|
||||
String? _response;
|
||||
String? _thinkingProcess;
|
||||
|
||||
Future<void> _fetchResponse() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/posts/${widget.postId}/insight',
|
||||
options: Options(
|
||||
sendTimeout: const Duration(minutes: 10),
|
||||
receiveTimeout: const Duration(minutes: 10),
|
||||
));
|
||||
final out = resp.data['response'] as String;
|
||||
|
||||
try {
|
||||
final document = XmlDocument.parse(out);
|
||||
_thinkingProcess = document.getElement('think')?.innerText.trim();
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
|
||||
setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchResponse();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.book_4_spark, size: 24),
|
||||
const Gap(16),
|
||||
Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
const Gap(4),
|
||||
Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20),
|
||||
const Gap(4),
|
||||
if (_response == null)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (_thinkingProcess != null && _thinkingProcess!.isNotEmpty)
|
||||
ExpansionTile(
|
||||
leading: const Icon(Symbols.info),
|
||||
title: Text('aiThinkingProcess'.tr()),
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
minTileHeight: 32,
|
||||
children: [
|
||||
SelectableText(
|
||||
_thinkingProcess!,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic),
|
||||
).padding(horizontal: 20, vertical: 8),
|
||||
],
|
||||
).padding(vertical: 8),
|
||||
SelectionArea(
|
||||
child: MarkdownTextContent(
|
||||
content: _response!,
|
||||
),
|
||||
).padding(horizontal: 20, top: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostVideoPlayer extends StatelessWidget {
|
||||
final SnPost data;
|
||||
|
||||
const _PostVideoPlayer({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +95,7 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
title: 'attachmentSetThumbnail'.tr(),
|
||||
pool: 'interactive',
|
||||
analyzeNow: true,
|
||||
),
|
||||
);
|
||||
@ -292,7 +293,7 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
constraints: const BoxConstraints(maxHeight: 120),
|
||||
child: Row(
|
||||
children: [
|
||||
const Gap(8),
|
||||
const Gap(16),
|
||||
if (thumbnail != null)
|
||||
ContextMenuArea(
|
||||
contextMenu: _createContextMenu(context, -1, thumbnail!),
|
||||
@ -337,15 +338,10 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
return Material(
|
||||
elevation: 4,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Row(
|
||||
|
@ -19,6 +19,7 @@ const Map<int, String> kPostVisibilityLevel = {
|
||||
|
||||
class PostMetaEditor extends StatelessWidget {
|
||||
final PostWriteController controller;
|
||||
|
||||
const PostMetaEditor({super.key, required this.controller});
|
||||
|
||||
Future<DateTime?> _selectDate(
|
||||
@ -87,28 +88,6 @@ class PostMetaEditor extends StatelessWidget {
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller.titleController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPostTitle'.tr(),
|
||||
border: UnderlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 24),
|
||||
if (controller.mode == 'articles') const Gap(4),
|
||||
if (controller.mode == 'articles')
|
||||
TextField(
|
||||
controller: controller.descriptionController,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPostDescription'.tr(),
|
||||
border: UnderlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 24),
|
||||
const Gap(4),
|
||||
PostTagsField(
|
||||
initialTags: controller.tags,
|
||||
labelText: 'fieldPostTags'.tr(),
|
||||
@ -133,8 +112,7 @@ class PostMetaEditor extends StatelessWidget {
|
||||
helperMaxLines: 2,
|
||||
border: UnderlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 24),
|
||||
const Gap(12),
|
||||
ListTile(
|
||||
@ -182,8 +160,7 @@ class PostMetaEditor extends StatelessWidget {
|
||||
leading: Icon(Symbols.person),
|
||||
trailing: Icon(Symbols.chevron_right),
|
||||
title: Text('postVisibleUsers').tr(),
|
||||
subtitle: Text('postSelectedUsers')
|
||||
.plural(controller.visibleUsers.length),
|
||||
subtitle: Text('postSelectedUsers').plural(controller.visibleUsers.length),
|
||||
onTap: () {
|
||||
_selectVisibleUser(context);
|
||||
},
|
||||
@ -194,8 +171,7 @@ class PostMetaEditor extends StatelessWidget {
|
||||
leading: Icon(Symbols.person),
|
||||
trailing: Icon(Symbols.chevron_right),
|
||||
title: Text('postInvisibleUsers').tr(),
|
||||
subtitle: Text('postSelectedUsers')
|
||||
.plural(controller.invisibleUsers.length),
|
||||
subtitle: Text('postSelectedUsers').plural(controller.invisibleUsers.length),
|
||||
onTap: () {
|
||||
_selectInvisibleUser(context);
|
||||
},
|
||||
@ -204,9 +180,7 @@ class PostMetaEditor extends StatelessWidget {
|
||||
leading: const Icon(Symbols.event_available),
|
||||
title: Text('postPublishedAt').tr(),
|
||||
subtitle: Text(
|
||||
controller.publishedAt != null
|
||||
? dateFormatter.format(controller.publishedAt!)
|
||||
: 'unset'.tr(),
|
||||
controller.publishedAt != null ? dateFormatter.format(controller.publishedAt!) : 'unset'.tr(),
|
||||
),
|
||||
trailing: controller.publishedAt != null
|
||||
? IconButton(
|
||||
@ -230,9 +204,7 @@ class PostMetaEditor extends StatelessWidget {
|
||||
leading: const Icon(Symbols.event_busy),
|
||||
title: Text('postPublishedUntil').tr(),
|
||||
subtitle: Text(
|
||||
controller.publishedUntil != null
|
||||
? dateFormatter.format(controller.publishedUntil!)
|
||||
: 'unset'.tr(),
|
||||
controller.publishedUntil != null ? dateFormatter.format(controller.publishedUntil!) : 'unset'.tr(),
|
||||
),
|
||||
trailing: controller.publishedUntil != null
|
||||
? IconButton(
|
||||
|
138
lib/widgets/post/post_poll.dart
Normal file
138
lib/widgets/post/post_poll.dart
Normal file
@ -0,0 +1,138 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/types/poll.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class PostPoll extends StatefulWidget {
|
||||
final SnPoll poll;
|
||||
|
||||
const PostPoll({super.key, required this.poll});
|
||||
|
||||
@override
|
||||
State<PostPoll> createState() => _PostPollState();
|
||||
}
|
||||
|
||||
class _PostPollState extends State<PostPoll> {
|
||||
bool _isBusy = false;
|
||||
late SnPoll _poll;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_poll = widget.poll;
|
||||
_fetchAnswer();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
String? _answeredChoice;
|
||||
|
||||
Future<void> _refreshPoll() async {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/polls/${widget.poll.id}');
|
||||
if (!mounted) return;
|
||||
setState(() => _poll = SnPoll.fromJson(resp.data!));
|
||||
}
|
||||
|
||||
Future<void> _fetchAnswer() async {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) return;
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp =
|
||||
await sn.client.get('/cgi/co/polls/${widget.poll.id}/answer');
|
||||
_answeredChoice = resp.data?['answer'];
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
// ignore because it may not found
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _voteForOption(SnPollOption option) async {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) return;
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/co/polls/${widget.poll.id}/answer', data: {
|
||||
'answer': option.id,
|
||||
});
|
||||
if (!mounted) return;
|
||||
HapticFeedback.heavyImpact();
|
||||
_answeredChoice = option.id;
|
||||
_refreshPoll();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
for (final option in _poll.options)
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: MediaQuery.of(context).size.width *
|
||||
(_poll.metric.byOptionsPercentage[option.id] ?? 0)
|
||||
.toDouble(),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
minTileHeight: 60,
|
||||
leading: _answeredChoice == option.id
|
||||
? const Icon(Symbols.circle, fill: 1)
|
||||
: const Icon(Symbols.circle),
|
||||
title: Text(option.name),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'pollVotes'
|
||||
.plural(_poll.metric.byOptions[option.id] ?? 0),
|
||||
),
|
||||
Text(' · ').padding(horizontal: 4),
|
||||
Text(
|
||||
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (option.description.isNotEmpty)
|
||||
Text(option.description),
|
||||
],
|
||||
),
|
||||
onTap: _isBusy ? null : () => _voteForOption(option),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
201
lib/widgets/post/post_poll_editor.dart
Normal file
201
lib/widgets/post/post_poll_editor.dart
Normal file
@ -0,0 +1,201 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class PollEditorDialog extends StatefulWidget {
|
||||
final SnPoll? poll;
|
||||
|
||||
const PollEditorDialog({super.key, this.poll});
|
||||
|
||||
@override
|
||||
State<PollEditorDialog> createState() => _PollEditorDialogState();
|
||||
}
|
||||
|
||||
class _PollEditorDialogState extends State<PollEditorDialog> {
|
||||
final TextEditingController _linkController = TextEditingController();
|
||||
final List<SnPollOption> _pollOptions = List.empty(growable: true);
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _fetchPoll() async {
|
||||
if (_linkController.text.isEmpty) return;
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/polls/${_linkController.text}');
|
||||
final out = SnPoll.fromJson(resp.data);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, out);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _applyPost() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = widget.poll == null
|
||||
? await sn.client.post('/cgi/co/polls', data: {
|
||||
'options': _pollOptions.where((ele) => ele.name.isNotEmpty).toList(),
|
||||
})
|
||||
: await sn.client.put('/cgi/co/polls/${widget.poll!.id}', data: {
|
||||
'options': _pollOptions.where((ele) => ele.name.isNotEmpty).toList(),
|
||||
});
|
||||
final out = SnPoll.fromJson(resp.data);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, out);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deletePoll() async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'pollEditorDelete'.tr(),
|
||||
'pollEditorDeleteDescription'.tr(),
|
||||
);
|
||||
if (!confirm) return;
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/co/polls/${widget.poll!.id}');
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, false);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pollOptions.addAll(widget.poll?.options ?? []);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_linkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.poll == null ? 'pollEditorNew' : 'pollEditorEdit').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 16,
|
||||
children: [
|
||||
if (widget.poll == null)
|
||||
TextField(
|
||||
controller: _linkController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
labelText: 'pollLinkExisting'.tr(),
|
||||
prefixText: '#',
|
||||
suffixIcon: IconButton(
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _isBusy ? null : () => _fetchPoll(),
|
||||
icon: const Icon(Icons.keyboard_arrow_right),
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < _pollOptions.length; i++)
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.circle),
|
||||
title: TextFormField(
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'pollOptionName'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
initialValue: _pollOptions[i].name,
|
||||
onChanged: (value) {
|
||||
// Looks like we don't need set state here cuz it got internal updated.
|
||||
_pollOptions[i] = _pollOptions[i].copyWith(name: value);
|
||||
},
|
||||
),
|
||||
trailing: IconButton(
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
setState(() => _pollOptions.removeAt(i));
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('pollOptionAdd').tr(),
|
||||
onTap: () {
|
||||
setState(
|
||||
() => _pollOptions.add(
|
||||
SnPollOption(id: const Uuid().v4(), icon: '', name: '', description: ''),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.poll != null)
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.delete),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('pollEditorDelete').tr(),
|
||||
onTap: _isBusy ? null : () => _deletePoll(),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.link_off),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('pollEditorUnlink').tr(),
|
||||
onTap: _isBusy ? null : () => Navigator.pop(context, false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => Navigator.pop(context),
|
||||
child: Text('cancel'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _applyPost(),
|
||||
child: Text('dialogConfirm'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -61,7 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
||||
);
|
||||
}
|
||||
}
|
||||
HapticFeedback.mediumImpact();
|
||||
HapticFeedback.heavyImpact();
|
||||
} catch (err) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
|
@ -5,10 +5,9 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
import 'package:surface/providers/config.dart';
|
||||
// Keep this import to make the web image render work
|
||||
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
|
||||
class UniversalImage extends StatelessWidget {
|
||||
final String url;
|
||||
|
@ -11,9 +11,11 @@
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_udid/flutter_udid_plugin.h>
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
#include <hotkey_manager_linux/hotkey_manager_linux_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
#include <media_kit_video/media_kit_video_plugin.h>
|
||||
#include <pasteboard/pasteboard_plugin.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
@ -32,6 +34,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
||||
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
||||
g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
|
||||
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
|
||||
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
|
||||
@ -41,6 +46,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
|
||||
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
|
||||
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
|
||||
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
@ -8,9 +8,11 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
hotkey_manager_linux
|
||||
media_kit_libs_linux
|
||||
media_kit_video
|
||||
pasteboard
|
||||
tray_manager
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
@ -8,6 +8,7 @@ import Foundation
|
||||
import bitsdojo_window_macos
|
||||
import connectivity_plus
|
||||
import device_info_plus
|
||||
import file_picker
|
||||
import file_saver
|
||||
import file_selector_macos
|
||||
import firebase_analytics
|
||||
@ -17,6 +18,7 @@ import flutter_inappwebview_macos
|
||||
import flutter_udid
|
||||
import flutter_webrtc
|
||||
import gal
|
||||
import hotkey_manager_macos
|
||||
import in_app_review
|
||||
import livekit_client
|
||||
import media_kit_libs_macos_video
|
||||
@ -28,6 +30,7 @@ import screen_brightness_macos
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
import tray_manager
|
||||
import url_launcher_macos
|
||||
import video_compress
|
||||
import wakelock_plus
|
||||
@ -36,6 +39,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
|
||||
@ -45,6 +49,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
||||
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
|
||||
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
||||
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
|
||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||
@ -56,6 +61,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
|
@ -2,69 +2,70 @@ PODS:
|
||||
- bitsdojo_window_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- croppy (0.0.1):
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_picker (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_saver (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_selector_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- Firebase/Analytics (11.6.0):
|
||||
- Firebase/Analytics (11.7.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (11.6.0):
|
||||
- Firebase/Core (11.7.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 11.6.0)
|
||||
- Firebase/CoreOnly (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- Firebase/Messaging (11.6.0):
|
||||
- FirebaseAnalytics (~> 11.7.0)
|
||||
- Firebase/CoreOnly (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- Firebase/Messaging (11.7.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.6.0)
|
||||
- firebase_analytics (11.4.0):
|
||||
- Firebase/Analytics (= 11.6.0)
|
||||
- FirebaseMessaging (~> 11.7.0)
|
||||
- firebase_analytics (11.4.2):
|
||||
- Firebase/Analytics (= 11.7.0)
|
||||
- firebase_core
|
||||
- FlutterMacOS
|
||||
- firebase_core (3.10.0):
|
||||
- Firebase/CoreOnly (~> 11.6.0)
|
||||
- firebase_core (3.11.0):
|
||||
- Firebase/CoreOnly (~> 11.7.0)
|
||||
- FlutterMacOS
|
||||
- firebase_messaging (15.2.0):
|
||||
- Firebase/CoreOnly (~> 11.6.0)
|
||||
- Firebase/Messaging (~> 11.6.0)
|
||||
- firebase_messaging (15.2.2):
|
||||
- Firebase/CoreOnly (~> 11.7.0)
|
||||
- Firebase/Messaging (~> 11.7.0)
|
||||
- firebase_core
|
||||
- FlutterMacOS
|
||||
- FirebaseAnalytics (11.6.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseAnalytics (11.7.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.7.0)
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleAppMeasurement (= 11.6.0)
|
||||
- GoogleAppMeasurement (= 11.7.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (11.6.0):
|
||||
- FirebaseCoreInternal (~> 11.6.0)
|
||||
- FirebaseCore (11.7.0):
|
||||
- FirebaseCoreInternal (~> 11.7.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- FirebaseCoreInternal (11.6.0):
|
||||
- FirebaseCoreInternal (11.7.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseInstallations (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseMessaging (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -85,21 +86,21 @@ PODS:
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleAppMeasurement (11.6.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
|
||||
- GoogleAppMeasurement (11.7.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.7.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.6.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.7.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.7.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
@ -135,9 +136,13 @@ PODS:
|
||||
- GoogleUtilities/UserDefaults (8.0.2):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- HotKey (0.2.1)
|
||||
- hotkey_manager_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- HotKey
|
||||
- in_app_review (2.0.0):
|
||||
- FlutterMacOS
|
||||
- livekit_client (2.3.5):
|
||||
- livekit_client (2.3.6):
|
||||
- flutter_webrtc
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@ -172,6 +177,8 @@ PODS:
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- tray_manager (0.0.1):
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- video_compress (0.3.0):
|
||||
@ -182,9 +189,10 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
||||
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
|
||||
@ -195,6 +203,7 @@ DEPENDENCIES:
|
||||
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
||||
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
|
||||
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
||||
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
||||
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||
@ -207,6 +216,7 @@ DEPENDENCIES:
|
||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
@ -222,6 +232,7 @@ SPEC REPOS:
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- HotKey
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
@ -232,11 +243,13 @@ EXTERNAL SOURCES:
|
||||
bitsdojo_window_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
|
||||
connectivity_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
|
||||
croppy:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
|
||||
device_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||
file_picker:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
||||
file_saver:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
|
||||
file_selector_macos:
|
||||
@ -257,6 +270,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral
|
||||
gal:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
||||
hotkey_manager_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
|
||||
in_app_review:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
|
||||
livekit_client:
|
||||
@ -281,6 +296,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
sqflite_darwin:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||
tray_manager:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
video_compress:
|
||||
@ -290,30 +307,33 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
|
||||
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
|
||||
connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802
|
||||
croppy: 25a638bd7d05411d8c697f481568f261037694fc
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
|
||||
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
|
||||
firebase_analytics: 5249f87da6fed852901581aab2602e0280ec2fdb
|
||||
firebase_core: 6d9bb8b0ea817e8fe0d928177d42275b45fdba6f
|
||||
firebase_messaging: ae8e88b586e4d50abc7cac5bacf74d21967fd226
|
||||
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
|
||||
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
|
||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
||||
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
|
||||
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
|
||||
Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4
|
||||
firebase_analytics: 41d88c024a7756462a803e36236ba74f24cdc2c5
|
||||
firebase_core: 751d3d919b95d4ae46ab049d0d64d42d4eec086b
|
||||
firebase_messaging: cc174f19945e9541e140e3cb0118448e59b38c6c
|
||||
FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec
|
||||
FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4
|
||||
FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881
|
||||
FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9
|
||||
FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c
|
||||
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
|
||||
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
|
||||
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
|
||||
GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
|
||||
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
|
||||
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
||||
livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
|
||||
livekit_client: 0ad107154753a5a76802d2222c040223ad049499
|
||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||
@ -328,6 +348,7 @@ SPEC CHECKSUMS:
|
||||
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
|
240
pubspec.lock
240
pubspec.lock
@ -13,10 +13,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "27899c95f9e7ec06c8310e6e0eac967707714b9f1450c4a58fa00ca011a4a8ae"
|
||||
sha256: e051259913915ea5bc8fe18664596bea08592fd123930605d562969cd7315fcd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.49"
|
||||
version: "1.3.51"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
@ -138,26 +138,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948"
|
||||
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.3"
|
||||
version: "4.0.4"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e"
|
||||
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.4.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
|
||||
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.14"
|
||||
version: "2.4.15"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -214,6 +214,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
chalkdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: chalkdart
|
||||
sha256: "08c910ee341fcdd1e46f20ddce59b13c1d85f5d97f2fd2f12014c46ede670e40"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -266,10 +274,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
|
||||
sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
version: "6.1.3"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -338,18 +346,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
|
||||
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.7"
|
||||
version: "2.3.8"
|
||||
dart_webrtc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dart_webrtc
|
||||
sha256: e65506edb452148220efab53d8d2f8bb9d827bd8bcd53cf3a3e6df70b27f3d86
|
||||
sha256: "3b3ff59c66cbc1577ed0f28d7005b5163555208fb1697a42207424ab8baa27c5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.10"
|
||||
version: "1.5.0"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -362,10 +370,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: b37d37c2f912ad4e8ec694187de87d05de2a3cb82b465ff1f65f65a2d05de544
|
||||
sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.2.1"
|
||||
version: "11.3.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -378,10 +386,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
|
||||
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.7.0"
|
||||
version: "5.8.0+1"
|
||||
dio_smart_retry:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -394,10 +402,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
|
||||
sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.1.0"
|
||||
dismissible_page:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -418,10 +426,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: easy_localization
|
||||
sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201
|
||||
sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
version: "3.0.7+1"
|
||||
easy_localization_loader:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -490,10 +498,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204
|
||||
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.7"
|
||||
version: "8.3.7"
|
||||
file_saver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -538,34 +546,34 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_analytics
|
||||
sha256: "498c6cb8468e348a556709c745d92a52173ab3a9b906aa0593393f0787f201ea"
|
||||
sha256: "47428047a0778f72af53a3c7cb5d556e1cb25e2327cc8aa40d544971dc6245b2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.4.0"
|
||||
version: "11.4.2"
|
||||
firebase_analytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_platform_interface
|
||||
sha256: ccbb350554e98afdb4b59852689292d194d31232a2647b5012a66622b3711df9
|
||||
sha256: "1076f4b041f76143e14878c70f0758f17fe5910c0cd992db9e93bd3c3584512b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
version: "4.3.2"
|
||||
firebase_analytics_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_web
|
||||
sha256: "68e1f18fc16482c211c658e739c25f015b202a260d9ad8249c6d3d7963b8105f"
|
||||
sha256: "8f6dd64ea6d28b7f5b9e739d183a9e1c7f17027794a3e9aba1879621d42426ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.10+6"
|
||||
version: "0.5.10+8"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "0307c1fde82e2b8b97e0be2dab93612aff9a72f31ebe9bfac66ed8b37ef7c568"
|
||||
sha256: "93dc4dd12f9b02c5767f235307f609e61ed9211047132d07f9e02c668f0bfc33"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.10.0"
|
||||
version: "3.11.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -578,34 +586,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: fbc008cf390d909b823763064b63afefe9f02d8afdb13eb3f485b871afee956b
|
||||
sha256: "0e13c80f0de8acaa5d0519cbe23c8b4cc138a2d5d508b5755c861bdfc9762678"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.19.0"
|
||||
version: "2.20.0"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "48a8a59197c1c5174060ba9aa1e0036e9b5a0d28a0cc22d19c1fcabc67fafe3c"
|
||||
sha256: "3dee3b0cbfe719e64773cb7d1cad57c58b2235a8c136f5715fe733a54058c783"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
version: "15.2.2"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "9770a8e91f54296829dcaa61ce9b7c2f9ae9abbf99976dd3103a60470d5264dd"
|
||||
sha256: e9ea726b9bb864fc6223bb66422bd9877b9973ae51967754a769b0d01e201c1e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
version: "4.6.2"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: "329ca4ef45ec616abe6f1d5e58feed0934a50840a65aa327052354ad3c64ed77"
|
||||
sha256: "5f7b40e8bf861a37f8b8196e347d8a919750421a45f0b45d1bb74e98fa72726e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.10.0"
|
||||
version: "3.10.2"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -764,10 +772,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487
|
||||
sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.5"
|
||||
version: "0.7.6+2"
|
||||
flutter_native_splash:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -830,10 +838,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_webrtc
|
||||
sha256: "188401cc3275bc4f1f965babdff6cac612a4b46572f1e49f49db8af5361d5712"
|
||||
sha256: "572df3de6c828e571db4b75b4a96a15c2f34fa3d420a84438f44a3158b22e81a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.6"
|
||||
version: "0.12.9"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -878,18 +886,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.3"
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d"
|
||||
sha256: "04539267a740931c6d4479a10d466717ca5901c6fdfd3fcda09391bbb8ebd651"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.6.3"
|
||||
version: "14.8.0"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -934,10 +942,50 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: home_widget
|
||||
sha256: b313e3304c0429669fddf1286e1fbf61a64b873f38ba30b3eb890ef0d7560b12
|
||||
sha256: "7430f7549d42cef2e729bd3c779de748b93f1eb78b1abfe6bca8fffd1cfce3e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
version: "0.7.0+1"
|
||||
hotkey_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hotkey_manager
|
||||
sha256: "06f0655b76c8dd322fb7101dc615afbdbf39c3d3414df9e059c33892104479cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
hotkey_manager_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotkey_manager_linux
|
||||
sha256: "83676bda8210a3377bc6f1977f193bc1dbdd4c46f1bdd02875f44b6eff9a8473"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
hotkey_manager_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotkey_manager_macos
|
||||
sha256: "03b5967e64357b9ac05188ea4a5df6fe4ed4205762cb80aaccf8916ee1713c96"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
hotkey_manager_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotkey_manager_platform_interface
|
||||
sha256: "98ffca25b8cc9081552902747b2942e3bc37855389a4218c9d50ca316b653b13"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
hotkey_manager_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotkey_manager_windows
|
||||
sha256: "0d03ced9fe563ed0b68f0a0e1b22c9ffe26eb8053cb960e401f68a4f070e0117"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -950,10 +998,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
|
||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
version: "1.3.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -995,7 +1043,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c
|
||||
@ -1030,10 +1078,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
|
||||
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
version: "0.2.1+2"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1150,10 +1198,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: livekit_client
|
||||
sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
|
||||
sha256: "0cfb2f48eff7a93ea8927696dc6f218aebd2fcd1fcc1b1a7b2f53ff3597fdb52"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
version: "2.3.6"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1206,10 +1254,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: material_symbols_icons
|
||||
sha256: "89aac72d25dd49303f71b3b1e70f8374791846729365b25bebc2a2531e5b86cd"
|
||||
sha256: "1403944c2a68dbdf934fca2b2115a372e70a4a185c136ab71a9d16e2108d0b14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2801.1"
|
||||
version: "4.2805.1"
|
||||
media_kit:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1282,6 +1330,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.5"
|
||||
menu_base:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: menu_base
|
||||
sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1334,18 +1390,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790"
|
||||
sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.3"
|
||||
version: "8.2.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b
|
||||
sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.1.0"
|
||||
pasteboard:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1710,18 +1766,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a
|
||||
sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
version: "2.5.2"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5"
|
||||
sha256: ea86be7b7114f9e94fddfbb52649e59a03d6627ccd2387ebddcd6624719e9f16
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.4.5"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1774,10 +1830,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.0"
|
||||
shortid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shortid
|
||||
sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -1951,6 +2015,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
tray_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tray_manager
|
||||
sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1959,6 +2031,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uni_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uni_platform
|
||||
sha256: e02213a7ee5352212412ca026afd41d269eb00d982faa552f419ffc2debfad84
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
universal_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2059,10 +2139,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
|
||||
sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.15"
|
||||
version: "1.1.18"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2107,10 +2187,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_compress
|
||||
sha256: "5b42d89f3970c956bad7a86c29682b0892c11a4ddf95ae6e29897ee28788e377"
|
||||
sha256: "31bc5cdb9a02ba666456e5e1907393c28e6e0e972980d7d8d619a7beda0d4f20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
version: "3.1.4"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2171,26 +2251,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
|
||||
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
webrtc_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webrtc_interface
|
||||
sha256: abec3ab7956bd5ac539cf34a42fa0c82ea26675847c0966bb85160400eea9388
|
||||
sha256: "10fc6dc0ac16f909f5e434c18902415211d759313c87261f1e4ec5b4f6a04c26"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29"
|
||||
sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.10.0"
|
||||
version: "5.10.1"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2216,7 +2296,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user