Compare commits

..

77 Commits

Author SHA1 Message Date
ae9a7eb0fd 🚀 Launch 2.2.2+61 2025-02-02 13:35:43 +08:00
5d6fb2442f Able to config preferred language 2025-02-01 19:35:50 +08:00
5a85985534 Featured comment 2025-01-31 23:16:14 +08:00
c80499db03 Share to chat channel 2025-01-31 22:52:21 +08:00
b8dcdb2315 💄 Move the connection indicator 2025-01-31 21:50:18 +08:00
b7b921f1f4 📱 Fix new notify indicator on large screen 2025-01-31 20:26:20 +08:00
319d5c7d7f ♻️ Refactor notification indicator 2025-01-31 20:12:46 +08:00
4b5b001739 🐛 Fix open from widget cause multiple activity 2025-01-31 00:39:10 +08:00
db8871a455 🚀 Launch 2.2.2+60 2025-01-31 00:22:06 +08:00
38dcaa6066 AI Post Insight 2025-01-30 14:58:06 +08:00
03275b46ca 🚀 Launch 2.2.2+59 2025-01-29 21:54:00 +08:00
cf3b482fef 🐛 Bug fixes 2025-01-29 20:42:41 +08:00
aa4c04d4ef 🌐 Complete translations 2025-01-29 20:32:56 +08:00
73b82f65e4 Basic wallet page 2025-01-29 15:18:35 +08:00
9471fe40fe In-app language switcher 2025-01-28 23:09:07 +08:00
0d1e18735e 💄 Give a link to open wiki when error occurred. 2025-01-28 22:57:44 +08:00
8bb62b5992 🚀 Launch 2.2.2+58 2025-01-28 21:15:11 +08:00
1e8a6dea5b 💄 Optimize attachment list 2025-01-28 20:39:34 +08:00
5c2804cc4d 💄 Optimize news design 2025-01-28 20:21:51 +08:00
0dbb8f132a Factor settings with TOTP, In app notify authenticate method 2025-01-28 19:55:35 +08:00
3395f3dbd0 Create auth factor 2025-01-28 00:52:44 +08:00
d258ba776e ♻️ Splitting up account page and settings 2025-01-27 20:14:02 +08:00
0dcfcaad56 🚀 Launch 2.2.2+57 2025-01-26 15:04:22 +08:00
687e720956 💄 Optimize news 2025-01-26 14:50:52 +08:00
180876949e Home screen today news 2025-01-26 14:38:01 +08:00
9718965809 🐛 Bug fixes 2025-01-26 14:17:08 +08:00
5377161fb0 News reader 2025-01-26 12:38:43 +08:00
963e538ae5 News Reader Basis 2025-01-26 02:12:03 +08:00
a355e3bf90 🚀 Launch 2.2.2+56 2025-01-25 14:52:40 +08:00
cb4a2598c8 ♻️ Refactored router 2025-01-25 14:35:04 +08:00
950612dc07 🐛 Fix error when went to publisher page from account page 2025-01-25 14:27:09 +08:00
cbd1eaf1af 🐛 Fix some link preview compatibility issue 2025-01-25 01:40:54 +08:00
ac41cbd99f Toggleable preview link 2025-01-25 01:36:11 +08:00
9f9c90abc4 🐛 Fix call room meaningless safe area 2025-01-23 19:22:55 +08:00
87029e3538 🚀 Launch 2.2.2+55 2025-01-21 21:04:33 +08:00
127d9adc09 💄 Finishing up refactor background image related changes 2025-01-21 20:50:07 +08:00
c82dc7ad85 ♻️ Refactored background image (skip ci) 2025-01-21 20:35:04 +08:00
36bcff7a7c Add haptic feedback on post reaction 2025-01-21 15:07:44 +08:00
38201b547a 💄 Snackbar use floating mode while using m3 2025-01-21 15:06:27 +08:00
ed0334fcda 🐛 Bug fixes 2025-01-21 15:04:55 +08:00
fbb486b90b 💄 Optimized attachment list 2025-01-21 14:57:04 +08:00
9b34f385d5 🐛 Fix app bar buttons clicking event got absorb by indicators 2025-01-21 14:47:13 +08:00
bb7b731602 Rollback to old style attachment list 2025-01-21 12:12:21 +08:00
19076f8136 🚀 Launch 2.2.2+54 2025-01-20 17:35:13 +08:00
dc77a936ce ⬆️ Upgrade deps 2025-01-20 16:53:38 +08:00
7f58710c6f Notification indicator 2025-01-20 16:52:53 +08:00
068ddcdcdc 💄 Optimized connection indicator 2025-01-20 14:40:26 +08:00
f4e9252ca0 💄 Optimized post list 2025-01-20 14:21:41 +08:00
3b1e918117 🐛 Fix side nav cause render error 2025-01-20 01:43:11 +08:00
ed7981fdaf 🧪 Post max width 2025-01-19 17:20:24 +08:00
9698ca53e4 Swipe up to view attachment details 2025-01-19 11:44:14 +08:00
ddc1dc7daf 💄 Optimize attachment zoom page 2025-01-19 01:00:00 +08:00
1625a957f8 👔 Use material design 3 by default 2025-01-19 00:39:47 +08:00
2dc50d627e 🧱 Fix roadsign config 2025-01-16 21:51:28 +08:00
2ffde9a3dd 🚀 Launch 2.2.2+53 2025-01-15 16:00:59 +08:00
5967a91ae1 Chat user popover 2025-01-15 15:52:52 +08:00
32c1effcb5 💄 Post editor content max width 2025-01-15 15:19:46 +08:00
9d0e19c56f 🐛 Fix chat messages 2025-01-15 15:14:13 +08:00
acf4e634fe 🐛 Fix websocket will put message in wrong channel 2025-01-14 23:32:02 +08:00
25942c2338 💄 Optimize chat max width 2025-01-14 23:30:35 +08:00
a4f81f6ba1 🐛 Post auto warp 2025-01-14 23:24:11 +08:00
c1b9090e51 💄 Optimized attachment list 2025-01-14 23:17:34 +08:00
f494f70003 🚀 Launch 2.2.2+52 2025-01-08 18:07:51 +08:00
fb2a55a909 🐛 Fix editing message did not load the attachment 2025-01-08 17:48:46 +08:00
4edfa7fd50 🐛 Optimizing styling of chat 2025-01-08 17:37:16 +08:00
d699cac9b1 🚀 Launch 2.2.2+51 2025-01-07 18:10:20 +08:00
c0428e12c1 🐛 Fixed the drawer styling issue 2025-01-07 13:11:45 +08:00
55f434ff05 🚀 Launch 2.2.2+40 2025-01-06 23:39:49 +08:00
f2b3bdda2d 🐛 Add ability check to text selection chat message action 2025-01-06 23:17:24 +08:00
1f6bf33b0e Chat message action on system text selection area 2025-01-06 23:15:18 +08:00
e2027b1a32 Able to prefer sidebar to collapse 2025-01-06 22:57:44 +08:00
2b3a58b55e Optimize temporary save post scenario 2025-01-06 22:12:10 +08:00
6ac536412a 🚀 Launch 2.2.2+49 2025-01-06 22:05:20 +08:00
52f8ffe4e4 💄 Update the app bar color when in transparent mode 2025-01-06 21:57:50 +08:00
aca81431aa 🐛 Fix desktop share post as image do not include file extension name 2025-01-06 21:53:35 +08:00
1fadd850b7 💄 Optimize some styling 2025-01-06 21:46:21 +08:00
ed2a9a21b6 🐛 Fix chat username height difference 2025-01-06 19:18:23 +08:00
94 changed files with 6830 additions and 2063 deletions

View File

@ -1,12 +1,12 @@
{ {
"sync": { "sync": {
"region": "solian-next", "region": "solian",
"configPath": "roadsign.toml" "configPath": "roadsign.toml"
}, },
"deployments": [ "deployments": [
{ {
"region": "solian-next", "region": "solian",
"site": "solian-next-web", "site": "solian-web",
"path": "build/web" "path": "build/web"
} }
] ]

View File

@ -17,11 +17,16 @@
android:label="Solian" android:label="Solian"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"
android:requestLegacyExternalStorage="true"> android:requestLegacyExternalStorage="true">
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleInstance"
android:taskAffinity="" android:taskAffinity=""
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

View File

@ -15,11 +15,11 @@ body:json {
"client_id": "{{third_client_id}}", "client_id": "{{third_client_id}}",
"client_secret":"{{third_client_tk}}", "client_secret":"{{third_client_tk}}",
"type": "general", "type": "general",
"subject": "Merry Christmas!", "subject": "新年快乐!",
"subtitle": "一条来自 Solar Network 团队的信息", "subtitle": "一条来自 Solar Network 团队的信息",
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄", "content": "今天是农历正月初一,小羊祝您新年快乐 🎉",
"metadata": { "metadata": {
"image": "6EqsYQwmFRCkbmhR" "image": "D2EDbcrsTugs3xk5"
}, },
"priority": 10 "priority": 10
} }

View File

@ -0,0 +1,26 @@
meta {
name: Developer Notify One User
type: http
seq: 2
}
post {
url: {{endpoint}}/cgi/id/dev/notify/1
body: json
auth: inherit
}
body:json {
{
"client_id": "{{third_client_id}}",
"client_secret":"{{third_client_tk}}",
"type": "general",
"subject": "测试",
"subtitle": "Alphabot です",
"content": "全新通知动画",
"metadata": {
"image": "D2EDbcrsTugs3xk5"
},
"priority": 10
}
}

View File

@ -0,0 +1,11 @@
meta {
name: List News Sources
type: http
seq: 3
}
get {
url: {{endpoint}}/cgi/re/well-known/sources
body: none
auth: none
}

17
api/Reader/List News.bru Normal file
View File

@ -0,0 +1,17 @@
meta {
name: List News
type: http
seq: 2
}
get {
url: {{endpoint}}/cgi/re/news?take=10&offset=0&source=shadiao
body: none
auth: none
}
params:query {
take: 10
offset: 0
source: shadiao
}

View File

@ -0,0 +1,18 @@
meta {
name: Trigger Scan News
type: http
seq: 1
}
post {
url: {{endpoint}}/cgi/re/admin/scan
body: json
auth: inherit
}
body:json {
{
"sources": ["taiwan-ltn"],
"eager": true
}
}

View File

@ -0,0 +1,11 @@
meta {
name: Run Database Maintenance
type: http
seq: 1
}
post {
url: {{endpoint}}/wt/maintenance/database
body: none
auth: inherit
}

View File

@ -17,6 +17,10 @@
"screenAccountProfileEdit": "Edit Profile", "screenAccountProfileEdit": "Edit Profile",
"screenAbuseReport": "Abuse Reports", "screenAbuseReport": "Abuse Reports",
"screenSettings": "Settings", "screenSettings": "Settings",
"screenAccountSettings": "Account Settings",
"screenFactorSettings": "Auth Factors",
"screenAccountWallet": "Wallet",
"screenNews": "News",
"screenAlbum": "Album", "screenAlbum": "Album",
"screenChat": "Chat", "screenChat": "Chat",
"screenChatManage": "Edit Channel", "screenChatManage": "Edit Channel",
@ -103,8 +107,18 @@
}, },
"loginEnterPassword": "Enter the code", "loginEnterPassword": "Enter the code",
"loginSuccess": "Logged in as {}", "loginSuccess": "Logged in as {}",
"authFactorDelete": "Delete Auth Factor",
"authFactorDeleteDescription": "Are you sure you want delete auth factor {}?",
"authFactorPassword": "Password", "authFactorPassword": "Password",
"authFactorPasswordDescription": "The password you set when you registered.",
"authFactorEmail": "Email verification code", "authFactorEmail": "Email verification code",
"authFactorEmailDescription": "An one-time code sent to the email address you set when you registered.",
"authFactorTOTP": "Time-based OTP",
"authFactorTOTPDescription": "A one-time code generated by a TOTP authenticator such as Google Authenticator or Authy.",
"authFactorInAppNotify": "In-app notification",
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
"authFactorAdd": "Add a factor",
"authFactorAddSubtitle": "Provide another way to login your account.",
"accountIntroTitle": "Hello there!", "accountIntroTitle": "Hello there!",
"accountIntroSubtitle": "Pick an option below to get started.", "accountIntroSubtitle": "Pick an option below to get started.",
"accountLogout": "Logout", "accountLogout": "Logout",
@ -113,8 +127,14 @@
"accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.", "accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.",
"accountPublishers": "Your publishers", "accountPublishers": "Your publishers",
"accountPublishersSubtitle": "Manage your publish identities.", "accountPublishersSubtitle": "Manage your publish identities.",
"accountSettings": "Account Settings",
"accountSettingsSubtitle": "Manage your account and make it yours.",
"accountProfileEdit": "Edit your profile", "accountProfileEdit": "Edit your profile",
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.", "accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
"accountWallet": "Wallet",
"accountWalletSubtitle": "View your balance and transactions.",
"factorSettings": "Auth Factors",
"factorSettingsSubtitle": "Manage your authentication factors.",
"accountProfileEditApplied": "Profile modification applied.", "accountProfileEditApplied": "Profile modification applied.",
"publishersNew": "New Publisher", "publishersNew": "New Publisher",
"publisherNewSubtitle": "Create a new publisher identity.", "publisherNewSubtitle": "Create a new publisher identity.",
@ -179,8 +199,13 @@
"other": "{} comments" "other": "{} comments"
}, },
"settingsAppearance": "Appearance", "settingsAppearance": "Appearance",
"settingsDisplayLanguage": "Display Language",
"settingsDisplayLanguageDescription": "Set the application language.",
"settingsDisplayLanguageSystem": "Follow System",
"settingsAppBarTransparent": "Transparent App Bar", "settingsAppBarTransparent": "Transparent App Bar",
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.", "settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
"settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
"settingsDrawerPreferCollapseDescription": "Make the drawer to collapse even when the screen is wide enough.",
"settingsBackgroundImage": "Background Image", "settingsBackgroundImage": "Background Image",
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.", "settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
"settingsBackgroundImageClear": "Clear Existing Background Image", "settingsBackgroundImageClear": "Clear Existing Background Image",
@ -191,6 +216,13 @@
"settingsColorSchemeDescription": "Set the application primary color.", "settingsColorSchemeDescription": "Set the application primary color.",
"settingsColorSeed": "Color Seed", "settingsColorSeed": "Color Seed",
"settingsColorSeedDescription": "Select one of the present color schemes.", "settingsColorSeedDescription": "Select one of the present color schemes.",
"settingsFeatures": "Features",
"settingsNotifyWithHaptic": "Haptic when Notified",
"settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.",
"settingsExpandPostLink": "Expand Post Link",
"settingsExpandPostLinkDescription": "Expand the post link in the post list.",
"settingsExpandChatLink": "Expand Chat Link",
"settingsExpandChatLinkDescription": "Expand the chat link in the chat list.",
"settingsNetwork": "Network", "settingsNetwork": "Network",
"settingsNetworkServer": "HyperNet Server", "settingsNetworkServer": "HyperNet Server",
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.", "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
@ -209,12 +241,15 @@
"settingsMisc": "Misc", "settingsMisc": "Misc",
"settingsMiscAbout": "About", "settingsMiscAbout": "About",
"settingsMiscAboutDescription": "View the version information of Solian.", "settingsMiscAboutDescription": "View the version information of Solian.",
"settingsAccountLanguage": "Account Language",
"settingsAccountLanguageDescription": "Set the language for email, notification, and other account-related content.",
"sensitiveContent": "Sensitive Content", "sensitiveContent": "Sensitive Content",
"sensitiveContentCollapsed": "Sensitive content has been collapsed.", "sensitiveContentCollapsed": "Sensitive content has been collapsed.",
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.", "sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
"sensitiveContentReveal": "Reveal", "sensitiveContentReveal": "Reveal",
"serverConnecting": "Connecting to server...", "serverConnecting": "Connecting...",
"serverDisconnected": "Lost connection from server", "serverDisconnected": "Connection Lost",
"serverConnected": "Connected",
"fieldChatAlias": "Channel Alias", "fieldChatAlias": "Channel Alias",
"fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.", "fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.",
"fieldChatName": "Name", "fieldChatName": "Name",
@ -292,6 +327,7 @@
"addAttachmentFromCameraPhoto": "Take photo", "addAttachmentFromCameraPhoto": "Take photo",
"addAttachmentFromCameraVideo": "Take video", "addAttachmentFromCameraVideo": "Take video",
"addAttachmentFromRandomId": "Link via RID", "addAttachmentFromRandomId": "Link via RID",
"attachmentDetailInfo": "Attachment details",
"attachmentPastedImage": "Pasted Image", "attachmentPastedImage": "Pasted Image",
"attachmentInsertLink": "Insert Link", "attachmentInsertLink": "Insert Link",
"attachmentSetAsPostThumbnail": "Set as post thumbnail", "attachmentSetAsPostThumbnail": "Set as post thumbnail",
@ -520,11 +556,15 @@
"postImageShareAds": "Explore posts on the Solar Network", "postImageShareAds": "Explore posts on the Solar Network",
"postShare": "Share", "postShare": "Share",
"postShareImage": "Share via Image", "postShareImage": "Share via Image",
"postGetInsight": "Get Insight",
"postGetInsightTitle": "AI Insight",
"postGetInsightDescription": "AI may make mistakes, check important information.",
"appInitializing": "Initializing", "appInitializing": "Initializing",
"poweredBy": "Powered by {}", "poweredBy": "Powered by {}",
"shareIntent": "Share", "shareIntent": "Share",
"shareIntentDescription": "What do you want to do with the content you are sharing?", "shareIntentDescription": "What do you want to do with the content you are sharing?",
"shareIntentPostStory": "Post a Story", "shareIntentPostStory": "Post a Story",
"shareIntentSendChannel": "Share to Channel",
"updateAvailable": "Update Available", "updateAvailable": "Update Available",
"updateOngoing": "Updating, please wait...", "updateOngoing": "Updating, please wait...",
"custom": "Custom", "custom": "Custom",
@ -537,6 +577,7 @@
"colorSchemeWhite": "White", "colorSchemeWhite": "White",
"colorSchemeBlack": "Black", "colorSchemeBlack": "Black",
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.", "colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
"postFeaturedComment": "Featured Comment",
"postCategoryTechnology": "Technology", "postCategoryTechnology": "Technology",
"postCategoryGaming": "Gaming", "postCategoryGaming": "Gaming",
"postCategoryLife": "Life", "postCategoryLife": "Life",
@ -547,5 +588,26 @@
"postCategoryKnowledge": "Knowledge", "postCategoryKnowledge": "Knowledge",
"postCategoryLiterature": "Literature", "postCategoryLiterature": "Literature",
"postCategoryFunny": "Funny", "postCategoryFunny": "Funny",
"postCategoryUncategorized": "Uncategorized" "postCategoryUncategorized": "Uncategorized",
"newsAllSources": "All News",
"newsReadingProviderSwap": "Swap",
"newsReadingFromReader": "You're reading from HyperNet.Reader",
"newsReadingFromOriginal": "You're reading the original article",
"newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.",
"newsToday": "Today's News",
"totpPostSetup": "One More Thing",
"totpPostSetupDescription": "Scan the QR Code below with Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden or any of kind of authenticator app which supports TOTP.",
"totpNeverShare": "Never share this QR Code",
"needHelp": "Need Help?",
"needHelpLaunch": "Check out our Goatpedia!",
"walletCreate": "Create a Wallet",
"walletCreateSubtitle": "Create a wallet to start using Source Points",
"walletCreatePassword": "Set a payment password for your new wallet below",
"walletCurrencyShort": "SRC",
"walletCurrency": {
"one": "{} Source Point",
"other": "{} Source Points"
},
"aiThinkingProcess": "AI Thinking Process",
"accountSettingsApplied": "Account settings have been applied."
} }

View File

@ -15,6 +15,10 @@
"screenAccountProfileEdit": "编辑资料", "screenAccountProfileEdit": "编辑资料",
"screenAbuseReport": "滥用检举", "screenAbuseReport": "滥用检举",
"screenSettings": "设置", "screenSettings": "设置",
"screenAccountSettings": "账号设置",
"screenFactorSettings": "验证因子",
"screenAccountWallet": "钱包",
"screenNews": "新闻",
"screenAlbum": "相册", "screenAlbum": "相册",
"screenChat": "聊天", "screenChat": "聊天",
"screenChatManage": "编辑聊天频道", "screenChatManage": "编辑聊天频道",
@ -87,8 +91,18 @@
}, },
"loginEnterPassword": "验证代码", "loginEnterPassword": "验证代码",
"loginSuccess": "登录为 {}", "loginSuccess": "登录为 {}",
"authFactorDelete": "删除验证因子",
"authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
"authFactorPassword": "密码", "authFactorPassword": "密码",
"authFactorPasswordDescription": "注册时选择设置的密码。",
"authFactorEmail": "电邮一次性验证码", "authFactorEmail": "电邮一次性验证码",
"authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
"authFactorTOTP": "时序验证码",
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
"authFactorInAppNotify": "应用内通知验证码",
"authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
"authFactorAdd": "添加新验证因子",
"authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
"accountIntroTitle": "喜欢您来!", "accountIntroTitle": "喜欢您来!",
"accountIntroSubtitle": "登陆以探索更广大的世界。", "accountIntroSubtitle": "登陆以探索更广大的世界。",
"accountLogout": "退出登录", "accountLogout": "退出登录",
@ -97,8 +111,14 @@
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。", "accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
"accountPublishers": "你的发布者", "accountPublishers": "你的发布者",
"accountPublishersSubtitle": "管理你的公共形象。", "accountPublishersSubtitle": "管理你的公共形象。",
"accountSettings": "帐号设置",
"accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
"accountProfileEdit": "编辑资料", "accountProfileEdit": "编辑资料",
"accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。", "accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
"accountWallet": "钱包",
"accountWalletSubtitle": "查看你的余额和交易记录。",
"factorSettings": "验证因子",
"factorSettingsSubtitle": "管理你的登陆验证方式。",
"accountProfileEditApplied": "个人资料修改已被应用。", "accountProfileEditApplied": "个人资料修改已被应用。",
"publishersNew": "新发布者", "publishersNew": "新发布者",
"publisherNewSubtitle": "创建一个新的公共身份。", "publisherNewSubtitle": "创建一个新的公共身份。",
@ -177,6 +197,9 @@
"other": "{} 条评论" "other": "{} 条评论"
}, },
"settingsAppearance": "外观", "settingsAppearance": "外观",
"settingsDisplayLanguage": "显示语言",
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
"settingsDisplayLanguageSystem": "跟随系统",
"settingsBackgroundImage": "背景图片", "settingsBackgroundImage": "背景图片",
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。", "settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
"settingsBackgroundImageClear": "清除现存背景图", "settingsBackgroundImageClear": "清除现存背景图",
@ -185,10 +208,19 @@
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。", "settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
"settingsAppBarTransparent": "透明顶栏", "settingsAppBarTransparent": "透明顶栏",
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。", "settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
"settingsDrawerPreferCollapse": "侧边栏偏好折叠",
"settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
"settingsColorScheme": "主题色", "settingsColorScheme": "主题色",
"settingsColorSchemeDescription": "设置应用主题色。", "settingsColorSchemeDescription": "设置应用主题色。",
"settingsColorSeed": "预设色彩主题", "settingsColorSeed": "预设色彩主题",
"settingsColorSeedDescription": "选择一个预设色彩主题。", "settingsColorSeedDescription": "选择一个预设色彩主题。",
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知时振动",
"settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
"settingsExpandPostLink": "展开帖子链接",
"settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。",
"settingsExpandChatLink": "展开聊天链接",
"settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。",
"settingsNetwork": "网络", "settingsNetwork": "网络",
"settingsNetworkServer": "HyperNet 服务器", "settingsNetworkServer": "HyperNet 服务器",
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。", "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
@ -207,12 +239,15 @@
"settingsMisc": "杂项", "settingsMisc": "杂项",
"settingsMiscAbout": "关于", "settingsMiscAbout": "关于",
"settingsMiscAboutDescription": "查看 Solian 的版本信息。", "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
"settingsAccountLanguage": "帐号偏好语言",
"settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。",
"sensitiveContent": "敏感内容", "sensitiveContent": "敏感内容",
"sensitiveContentCollapsed": "敏感内容已折叠。", "sensitiveContentCollapsed": "敏感内容已折叠。",
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。", "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
"sensitiveContentReveal": "显示内容", "sensitiveContentReveal": "显示内容",
"serverConnecting": "正在连接服务器…", "serverConnecting": "正在连接…",
"serverDisconnected": "已与服务器断开连接", "serverDisconnected": "已断开连接",
"serverConnected": "已连接",
"fieldChatAlias": "频道别名", "fieldChatAlias": "频道别名",
"fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。", "fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
"fieldChatName": "名称", "fieldChatName": "名称",
@ -290,6 +325,7 @@
"addAttachmentFromCameraPhoto": "拍摄照片", "addAttachmentFromCameraPhoto": "拍摄照片",
"addAttachmentFromCameraVideo": "拍摄视频", "addAttachmentFromCameraVideo": "拍摄视频",
"addAttachmentFromRandomId": "通过访问 ID 链接", "addAttachmentFromRandomId": "通过访问 ID 链接",
"attachmentDetailInfo": "附件详细信息",
"attachmentPastedImage": "粘贴的图片", "attachmentPastedImage": "粘贴的图片",
"attachmentInsertLink": "插入连接", "attachmentInsertLink": "插入连接",
"attachmentSetAsPostThumbnail": "设置为帖子缩略图", "attachmentSetAsPostThumbnail": "设置为帖子缩略图",
@ -518,11 +554,15 @@
"postImageShareAds": "来 Solar Network 探索更多有趣帖子", "postImageShareAds": "来 Solar Network 探索更多有趣帖子",
"postShare": "分享", "postShare": "分享",
"postShareImage": "分享帖图", "postShareImage": "分享帖图",
"postGetInsight": "获取见解",
"postGetInsightTitle": "AI 见解",
"postGetInsightDescription": "AI 可能会出错,检查信息真实性。",
"appInitializing": "正在初始化", "appInitializing": "正在初始化",
"poweredBy": "由 {} 提供支持", "poweredBy": "由 {} 提供支持",
"shareIntent": "分享", "shareIntent": "分享",
"shareIntentDescription": "您想对您分享的内容做些什么?", "shareIntentDescription": "您想对您分享的内容做些什么?",
"shareIntentPostStory": "发布动态", "shareIntentPostStory": "发布动态",
"shareIntentSendChannel": "分享到聊天频道",
"updateAvailable": "检测到更新可用", "updateAvailable": "检测到更新可用",
"updateOngoing": "正在更新,请稍后……", "updateOngoing": "正在更新,请稍后……",
"custom": "自定义", "custom": "自定义",
@ -535,6 +575,7 @@
"colorSchemeWhite": "白色", "colorSchemeWhite": "白色",
"colorSchemeBlack": "黑色", "colorSchemeBlack": "黑色",
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。", "colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
"postFeaturedComment": "精选评论",
"postCategoryTechnology": "技术", "postCategoryTechnology": "技术",
"postCategoryGaming": "游戏", "postCategoryGaming": "游戏",
"postCategoryLife": "生活", "postCategoryLife": "生活",
@ -545,5 +586,26 @@
"postCategoryKnowledge": "知识", "postCategoryKnowledge": "知识",
"postCategoryLiterature": "文学", "postCategoryLiterature": "文学",
"postCategoryFunny": "搞笑", "postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分类" "postCategoryUncategorized": "未分类",
"newsAllSources": "所有新闻",
"newsReadingProviderSwap": "切换",
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
"newsReadingFromOriginal": "你正在阅读原始文章",
"newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。",
"newsToday": "快讯",
"totpPostSetup": "还有一件事",
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的验证器扫描本 QR Code 来添加。",
"totpNeverShare": "永远不要分享这个 QR Code",
"needHelp": "需要帮助?",
"needHelpLaunch": "查看我们的山羊维基!",
"walletCreate": "创建钱包",
"walletCreateSubtitle": "创建于一个钱包来开始使用源点。",
"walletCreatePassword": "在下方设置你的付款密码",
"walletCurrencyShort": "源点",
"walletCurrency": {
"one": "{} 源点",
"other": "{} 源点"
},
"aiThinkingProcess": "AI 思考过程",
"accountSettingsApplied": "帐号设置已应用。"
} }

View File

@ -15,6 +15,10 @@
"screenAccountProfileEdit": "編輯資料", "screenAccountProfileEdit": "編輯資料",
"screenAbuseReport": "濫用檢舉", "screenAbuseReport": "濫用檢舉",
"screenSettings": "設置", "screenSettings": "設置",
"screenAccountSettings": "賬號設置",
"screenFactorSettings": "驗證因子",
"screenAccountWallet": "錢包",
"screenNews": "新聞",
"screenAlbum": "相冊", "screenAlbum": "相冊",
"screenChat": "聊天", "screenChat": "聊天",
"screenChatManage": "編輯聊天頻道", "screenChatManage": "編輯聊天頻道",
@ -87,8 +91,18 @@
}, },
"loginEnterPassword": "驗證代碼", "loginEnterPassword": "驗證代碼",
"loginSuccess": "登錄為 {}", "loginSuccess": "登錄為 {}",
"authFactorDelete": "刪除驗證因子",
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
"authFactorPassword": "密碼", "authFactorPassword": "密碼",
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
"authFactorEmail": "電郵一次性驗證碼", "authFactorEmail": "電郵一次性驗證碼",
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
"authFactorTOTP": "時序驗證碼",
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
"authFactorInAppNotify": "應用內通知驗證碼",
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
"authFactorAdd": "添加新驗證因子",
"authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。",
"accountIntroTitle": "喜歡您來!", "accountIntroTitle": "喜歡您來!",
"accountIntroSubtitle": "登陸以探索更廣大的世界。", "accountIntroSubtitle": "登陸以探索更廣大的世界。",
"accountLogout": "退出登錄", "accountLogout": "退出登錄",
@ -97,8 +111,14 @@
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。", "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
"accountPublishers": "你的發佈者", "accountPublishers": "你的發佈者",
"accountPublishersSubtitle": "管理你的公共形象。", "accountPublishersSubtitle": "管理你的公共形象。",
"accountSettings": "帳號設置",
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
"accountProfileEdit": "編輯資料", "accountProfileEdit": "編輯資料",
"accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。", "accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
"accountWallet": "錢包",
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
"factorSettings": "驗證因子",
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
"accountProfileEditApplied": "個人資料修改已被應用。", "accountProfileEditApplied": "個人資料修改已被應用。",
"publishersNew": "新發布者", "publishersNew": "新發布者",
"publisherNewSubtitle": "創建一個新的公共身份。", "publisherNewSubtitle": "創建一個新的公共身份。",
@ -177,6 +197,9 @@
"other": "{} 條評論" "other": "{} 條評論"
}, },
"settingsAppearance": "外觀", "settingsAppearance": "外觀",
"settingsDisplayLanguage": "顯示語言",
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
"settingsDisplayLanguageSystem": "跟隨系統",
"settingsBackgroundImage": "背景圖片", "settingsBackgroundImage": "背景圖片",
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。", "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
"settingsBackgroundImageClear": "清除現存背景圖", "settingsBackgroundImageClear": "清除現存背景圖",
@ -185,10 +208,19 @@
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。", "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
"settingsAppBarTransparent": "透明頂欄", "settingsAppBarTransparent": "透明頂欄",
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。", "settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
"settingsColorScheme": "主題色", "settingsColorScheme": "主題色",
"settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題", "settingsColorSeed": "預設色彩主題",
"settingsColorSeedDescription": "選擇一個預設色彩主題。", "settingsColorSeedDescription": "選擇一個預設色彩主題。",
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知時振動",
"settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
"settingsExpandPostLink": "展開帖子鏈接",
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
"settingsExpandChatLink": "展開聊天鏈接",
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
"settingsNetwork": "網絡", "settingsNetwork": "網絡",
"settingsNetworkServer": "HyperNet 服務器", "settingsNetworkServer": "HyperNet 服務器",
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
@ -207,12 +239,15 @@
"settingsMisc": "雜項", "settingsMisc": "雜項",
"settingsMiscAbout": "關於", "settingsMiscAbout": "關於",
"settingsMiscAboutDescription": "查看 Solian 的版本信息。", "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
"settingsAccountLanguage": "帳號偏好語言",
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
"sensitiveContent": "敏感內容", "sensitiveContent": "敏感內容",
"sensitiveContentCollapsed": "敏感內容已摺疊。", "sensitiveContentCollapsed": "敏感內容已摺疊。",
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
"sensitiveContentReveal": "顯示內容", "sensitiveContentReveal": "顯示內容",
"serverConnecting": "正在連接服務器…", "serverConnecting": "正在連接…",
"serverDisconnected": "已與服務器斷開連接", "serverDisconnected": "已斷開連接",
"serverConnected": "已連接",
"fieldChatAlias": "頻道別名", "fieldChatAlias": "頻道別名",
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
"fieldChatName": "名稱", "fieldChatName": "名稱",
@ -290,6 +325,7 @@
"addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraPhoto": "拍攝照片",
"addAttachmentFromCameraVideo": "拍攝視頻", "addAttachmentFromCameraVideo": "拍攝視頻",
"addAttachmentFromRandomId": "通過訪問 ID 鏈接", "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
"attachmentDetailInfo": "附件詳細信息",
"attachmentPastedImage": "粘貼的圖片", "attachmentPastedImage": "粘貼的圖片",
"attachmentInsertLink": "插入連接", "attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖", "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
@ -518,11 +554,15 @@
"postImageShareAds": "來 Solar Network 探索更多有趣帖子", "postImageShareAds": "來 Solar Network 探索更多有趣帖子",
"postShare": "分享", "postShare": "分享",
"postShareImage": "分享帖圖", "postShareImage": "分享帖圖",
"postGetInsight": "獲取見解",
"postGetInsightTitle": "AI 見解",
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
"appInitializing": "正在初始化", "appInitializing": "正在初始化",
"poweredBy": "由 {} 提供支持", "poweredBy": "由 {} 提供支持",
"shareIntent": "分享", "shareIntent": "分享",
"shareIntentDescription": "您想對您分享的內容做些什麼?", "shareIntentDescription": "您想對您分享的內容做些什麼?",
"shareIntentPostStory": "發佈動態", "shareIntentPostStory": "發佈動態",
"shareIntentSendChannel": "分享到聊天頻道",
"updateAvailable": "檢測到更新可用", "updateAvailable": "檢測到更新可用",
"updateOngoing": "正在更新,請稍後……", "updateOngoing": "正在更新,請稍後……",
"custom": "自定義", "custom": "自定義",
@ -535,6 +575,7 @@
"colorSchemeWhite": "白色", "colorSchemeWhite": "白色",
"colorSchemeBlack": "黑色", "colorSchemeBlack": "黑色",
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。", "colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
"postFeaturedComment": "精選評論",
"postCategoryTechnology": "技術", "postCategoryTechnology": "技術",
"postCategoryGaming": "遊戲", "postCategoryGaming": "遊戲",
"postCategoryLife": "生活", "postCategoryLife": "生活",
@ -545,5 +586,26 @@
"postCategoryKnowledge": "知識", "postCategoryKnowledge": "知識",
"postCategoryLiterature": "文學", "postCategoryLiterature": "文學",
"postCategoryFunny": "搞笑", "postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分類" "postCategoryUncategorized": "未分類",
"newsAllSources": "所有新聞",
"newsReadingProviderSwap": "切換",
"newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
"newsReadingFromOriginal": "你正在閲讀原始文章",
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
"newsToday": "快訊",
"totpPostSetup": "還有一件事",
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。",
"totpNeverShare": "永遠不要分享這個 QR Code",
"needHelp": "需要幫助?",
"needHelpLaunch": "查看我們的山羊維基!",
"walletCreate": "創建錢包",
"walletCreateSubtitle": "創建於一個錢包來開始使用源點。",
"walletCreatePassword": "在下方設置你的付款密碼",
"walletCurrencyShort": "源點",
"walletCurrency": {
"one": "{} 源點",
"other": "{} 源點"
},
"aiThinkingProcess": "AI 思考過程",
"accountSettingsApplied": "帳號設置已應用。"
} }

View File

@ -15,6 +15,10 @@
"screenAccountProfileEdit": "編輯資料", "screenAccountProfileEdit": "編輯資料",
"screenAbuseReport": "濫用檢舉", "screenAbuseReport": "濫用檢舉",
"screenSettings": "設置", "screenSettings": "設置",
"screenAccountSettings": "賬號設置",
"screenFactorSettings": "驗證因子",
"screenAccountWallet": "錢包",
"screenNews": "新聞",
"screenAlbum": "相冊", "screenAlbum": "相冊",
"screenChat": "聊天", "screenChat": "聊天",
"screenChatManage": "編輯聊天頻道", "screenChatManage": "編輯聊天頻道",
@ -87,8 +91,18 @@
}, },
"loginEnterPassword": "驗證代碼", "loginEnterPassword": "驗證代碼",
"loginSuccess": "登錄為 {}", "loginSuccess": "登錄為 {}",
"authFactorDelete": "刪除驗證因子",
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
"authFactorPassword": "密碼", "authFactorPassword": "密碼",
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
"authFactorEmail": "電郵一次性驗證碼", "authFactorEmail": "電郵一次性驗證碼",
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
"authFactorTOTP": "時序驗證碼",
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
"authFactorInAppNotify": "應用內通知驗證碼",
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
"authFactorAdd": "添加新驗證因子",
"authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。",
"accountIntroTitle": "喜歡您來!", "accountIntroTitle": "喜歡您來!",
"accountIntroSubtitle": "登陸以探索更廣大的世界。", "accountIntroSubtitle": "登陸以探索更廣大的世界。",
"accountLogout": "退出登錄", "accountLogout": "退出登錄",
@ -97,8 +111,14 @@
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。", "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
"accountPublishers": "你的發佈者", "accountPublishers": "你的發佈者",
"accountPublishersSubtitle": "管理你的公共形象。", "accountPublishersSubtitle": "管理你的公共形象。",
"accountSettings": "帳號設置",
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
"accountProfileEdit": "編輯資料", "accountProfileEdit": "編輯資料",
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。", "accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
"accountWallet": "錢包",
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
"factorSettings": "驗證因子",
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
"accountProfileEditApplied": "個人資料修改已被應用。", "accountProfileEditApplied": "個人資料修改已被應用。",
"publishersNew": "新發布者", "publishersNew": "新發布者",
"publisherNewSubtitle": "創建一個新的公共身份。", "publisherNewSubtitle": "創建一個新的公共身份。",
@ -177,6 +197,9 @@
"other": "{} 條評論" "other": "{} 條評論"
}, },
"settingsAppearance": "外觀", "settingsAppearance": "外觀",
"settingsDisplayLanguage": "顯示語言",
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
"settingsDisplayLanguageSystem": "跟隨系統",
"settingsBackgroundImage": "背景圖片", "settingsBackgroundImage": "背景圖片",
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。", "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
"settingsBackgroundImageClear": "清除現存背景圖", "settingsBackgroundImageClear": "清除現存背景圖",
@ -185,10 +208,19 @@
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。", "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
"settingsAppBarTransparent": "透明頂欄", "settingsAppBarTransparent": "透明頂欄",
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。", "settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
"settingsColorScheme": "主題色", "settingsColorScheme": "主題色",
"settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題", "settingsColorSeed": "預設色彩主題",
"settingsColorSeedDescription": "選擇一個預設色彩主題。", "settingsColorSeedDescription": "選擇一個預設色彩主題。",
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知時振動",
"settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
"settingsExpandPostLink": "展開帖子鏈接",
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
"settingsExpandChatLink": "展開聊天鏈接",
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
"settingsNetwork": "網絡", "settingsNetwork": "網絡",
"settingsNetworkServer": "HyperNet 服務器", "settingsNetworkServer": "HyperNet 服務器",
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
@ -207,12 +239,15 @@
"settingsMisc": "雜項", "settingsMisc": "雜項",
"settingsMiscAbout": "關於", "settingsMiscAbout": "關於",
"settingsMiscAboutDescription": "查看 Solian 的版本信息。", "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
"settingsAccountLanguage": "帳號偏好語言",
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
"sensitiveContent": "敏感內容", "sensitiveContent": "敏感內容",
"sensitiveContentCollapsed": "敏感內容已摺疊。", "sensitiveContentCollapsed": "敏感內容已摺疊。",
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
"sensitiveContentReveal": "顯示內容", "sensitiveContentReveal": "顯示內容",
"serverConnecting": "正在連接服務器…", "serverConnecting": "正在連接…",
"serverDisconnected": "已與服務器斷開連接", "serverDisconnected": "已斷開連接",
"serverConnected": "已連接",
"fieldChatAlias": "頻道別名", "fieldChatAlias": "頻道別名",
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
"fieldChatName": "名稱", "fieldChatName": "名稱",
@ -290,6 +325,7 @@
"addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraPhoto": "拍攝照片",
"addAttachmentFromCameraVideo": "拍攝視頻", "addAttachmentFromCameraVideo": "拍攝視頻",
"addAttachmentFromRandomId": "通過訪問 ID 鏈接", "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
"attachmentDetailInfo": "附件詳細信息",
"attachmentPastedImage": "粘貼的圖片", "attachmentPastedImage": "粘貼的圖片",
"attachmentInsertLink": "插入連接", "attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖", "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
@ -518,11 +554,15 @@
"postImageShareAds": "來 Solar Network 探索更多有趣帖子", "postImageShareAds": "來 Solar Network 探索更多有趣帖子",
"postShare": "分享", "postShare": "分享",
"postShareImage": "分享帖圖", "postShareImage": "分享帖圖",
"postGetInsight": "獲取見解",
"postGetInsightTitle": "AI 見解",
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
"appInitializing": "正在初始化", "appInitializing": "正在初始化",
"poweredBy": "由 {} 提供支持", "poweredBy": "由 {} 提供支持",
"shareIntent": "分享", "shareIntent": "分享",
"shareIntentDescription": "您想對您分享的內容做些什麼?", "shareIntentDescription": "您想對您分享的內容做些什麼?",
"shareIntentPostStory": "發佈動態", "shareIntentPostStory": "發佈動態",
"shareIntentSendChannel": "分享到聊天頻道",
"updateAvailable": "檢測到更新可用", "updateAvailable": "檢測到更新可用",
"updateOngoing": "正在更新,請稍後……", "updateOngoing": "正在更新,請稍後……",
"custom": "自定義", "custom": "自定義",
@ -535,6 +575,7 @@
"colorSchemeWhite": "白色", "colorSchemeWhite": "白色",
"colorSchemeBlack": "黑色", "colorSchemeBlack": "黑色",
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。", "colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
"postFeaturedComment": "精選評論",
"postCategoryTechnology": "技術", "postCategoryTechnology": "技術",
"postCategoryGaming": "遊戲", "postCategoryGaming": "遊戲",
"postCategoryLife": "生活", "postCategoryLife": "生活",
@ -545,5 +586,26 @@
"postCategoryKnowledge": "知識", "postCategoryKnowledge": "知識",
"postCategoryLiterature": "文學", "postCategoryLiterature": "文學",
"postCategoryFunny": "搞笑", "postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分類" "postCategoryUncategorized": "未分類",
"newsAllSources": "所有新聞",
"newsReadingProviderSwap": "切換",
"newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章",
"newsReadingFromOriginal": "你正在閱讀原始文章",
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
"newsToday": "快訊",
"totpPostSetup": "還有一件事",
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。",
"totpNeverShare": "永遠不要分享這個 QR Code",
"needHelp": "需要幫助?",
"needHelpLaunch": "查看我們的山羊維基!",
"walletCreate": "創建錢包",
"walletCreateSubtitle": "創建於一個錢包來開始使用源點。",
"walletCreatePassword": "在下方設置你的付款密碼",
"walletCurrencyShort": "源點",
"walletCurrency": {
"one": "{} 源點",
"other": "{} 源點"
},
"aiThinkingProcess": "AI 思考過程",
"accountSettingsApplied": "帳號設置已應用。"
} }

View File

@ -43,58 +43,58 @@ PODS:
- Flutter - Flutter
- file_saver (0.0.1): - file_saver (0.0.1):
- Flutter - Flutter
- Firebase/Analytics (11.4.0): - Firebase/Analytics (11.6.0):
- Firebase/Core - Firebase/Core
- Firebase/Core (11.4.0): - Firebase/Core (11.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseAnalytics (~> 11.4.0) - FirebaseAnalytics (~> 11.6.0)
- Firebase/CoreOnly (11.4.0): - Firebase/CoreOnly (11.6.0):
- FirebaseCore (= 11.4.0) - FirebaseCore (~> 11.6.0)
- Firebase/Messaging (11.4.0): - Firebase/Messaging (11.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.4.0) - FirebaseMessaging (~> 11.6.0)
- firebase_analytics (11.3.6): - firebase_analytics (11.4.1):
- Firebase/Analytics (= 11.4.0) - Firebase/Analytics (= 11.6.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_core (3.9.0): - firebase_core (3.10.1):
- Firebase/CoreOnly (= 11.4.0) - Firebase/CoreOnly (= 11.6.0)
- Flutter - Flutter
- firebase_messaging (15.1.6): - firebase_messaging (15.2.1):
- Firebase/Messaging (= 11.4.0) - Firebase/Messaging (= 11.6.0)
- firebase_core - firebase_core
- Flutter - Flutter
- FirebaseAnalytics (11.4.0): - FirebaseAnalytics (11.6.0):
- FirebaseAnalytics/AdIdSupport (= 11.4.0) - FirebaseAnalytics/AdIdSupport (= 11.6.0)
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseAnalytics/AdIdSupport (11.4.0): - FirebaseAnalytics/AdIdSupport (11.6.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.4.0) - GoogleAppMeasurement (= 11.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseCore (11.4.0): - FirebaseCore (11.6.0):
- FirebaseCoreInternal (~> 11.0) - FirebaseCoreInternal (~> 11.6.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0) - GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreInternal (11.6.0): - FirebaseCoreInternal (11.6.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseInstallations (11.4.0): - FirebaseInstallations (11.6.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseMessaging (11.4.0): - FirebaseMessaging (11.6.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0) - GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -105,32 +105,39 @@ PODS:
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_app_update (0.0.1): - flutter_app_update (0.0.1):
- Flutter - Flutter
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 6.0.3)
- flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 6.0.3)
- flutter_native_splash (2.4.3): - flutter_native_splash (2.4.3):
- Flutter - Flutter
- flutter_udid (0.0.1): - flutter_udid (0.0.1):
- Flutter - Flutter
- SAMKeychain - SAMKeychain
- flutter_webrtc (0.12.2): - flutter_webrtc (0.12.6):
- Flutter - Flutter
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- GoogleAppMeasurement (11.4.0): - GoogleAppMeasurement (11.6.0):
- GoogleAppMeasurement/AdIdSupport (= 11.4.0) - GoogleAppMeasurement/AdIdSupport (= 11.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/AdIdSupport (11.4.0): - GoogleAppMeasurement/AdIdSupport (11.6.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0) - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/WithoutAdIdSupport (11.4.0): - GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
@ -173,7 +180,7 @@ PODS:
- in_app_review (2.0.0): - in_app_review (2.0.0):
- Flutter - Flutter
- Kingfisher (8.1.3) - Kingfisher (8.1.3)
- livekit_client (2.3.4): - livekit_client (2.3.5):
- Flutter - Flutter
- flutter_webrtc - flutter_webrtc
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
@ -188,6 +195,7 @@ PODS:
- nanopb/encode (= 3.30910.0) - nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0) - nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0) - nanopb/encode (3.30910.0)
- OrderedSet (6.0.3)
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- pasteboard (0.0.1): - pasteboard (0.0.1):
@ -239,6 +247,7 @@ DEPENDENCIES:
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`) - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
@ -282,6 +291,7 @@ SPEC REPOS:
- GoogleUtilities - GoogleUtilities
- Kingfisher - Kingfisher
- nanopb - nanopb
- OrderedSet
- PromisesObjC - PromisesObjC
- SAMKeychain - SAMKeychain
- SDWebImage - SDWebImage
@ -309,6 +319,8 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
flutter_app_update: flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios" :path: ".symlinks/plugins/flutter_app_update/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_native_splash: flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_udid: flutter_udid:
@ -367,35 +379,37 @@ SPEC CHECKSUMS:
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75 firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e
firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10 firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b
firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9 firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49 FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563 flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
livekit_client: 4eaa7a2968fc7e7c57888f43d90394547cc8d9e9 livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46

View File

@ -74,6 +74,7 @@ class ChatMessageController extends ChangeNotifier {
_wsSubscription = _ws.stream.stream.listen((event) { _wsSubscription = _ws.stream.stream.listen((event) {
switch (event.method) { switch (event.method) {
case 'events.new': case 'events.new':
if (event.payload?['channel_id'] != channel?.id) break;
final payload = SnChatMessage.fromJson(event.payload!); final payload = SnChatMessage.fromJson(event.payload!);
_addMessage(payload); _addMessage(payload);
break; break;

View File

@ -154,7 +154,10 @@ class PostWriteController extends ChangeNotifier {
final TextEditingController descriptionController = TextEditingController(); final TextEditingController descriptionController = TextEditingController();
final TextEditingController aliasController = TextEditingController(); final TextEditingController aliasController = TextEditingController();
PostWriteController() { bool _temporarySaveActive = false;
PostWriteController({bool doLoadFromTemporary = true}) {
_temporarySaveActive = doLoadFromTemporary;
titleController.addListener(() { titleController.addListener(() {
_temporaryPlanSave(); _temporaryPlanSave();
notifyListeners(); notifyListeners();
@ -166,7 +169,7 @@ class PostWriteController extends ChangeNotifier {
contentController.addListener(() { contentController.addListener(() {
_temporaryPlanSave(); _temporaryPlanSave();
}); });
_temporaryLoad(); if (doLoadFromTemporary) _temporaryLoad();
} }
String mode = kTitleMap.keys.first; String mode = kTitleMap.keys.first;
@ -317,6 +320,7 @@ class PostWriteController extends ChangeNotifier {
Timer? _temporarySaveTimer; Timer? _temporarySaveTimer;
void _temporaryPlanSave() { void _temporaryPlanSave() {
if (!_temporarySaveActive) return;
_temporarySaveTimer?.cancel(); _temporarySaveTimer?.cancel();
_temporarySaveTimer = Timer(const Duration(seconds: 1), () { _temporarySaveTimer = Timer(const Duration(seconds: 1), () {
_temporarySave(); _temporarySave();

View File

@ -258,6 +258,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
Future<void> _initialize() async { Future<void> _initialize() async {
try { try {
final cfg = context.read<ConfigProvider>();
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
final home = context.read<HomeWidgetProvider>(); final home = context.read<HomeWidgetProvider>();
await home.initialize(); await home.initialize();
if (!mounted) return; if (!mounted) return;
@ -275,6 +279,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
await ws.tryConnect(); await ws.tryConnect();
if (!mounted) return; if (!mounted) return;
final notify = context.read<NotificationProvider>(); final notify = context.read<NotificationProvider>();
notify.listen();
await notify.registerPushNotifications(); await notify.registerPushNotifications();
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
@ -298,6 +303,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.child; final cfg = context.read<ConfigProvider>();
return NotificationListener<SizeChangedLayoutNotification>(
onNotification: (notification) {
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
return false;
},
child: SizeChangedLayoutNotifier(
child: widget.child,
),
);
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/providers/widget.dart'; import 'package:surface/providers/widget.dart';
@ -12,6 +13,10 @@ const kNetworkServerStoreKey = 'app_server_url';
const kAppbarTransparentStoreKey = 'app_bar_transparent'; const kAppbarTransparentStoreKey = 'app_bar_transparent';
const kAppBackgroundStoreKey = 'app_has_background'; const kAppBackgroundStoreKey = 'app_has_background';
const kAppColorSchemeStoreKey = 'app_color_scheme'; const kAppColorSchemeStoreKey = 'app_color_scheme';
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const kAppExpandPostLink = 'app_expand_post_link';
const kAppExpandChatLink = 'app_expand_chat_link';
const Map<String, FilterQuality> kImageQualityLevel = { const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none, 'settingsImageQualityLowest': FilterQuality.none,
@ -33,6 +38,32 @@ class ConfigProvider extends ChangeNotifier {
prefs = await SharedPreferences.getInstance(); prefs = await SharedPreferences.getInstance();
} }
bool drawerIsCollapsed = false;
bool drawerIsExpanded = false;
void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
bool newDrawerIsCollapsed = false;
bool newDrawerIsExpanded = false;
if (withMediaQuery) {
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450;
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451;
} else {
final rpb = ResponsiveBreakpoints.of(context);
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
newDrawerIsExpanded = rpb.largerThan(TABLET)
? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
? false
: true
: false;
}
if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
drawerIsExpanded = newDrawerIsExpanded;
drawerIsCollapsed = newDrawerIsCollapsed;
notifyListeners();
}
}
FilterQuality get imageQuality { FilterQuality get imageQuality {
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high; return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
} }

View File

@ -58,6 +58,11 @@ class NavigationProvider extends ChangeNotifier {
screen: 'realm', screen: 'realm',
label: 'screenRealm', label: 'screenRealm',
), ),
AppNavDestination(
icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
screen: 'news',
label: 'screenNews',
),
AppNavDestination( AppNavDestination(
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20), icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
screen: 'album', screen: 'album',
@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier {
List<AppNavDestination> destinations = []; List<AppNavDestination> destinations = [];
int get pinnedDestinationCount => int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
destinations.where((ele) => ele.isPinned).length;
NavigationProvider() { NavigationProvider() {
buildDestinations(kDefaultPinnedDestination); buildDestinations(kDefaultPinnedDestination);
@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier {
} }
bool isIndexInRange(int min, int max) { bool isIndexInRange(int min, int max) {
return _currentIndex != null && return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
_currentIndex! >= min &&
_currentIndex! < max;
} }
void autoDetectIndex(GoRouter? state) { void autoDetectIndex(GoRouter? state) {
if (state == null) return; if (state == null) return;
final idx = destinations.indexWhere( final idx = destinations.indexWhere(
(ele) => (ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
ele.screen ==
state.routerDelegate.currentConfiguration.last.route.name,
); );
_currentIndex = idx == -1 ? null : idx; _currentIndex = idx == -1 ? null : idx;
notifyListeners(); notifyListeners();

View File

@ -4,18 +4,26 @@ import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_udid/flutter_udid.dart'; import 'package:flutter_udid/flutter_udid.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/types/notification.dart';
class NotificationProvider extends ChangeNotifier { class NotificationProvider extends ChangeNotifier {
late final SnNetworkProvider _sn; late final SnNetworkProvider _sn;
late final UserProvider _ua; late final UserProvider _ua;
late final WebSocketProvider _ws;
late final ConfigProvider _cfg;
NotificationProvider(BuildContext context) { NotificationProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>(); _sn = context.read<SnNetworkProvider>();
_ua = context.read<UserProvider>(); _ua = context.read<UserProvider>();
_ws = context.read<WebSocketProvider>();
_cfg = context.read<ConfigProvider>();
} }
Future<void> registerPushNotifications() async { Future<void> registerPushNotifications() async {
@ -62,4 +70,30 @@ class NotificationProvider extends ChangeNotifier {
}, },
); );
} }
int showingCount = 0;
List<SnNotification> notifications = List.empty(growable: true);
void listen() {
_ws.stream.stream.listen((event) {
if (event.method == 'notifications.new') {
final notification = SnNotification.fromJson(event.payload!);
if (showingCount < 0) showingCount = 0;
showingCount++;
notifications.add(notification);
Future.delayed(const Duration(seconds: 3), () {
if (showingCount >= 0) showingCount--;
notifyListeners();
});
notifyListeners();
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.mediumImpact();
}
});
}
void clear() {
showingCount = 0;
notifyListeners();
}
} }

View File

@ -53,4 +53,11 @@ class UserProvider extends ChangeNotifier {
user = null; user = null;
notifyListeners(); notifyListeners();
} }
void setLanguage(String? value) {
if (value == null) return;
if (user == null) return;
user = user!.copyWith(language: value);
notifyListeners();
}
} }

View File

@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:surface/screens/abuse_report.dart'; import 'package:surface/screens/abuse_report.dart';
import 'package:surface/screens/account.dart'; import 'package:surface/screens/account.dart';
import 'package:surface/screens/account/pfp.dart'; import 'package:surface/screens/account/account_settings.dart';
import 'package:surface/screens/account/factor_settings.dart';
import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/screens/account/profile_edit.dart'; import 'package:surface/screens/account/profile_edit.dart';
import 'package:surface/screens/account/publishers/publisher_edit.dart'; import 'package:surface/screens/account/publishers/publisher_edit.dart';
import 'package:surface/screens/account/publishers/publisher_new.dart'; import 'package:surface/screens/account/publishers/publisher_new.dart';
@ -19,6 +21,8 @@ import 'package:surface/screens/chat/room.dart';
import 'package:surface/screens/explore.dart'; import 'package:surface/screens/explore.dart';
import 'package:surface/screens/friend.dart'; import 'package:surface/screens/friend.dart';
import 'package:surface/screens/home.dart'; import 'package:surface/screens/home.dart';
import 'package:surface/screens/news/news_detail.dart';
import 'package:surface/screens/news/news_list.dart';
import 'package:surface/screens/notification.dart'; import 'package:surface/screens/notification.dart';
import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/screens/post/post_detail.dart';
import 'package:surface/screens/post/post_editor.dart'; import 'package:surface/screens/post/post_editor.dart';
@ -29,290 +33,227 @@ import 'package:surface/screens/realm/manage.dart';
import 'package:surface/screens/realm/realm_detail.dart'; import 'package:surface/screens/realm/realm_detail.dart';
import 'package:surface/screens/settings.dart'; import 'package:surface/screens/settings.dart';
import 'package:surface/screens/sharing.dart'; import 'package:surface/screens/sharing.dart';
import 'package:surface/screens/wallet.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/about.dart'; import 'package:surface/widgets/about.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
Widget _fadeThroughTransition(
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: child,
);
}
final _appRoutes = [ final _appRoutes = [
ShellRoute( GoRoute(
builder: (context, state, child) => AppPageScaffold( path: '/',
body: child, name: 'home',
showAppBar: false, builder: (context, state) => const HomeScreen(),
), ),
GoRoute(
path: '/posts',
name: 'explore',
builder: (context, state) => const ExploreScreen(),
routes: [ routes: [
GoRoute( GoRoute(
path: '/', path: '/write/:mode',
name: 'home', name: 'postEditor',
pageBuilder: (context, state) => NoTransitionPage( builder: (context, state) => PostEditorScreen(
child: const HomeScreen(), mode: state.pathParameters['mode']!,
postEditId: int.tryParse(
state.uri.queryParameters['editing'] ?? '',
),
postReplyId: int.tryParse(
state.uri.queryParameters['replying'] ?? '',
),
postRepostId: int.tryParse(
state.uri.queryParameters['reposting'] ?? '',
),
extraProps: state.extra as PostEditorExtra?,
), ),
), ),
GoRoute( GoRoute(
path: '/posts', path: '/search',
name: 'explore', name: 'postSearch',
pageBuilder: (context, state) => NoTransitionPage( builder: (context, state) => PostSearchScreen(
child: const ExploreScreen(), initialTags: state.uri.queryParameters['tags']?.split(','),
), initialCategories: state.uri.queryParameters['categories']?.split(','),
routes: [
GoRoute(
path: '/write/:mode',
name: 'postEditor',
builder: (context, state) => AppBackground(
child: PostEditorScreen(
mode: state.pathParameters['mode']!,
postEditId: int.tryParse(
state.uri.queryParameters['editing'] ?? '',
),
postReplyId: int.tryParse(
state.uri.queryParameters['replying'] ?? '',
),
postRepostId: int.tryParse(
state.uri.queryParameters['reposting'] ?? '',
),
extraProps: state.extra as PostEditorExtraProps?,
),
),
),
GoRoute(
path: '/search',
name: 'postSearch',
builder: (context, state) => AppBackground(
child: PostSearchScreen(
initialTags: state.uri.queryParameters['tags']?.split(','),
initialCategories: state.uri.queryParameters['categories']?.split(','),
),
),
),
GoRoute(
path: '/publishers/:name',
name: 'postPublisher',
builder: (context, state) => AppBackground(
child: PostPublisherScreen(name: state.pathParameters['name']!),
),
),
GoRoute(
path: '/:slug',
name: 'postDetail',
builder: (context, state) => AppBackground(
child: PostDetailScreen(
slug: state.pathParameters['slug']!,
preload: state.extra as SnPost?,
),
),
),
],
),
GoRoute(
path: '/account',
name: 'account',
pageBuilder: (context, state) => NoTransitionPage(
child: const AccountScreen(),
),
routes: [],
),
GoRoute(
path: '/chat',
name: 'chat',
pageBuilder: (context, state) => NoTransitionPage(
child: const ChatScreen(),
),
routes: [
GoRoute(
path: '/:scope/:alias',
name: 'chatRoom',
builder: (context, state) => AppBackground(
child: ChatRoomScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
),
GoRoute(
path: '/:scope/:alias/call',
name: 'chatCallRoom',
builder: (context, state) => AppBackground(
child: CallRoomScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
),
GoRoute(
path: '/:scope/:alias/detail',
name: 'channelDetail',
builder: (context, state) => AppBackground(
child: ChannelDetailScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
),
GoRoute(
path: '/manage',
name: 'chatManage',
pageBuilder: (context, state) => CustomTransitionPage(
child: ChatManageScreen(
editingChannelAlias: state.uri.queryParameters['editing'],
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: AppBackground(
child: child,
),
);
},
),
),
GoRoute(
path: '/:alias',
name: 'realmDetail',
builder: (context, state) => AppBackground(
child: RealmDetailScreen(alias: state.pathParameters['alias']!),
),
),
],
),
GoRoute(
path: '/realm',
name: 'realm',
pageBuilder: (context, state) => NoTransitionPage(
child: const RealmScreen(),
),
routes: [
GoRoute(
path: '/manage',
name: 'realmManage',
pageBuilder: (context, state) => CustomTransitionPage(
child: RealmManageScreen(
editingRealmAlias: state.uri.queryParameters['editing'],
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: AppBackground(
child: child,
),
);
},
),
),
],
),
GoRoute(
path: '/album',
name: 'album',
pageBuilder: (context, state) => NoTransitionPage(
child: const AlbumScreen(),
), ),
), ),
GoRoute( GoRoute(
path: '/friend', path: '/publishers/:name',
name: 'friend', name: 'postPublisher',
pageBuilder: (context, state) => NoTransitionPage( builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
child: const FriendScreen(),
),
), ),
GoRoute( GoRoute(
path: '/notification', path: '/:slug',
name: 'notification', name: 'postDetail',
pageBuilder: (context, state) => NoTransitionPage( builder: (context, state) => PostDetailScreen(
child: const NotificationScreen(), slug: state.pathParameters['slug']!,
preload: state.extra as SnPost?,
), ),
), ),
], ],
), ),
ShellRoute( GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
builder: (context, state, child) => AppPageScaffold(body: child), GoRoute(
path: '/wallet',
name: 'accountWallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: '/settings',
name: 'accountSettings',
builder: (context, state) => AccountSettingsScreen(),
),
GoRoute(
path: '/settings/factors',
name: 'factorSettings',
builder: (context, state) => FactorSettingsScreen(),
),
GoRoute(
path: '/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(),
),
GoRoute(
path: '/publishers',
name: 'accountPublishers',
builder: (context, state) => PublisherScreen(),
),
GoRoute(
path: '/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(),
),
GoRoute(
path: '/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen(
name: state.pathParameters['name']!,
),
),
GoRoute(
path: '/:name',
name: 'accountProfilePage',
pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!),
),
),
]),
GoRoute(
path: '/chat',
name: 'chat',
builder: (context, state) => const ChatScreen(),
routes: [ routes: [
GoRoute( GoRoute(
path: '/auth/login', path: '/:scope/:alias',
name: 'authLogin', name: 'chatRoom',
builder: (context, state) => const AppBackground( builder: (context, state) => ChatRoomScreen(
child: LoginScreen(), scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
extra: state.extra as ChatRoomScreenExtra?,
), ),
), ),
GoRoute( GoRoute(
path: '/auth/register', path: '/:scope/:alias/call',
name: 'authRegister', name: 'chatCallRoom',
builder: (context, state) => const AppBackground( builder: (context, state) => CallRoomScreen(
child: RegisterScreen(), scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
), ),
), ),
GoRoute( GoRoute(
path: '/reports', path: '/:scope/:alias/detail',
name: 'abuseReport', name: 'channelDetail',
builder: (context, state) => const AppBackground( builder: (context, state) => ChannelDetailScreen(
child: AbuseReportScreen(), scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
), ),
), ),
GoRoute( GoRoute(
path: '/account/profile/edit', path: '/manage',
name: 'accountProfileEdit', name: 'chatManage',
builder: (context, state) => const AppBackground( builder: (context, state) => ChatManageScreen(
child: ProfileEditScreen(), editingChannelAlias: state.uri.queryParameters['editing'],
),
),
GoRoute(
path: '/account/publishers',
name: 'accountPublishers',
builder: (context, state) => const AppBackground(
child: PublisherScreen(),
),
),
GoRoute(
path: '/account/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => const AppBackground(
child: AccountPublisherNewScreen(),
),
),
GoRoute(
path: '/account/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AppBackground(
child: AccountPublisherEditScreen(
name: state.pathParameters['name']!,
),
), ),
), ),
], ],
), ),
GoRoute( GoRoute(
path: '/account/:name', path: '/realm',
name: 'accountProfilePage', name: 'realm',
pageBuilder: (context, state) => NoTransitionPage( pageBuilder: (context, state) => CustomTransitionPage(
child: UserScreen(name: state.pathParameters['name']!), transitionsBuilder: _fadeThroughTransition,
child: const RealmScreen(),
), ),
),
ShellRoute(
builder: (context, state, child) => AppPageScaffold(body: child),
routes: [ routes: [
GoRoute( GoRoute(
path: '/settings', path: '/:alias',
name: 'settings', name: 'realmDetail',
builder: (context, state) => const AppBackground( builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
child: SettingsScreen(), ),
GoRoute(
path: '/manage',
name: 'realmManage',
builder: (context, state) => RealmManageScreen(
editingRealmAlias: state.uri.queryParameters['editing'],
), ),
), ),
], ],
), ),
ShellRoute( GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
builder: (context, state, child) => AppPageScaffold(body: child), GoRoute(
routes: [ path: '/:hash',
GoRoute( name: 'newsDetail',
path: '/about', builder: (context, state) => NewsDetailScreen(
name: 'about', hash: state.pathParameters['hash']!,
builder: (context, state) => const AppBackground(
child: AboutScreen(),
),
), ),
], ),
]),
GoRoute(
path: '/album',
name: 'album',
builder: (context, state) => const AlbumScreen(),
),
GoRoute(
path: '/friend',
name: 'friend',
builder: (context, state) => const FriendScreen(),
),
GoRoute(
path: '/notification',
name: 'notification',
builder: (context, state) => const NotificationScreen(),
),
GoRoute(
path: '/auth/login',
name: 'authLogin',
builder: (context, state) => LoginScreen(),
),
GoRoute(
path: '/auth/register',
name: 'authRegister',
builder: (context, state) => RegisterScreen(),
),
GoRoute(
path: '/reports',
name: 'abuseReport',
builder: (context, state) => AbuseReportScreen(),
),
GoRoute(
path: '/settings',
name: 'settings',
builder: (context, state) => SettingsScreen(),
),
GoRoute(
path: '/about',
name: 'about',
builder: (context, state) => AboutScreen(),
), ),
]; ];

View File

@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import '../types/account.dart'; import '../types/account.dart';
@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAbuseReport').tr(),
),
body: Column( body: Column(
children: [ children: [
ListTile( ListTile(
@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
else else
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.only(top: 8),
itemCount: _reports.length, itemCount: _reports.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return ListTile( return ListTile(

View File

@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@ -12,6 +14,8 @@ import 'package:surface/providers/websocket.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart';
class AccountScreen extends StatelessWidget { class AccountScreen extends StatelessWidget {
const AccountScreen({super.key}); const AccountScreen({super.key});
@ -19,11 +23,51 @@ class AccountScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
final sn = context.read<SnNetworkProvider>();
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text("screenAccount").tr(), title: Text(
"screenAccount",
style: TextStyle(
color: Colors.white,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 5.0,
color: Color.fromARGB(255, 0, 0, 0),
),
],
),
).tr(),
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
? Stack(
fit: StackFit.expand,
children: [
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover),
Positioned(
top: 0,
left: 0,
right: 0,
height: 56 + MediaQuery.of(context).padding.top,
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
),
child: Container(
color: Colors.black.withOpacity(
clampDouble(10 * 0.1, 0, 0.5),
),
),
),
),
),
],
)
: null,
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Symbols.settings, fill: 1), icon: const Icon(Symbols.settings, fill: 1),
@ -82,16 +126,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
); );
}).padding(all: 20), }).padding(all: 20),
).padding(horizontal: 8, top: 16, bottom: 4), ).padding(horizontal: 8, top: 16, bottom: 4),
ListTile(
title: Text('accountProfileEdit').tr(),
subtitle: Text('accountProfileEditSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.contact_page),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountProfileEdit');
},
),
ListTile( ListTile(
title: Text('accountPublishers').tr(), title: Text('accountPublishers').tr(),
subtitle: Text('accountPublishersSubtitle').tr(), subtitle: Text('accountPublishersSubtitle').tr(),
@ -112,6 +146,36 @@ class _AuthorizedAccountScreen extends StatelessWidget {
GoRouter.of(context).pushNamed('abuseReport'); GoRouter.of(context).pushNamed('abuseReport');
}, },
), ),
ListTile(
title: Text('factorSettings').tr(),
subtitle: Text('factorSettingsSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.lock),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('factorSettings');
},
),
ListTile(
title: Text('accountWallet').tr(),
subtitle: Text('accountWalletSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.wallet),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountWallet');
},
),
ListTile(
title: Text('accountSettings').tr(),
subtitle: Text('accountSettingsSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.manage_accounts),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountSettings');
},
),
ListTile( ListTile(
title: Text('accountLogout').tr(), title: Text('accountLogout').tr(),
subtitle: Text('accountLogoutSubtitle').tr(), subtitle: Text('accountLogoutSubtitle').tr(),
@ -133,33 +197,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
await Hive.initFlutter(); await Hive.initFlutter();
}, },
), ),
ListTile(
title: Text('accountDeletion'.tr()),
subtitle: Text('accountDeletionActionDescription'.tr()),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.person_cancel),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context
.showConfirmDialog(
'accountDeletion'.tr(),
'accountDeletionDescription'.tr(),
)
.then((value) {
if (!value || !context.mounted) return;
final sn = context.read<SnNetworkProvider>();
sn.client.post('/cgi/id/users/me/deletion').then((value) {
if (context.mounted) {
context.showSnackbar('accountDeletionSubmitted'.tr());
}
}).catchError((err) {
if (context.mounted) {
context.showErrorDialog(err);
}
});
});
},
),
], ],
); );
} }

View 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);
}
});
});
},
),
],
),
),
);
}
}

View 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),
),
],
),
);
}
}

View File

@ -18,6 +18,7 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
class ProfileEditScreen extends StatefulWidget { class ProfileEditScreen extends StatefulWidget {
@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
onDateTimeChanged: (DateTime newDate) { onDateTimeChanged: (DateTime newDate) {
setState(() { setState(() {
_birthday = newDate; _birthday = newDate;
_birthdayController.text = _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
DateFormat(_kDateFormat).format(_birthday!);
}); });
}, },
), ),
@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
if (image == null) return; if (image == null) return;
if (!mounted) return; if (!mounted) return;
final ImageProvider imageProvider = final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); final aspectRatios =
final aspectRatios = place == 'banner' place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
? [CropAspectRatio(width: 16, height: 7)]
: [CropAspectRatio(width: 1, height: 1)];
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
? await showCupertinoImageCropper( ? await showCupertinoImageCropper(
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final rawBytes = final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
.buffer
.asUint8List();
try { try {
final attachment = await attach.directUploadOne( final attachment = await attach.directUploadOne(
@ -212,136 +207,141 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return SingleChildScrollView( return AppScaffold(
child: Column( appBar: AppBar(
crossAxisAlignment: CrossAxisAlignment.start, leading: const PageBackButton(),
children: [ title: Text('screenAccountProfileEdit').tr(),
LoadingIndicator(isActive: _isBusy), ),
const Gap(24), body: SingleChildScrollView(
Stack( child: Column(
clipBehavior: Clip.none, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Material( LoadingIndicator(isActive: _isBusy),
elevation: 0, const Gap(24),
child: InkWell( Stack(
child: ClipRRect( clipBehavior: Clip.none,
borderRadius: const BorderRadius.all(Radius.circular(8)), children: [
child: AspectRatio( Material(
aspectRatio: 16 / 9, elevation: 0,
child: Container( child: InkWell(
color: child: ClipRRect(
Theme.of(context).colorScheme.surfaceContainerHigh, borderRadius: const BorderRadius.all(Radius.circular(8)),
child: _banner != null child: AspectRatio(
? AutoResizeUniversalImage( aspectRatio: 16 / 9,
sn.getAttachmentUrl(_banner!), child: Container(
fit: BoxFit.cover, color: Theme.of(context).colorScheme.surfaceContainerHigh,
) child: _banner != null
: const SizedBox.shrink(), ? AutoResizeUniversalImage(
sn.getAttachmentUrl(_banner!),
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
), ),
), ),
),
onTap: () {
_updateImage('banner');
},
),
),
Positioned(
bottom: -28,
left: 16,
child: Material(
elevation: 2,
borderRadius: const BorderRadius.all(Radius.circular(40)),
child: InkWell(
child: AccountImage(content: _avatar, radius: 40),
onTap: () { onTap: () {
_updateImage('avatar'); _updateImage('banner');
}, },
), ),
), ),
), Positioned(
], bottom: -28,
).padding(horizontal: padding), left: 16,
const Gap(8 + 28), child: Material(
Column( elevation: 2,
children: [ borderRadius: const BorderRadius.all(Radius.circular(40)),
TextField( child: InkWell(
readOnly: true, child: AccountImage(content: _avatar, radius: 40),
controller: _usernameController, onTap: () {
decoration: InputDecoration( _updateImage('avatar');
border: const UnderlineInputBorder(), },
labelText: 'fieldUsername'.tr(),
helperText: 'fieldUsernameCannotEditHint'.tr(),
),
),
const Gap(4),
TextField(
controller: _nicknameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr(),
),
),
const Gap(4),
Row(
children: [
Flexible(
flex: 1,
child: TextField(
controller: _firstNameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldFirstName'.tr(),
),
), ),
), ),
const Gap(8), ),
Flexible( ],
flex: 1, ).padding(horizontal: padding),
child: TextField( const Gap(8 + 28),
controller: _lastNameController, Column(
decoration: InputDecoration( children: [
border: const UnderlineInputBorder(), TextField(
labelText: 'fieldLastName'.tr(), readOnly: true,
controller: _usernameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldUsername'.tr(),
helperText: 'fieldUsernameCannotEditHint'.tr(),
),
),
const Gap(4),
TextField(
controller: _nicknameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr(),
),
),
const Gap(4),
Row(
children: [
Flexible(
flex: 1,
child: TextField(
controller: _firstNameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldFirstName'.tr(),
),
), ),
), ),
const Gap(8),
Flexible(
flex: 1,
child: TextField(
controller: _lastNameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldLastName'.tr(),
),
),
),
],
),
const Gap(4),
TextField(
controller: _descriptionController,
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldDescription'.tr(),
), ),
],
),
const Gap(4),
TextField(
controller: _descriptionController,
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldDescription'.tr(),
), ),
), const Gap(4),
const Gap(4), TextField(
TextField( controller: _birthdayController,
controller: _birthdayController, readOnly: true,
readOnly: true, decoration: InputDecoration(
decoration: InputDecoration( border: const UnderlineInputBorder(),
border: const UnderlineInputBorder(), labelText: 'fieldBirthday'.tr(),
labelText: 'fieldBirthday'.tr(), ),
onTap: () => _selectBirthday(),
), ),
onTap: () => _selectBirthday(), ],
), ).padding(horizontal: padding + 8),
], const Gap(12),
).padding(horizontal: padding + 8), Row(
const Gap(12), mainAxisAlignment: MainAxisAlignment.end,
Row( children: [
mainAxisAlignment: MainAxisAlignment.end, ElevatedButton.icon(
children: [ onPressed: _isBusy ? null : _updateUserInfo,
ElevatedButton.icon( icon: const Icon(Symbols.save),
onPressed: _isBusy ? null : _updateUserInfo, label: Text('apply').tr(),
icon: const Icon(Symbols.save), ),
label: Text('apply').tr(), ],
), ).padding(horizontal: padding),
], ],
).padding(horizontal: padding), ),
],
), ),
); );
} }

View File

@ -241,6 +241,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return Scaffold( return Scaffold(
backgroundColor: Colors.transparent,
body: CustomScrollView( body: CustomScrollView(
controller: _scrollController, controller: _scrollController,
slivers: [ slivers: [
@ -594,7 +595,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
subtitle: Text('@${ele.name}'), subtitle: Text('@${ele.name}'),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).goNamed(
'postPublisher', 'postPublisher',
pathParameters: {'name': ele.name}, pathParameters: {'name': ele.name},
); );

View File

@ -18,6 +18,7 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
class AccountPublisherEditScreen extends StatefulWidget { class AccountPublisherEditScreen extends StatefulWidget {
@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return Scaffold( return AppScaffold(
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [

View File

@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class AccountPublisherNewScreen extends StatefulWidget { class AccountPublisherNewScreen extends StatefulWidget {
const AccountPublisherNewScreen({super.key}); const AccountPublisherNewScreen({super.key});
@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAccountPublisherNew').tr(),
),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [

View File

@ -10,6 +10,7 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class PublisherScreen extends StatefulWidget { class PublisherScreen extends StatefulWidget {
const PublisherScreen({super.key}); const PublisherScreen({super.key});
@ -32,8 +33,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
try { try {
final resp = await sn.client.get('/cgi/co/publishers/me'); final resp = await sn.client.get('/cgi/co/publishers/me');
final List<SnPublisher> out = List<SnPublisher>.from( final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
if (!mounted) return; if (!mounted) return;
@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAccountPublishers').tr(),
),
body: Column( body: Column(
children: [ children: [
ListTile( ListTile(
@ -62,9 +66,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.add_circle), leading: const Icon(Symbols.add_circle),
onTap: () { onTap: () {
GoRouter.of(context) GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
.pushNamed('accountPublisherNew')
.then((value) {
if (value == true) { if (value == true) {
_publishers.clear(); _publishers.clear();
_fetchPublishers(); _fetchPublishers();
@ -75,48 +77,52 @@ class _PublisherScreenState extends State<PublisherScreen> {
const Divider(height: 1), const Divider(height: 1),
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
Expanded( Expanded(
child: RefreshIndicator( child: MediaQuery.removePadding(
onRefresh: () { context: context,
_publishers.clear(); removeTop: true,
return _fetchPublishers(); child: RefreshIndicator(
}, onRefresh: () {
child: ListView.builder( _publishers.clear();
itemCount: _publishers.length, return _fetchPublishers();
itemBuilder: (context, idx) {
final publisher = _publishers[idx];
return ListTile(
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(content: publisher.avatar),
trailing: PopupMenuButton(
itemBuilder: (BuildContext context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.edit),
const Gap(16),
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'accountPublisherEdit',
pathParameters: {
'name': publisher.name,
},
).then((value) {
if (value == true) {
_publishers.clear();
_fetchPublishers();
}
});
},
),
],
),
);
}, },
child: ListView.builder(
itemCount: _publishers.length,
itemBuilder: (context, idx) {
final publisher = _publishers[idx];
return ListTile(
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(content: publisher.avatar),
trailing: PopupMenuButton(
itemBuilder: (BuildContext context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.edit),
const Gap(16),
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'accountPublisherEdit',
pathParameters: {
'name': publisher.name,
},
).then((value) {
if (value == true) {
_publishers.clear();
_fetchPublishers();
}
});
},
),
],
),
);
},
),
), ),
), ),
), ),

View File

@ -11,6 +11,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/attachment/attachment_zoom.dart'; import 'package:surface/widgets/attachment/attachment_zoom.dart';
import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class AlbumScreen extends StatefulWidget { class AlbumScreen extends StatefulWidget {
@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
body: CustomScrollView( body: CustomScrollView(
controller: _scrollController, controller: _scrollController,
slivers: [ slivers: [

View File

@ -7,17 +7,14 @@ import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/account/factor_settings.dart';
import 'package:surface/types/auth.dart'; import 'package:surface/types/auth.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import '../../providers/websocket.dart'; import '../../providers/websocket.dart';
final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
0: ('authFactorPassword'.tr(), Symbols.password, false),
1: ('authFactorEmail'.tr(), Symbols.email, true),
};
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});
@ -35,67 +32,73 @@ class _LoginScreenState extends State<LoginScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Theme( return AppScaffold(
data: Theme.of(context).copyWith(canvasColor: Colors.transparent), appBar: AppBar(
child: SingleChildScrollView( leading: const PageBackButton(),
child: PageTransitionSwitcher( title: Text('screenAuthLogin').tr(),
transitionBuilder: ( ),
Widget child, body: Theme(
Animation<double> primaryAnimation, data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
Animation<double> secondaryAnimation, child: SingleChildScrollView(
) { child: PageTransitionSwitcher(
return SharedAxisTransition( transitionBuilder: (
animation: primaryAnimation, Widget child,
secondaryAnimation: secondaryAnimation, Animation<double> primaryAnimation,
transitionType: SharedAxisTransitionType.horizontal, Animation<double> secondaryAnimation,
child: Container( ) {
constraints: BoxConstraints(maxWidth: 380), return SharedAxisTransition(
child: child, animation: primaryAnimation,
), secondaryAnimation: secondaryAnimation,
); transitionType: SharedAxisTransitionType.horizontal,
}, child: Container(
child: switch (_period % 3) { constraints: BoxConstraints(maxWidth: 380),
1 => _LoginPickerScreen( child: child,
key: const ValueKey(1), ),
ticket: _currentTicket, );
factors: _factors, },
onTicket: (p0) => setState(() { child: switch (_period % 3) {
_currentTicket = p0; 1 => _LoginPickerScreen(
}), key: const ValueKey(1),
onPickFactor: (p0) => setState(() { ticket: _currentTicket,
_factorPicked = p0; factors: _factors,
}), onTicket: (p0) => setState(() {
onNext: () => setState(() { _currentTicket = p0;
_period++; }),
}), onPickFactor: (p0) => setState(() {
), _factorPicked = p0;
2 => _LoginCheckScreen( }),
key: const ValueKey(2), onNext: () => setState(() {
ticket: _currentTicket, _period++;
factor: _factorPicked, }),
onTicket: (p0) => setState(() { ),
_currentTicket = p0; 2 => _LoginCheckScreen(
}), key: const ValueKey(2),
onNext: () => setState(() { ticket: _currentTicket,
_period = 1; factor: _factorPicked,
}), onTicket: (p0) => setState(() {
), _currentTicket = p0;
_ => _LoginLookupScreen( }),
key: const ValueKey(0), onNext: () => setState(() {
ticket: _currentTicket, _period = 1;
onTicket: (p0) => setState(() { }),
_currentTicket = p0; ),
}), _ => _LoginLookupScreen(
onFactor: (p0) => setState(() { key: const ValueKey(0),
_factors = p0; ticket: _currentTicket,
}), onTicket: (p0) => setState(() {
onNext: () => setState(() { _currentTicket = p0;
_period++; }),
}), onFactor: (p0) => setState(() {
), _factors = p0;
}, }),
).padding(all: 24), onNext: () => setState(() {
).center(), _period++;
}),
),
},
).padding(all: 24),
).center(),
),
); );
} }
} }
@ -205,7 +208,9 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
controller: _passwordController, controller: _passwordController,
obscureText: true, obscureText: true,
autofillHints: [ autofillHints: [
(_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode widget.factor!.type == 0
? AutofillHints.password
: AutofillHints.oneTimeCode
], ],
decoration: InputDecoration( decoration: InputDecoration(
isDense: true, isDense: true,
@ -260,7 +265,8 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
bool _isBusy = false; bool _isBusy = false;
int? _factorPicked; int? _factorPicked;
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()); Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
void _performGetFactorCode() async { void _performGetFactorCode() async {
if (_factorPicked == null) return; if (_factorPicked == null) return;
@ -321,11 +327,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
), ),
), ),
secondary: Icon( secondary: Icon(
_factorLabelMap[x.type]?.$2 ?? Symbols.question_mark, kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
), ),
title: Text( title: Text(
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(), kFactorTypes[x.type]?.$1 ?? 'unknown',
), ).tr(),
enabled: !widget.ticket!.factorTrail.contains(x.id), enabled: !widget.ticket!.factorTrail.contains(x.id),
value: _factorPicked == x.id, value: _factorPicked == x.id,
onChanged: (value) { onChanged: (value) {
@ -401,11 +407,14 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final lookupResp = await sn.client.get('/cgi/id/users/lookup?probe=$username'); final lookupResp =
await sn.client.get('/cgi/id/users/lookup?probe=$username');
await sn.client.post('/cgi/id/users/me/password-reset', data: { await sn.client.post('/cgi/id/users/me/password-reset', data: {
'user_id': lookupResp.data['id'], 'user_id': lookupResp.data['id'],
}); });
if (mounted) context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr()); if (mounted) {
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
}
} catch (err) { } catch (err) {
if (mounted) context.showErrorDialog(err); if (mounted) context.showErrorDialog(err);
} finally { } finally {
@ -430,7 +439,8 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
widget.onTicket(result.ticket); widget.onTicket(result.ticket);
// Pull factors // Pull factors
final factorResp = await sn.client.get('/cgi/id/auth/factors', queryParameters: { final factorResp =
await sn.client.get('/cgi/id/auth/factors', queryParameters: {
'ticketId': result.ticket!.id.toString(), 'ticketId': result.ticket!.id.toString(),
}); });
widget.onFactor( widget.onFactor(
@ -441,7 +451,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
widget.onNext(); widget.onNext();
} catch (err) { } catch (err) {
if(mounted) context.showErrorDialog(err); if (mounted) context.showErrorDialog(err);
return; return;
} finally { } finally {
setState(() => _isBusy = false); setState(() => _isBusy = false);
@ -524,7 +534,10 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
'termAcceptNextWithAgree'.tr(), 'termAcceptNextWithAgree'.tr(),
textAlign: TextAlign.end, textAlign: TextAlign.end,
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()), color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha((255 * 0.75).round()),
), ),
), ),
Material( Material(

View File

@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class RegisterScreen extends StatefulWidget { class RegisterScreen extends StatefulWidget {
@ -43,6 +44,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
'nick': nickname, 'nick': nickname,
'email': email, 'email': email,
'password': password, 'password': password,
'language': EasyLocalization.of(context)!.currentLocale.toString(),
}); });
if (!context.mounted) return; if (!context.mounted) return;
@ -54,175 +56,178 @@ class _RegisterScreenState extends State<RegisterScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StyledWidget(Container( return AppScaffold(
constraints: const BoxConstraints(maxWidth: 380), appBar: AppBar(
child: SingleChildScrollView( leading: const PageBackButton(),
child: Column( title: Text('screenAuthRegister').tr(),
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ body: StyledWidget(Container(
Align( constraints: const BoxConstraints(maxWidth: 380),
alignment: Alignment.centerLeft, child: SingleChildScrollView(
child: CircleAvatar( child: Column(
radius: 26, crossAxisAlignment: CrossAxisAlignment.start,
child: const Icon( children: [
Symbols.person_add, Align(
size: 28, alignment: Alignment.centerLeft,
), child: CircleAvatar(
).padding(bottom: 8), radius: 26,
), child: const Icon(
Text( Symbols.person_add,
'screenAuthRegister', size: 28,
style: const TextStyle( ),
fontSize: 28, ).padding(bottom: 8),
fontWeight: FontWeight.w900,
), ),
).tr().padding(left: 4, bottom: 16), Text(
Form( 'screenAuthRegister',
key: _formKey, style: const TextStyle(
autovalidateMode: AutovalidateMode.onUserInteraction, fontSize: 28,
child: Column( fontWeight: FontWeight.w900,
children: [ ),
TextFormField( ).tr().padding(left: 4, bottom: 16),
validator: (value) { Form(
if (value == null || value.length < 4 || value.length > 32) { key: _formKey,
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]); autovalidateMode: AutovalidateMode.onUserInteraction,
} child: Column(
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) { children: [
return 'fieldUsernameAlphanumOnly'.tr(); TextFormField(
} validator: (value) {
return null; if (value == null || value.length < 4 || value.length > 32) {
}, return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
autocorrect: false, }
enableSuggestions: false, if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
controller: _usernameController, return 'fieldUsernameAlphanumOnly'.tr();
autofillHints: const [AutofillHints.username], }
decoration: InputDecoration( return null;
isDense: true, },
border: const UnderlineInputBorder(), autocorrect: false,
labelText: 'fieldUsername'.tr(), enableSuggestions: false,
), controller: _usernameController,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), autofillHints: const [AutofillHints.username],
), decoration: InputDecoration(
const Gap(12), isDense: true,
TextFormField( border: const UnderlineInputBorder(),
validator: (value) { labelText: 'fieldUsername'.tr(),
if (value == null || value.length < 4 || value.length > 32) {
return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
}
return null;
},
autocorrect: false,
enableSuggestions: false,
controller: _nicknameController,
autofillHints: const [AutofillHints.nickname],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
if (!EmailValidator.validate(value)) {
return 'fieldEmailAddressMustBeValid'.tr();
}
return null;
},
autocorrect: false,
enableSuggestions: false,
controller: _emailController,
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldEmail'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
return null;
},
obscureText: true,
autocorrect: false,
enableSuggestions: false,
autofillHints: const [AutofillHints.password],
controller: _passwordController,
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldPassword'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
).padding(horizontal: 7),
),
const Gap(16),
Align(
alignment: Alignment.centerRight,
child: StyledWidget(
Container(
constraints: const BoxConstraints(maxWidth: 290),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'termAcceptNextWithAgree'.tr(),
textAlign: TextAlign.end,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha((255 * 0.75).round()),
),
), ),
Material( onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
color: Colors.transparent, ),
child: InkWell( const Gap(12),
child: Row( TextFormField(
mainAxisSize: MainAxisSize.min, validator: (value) {
children: [ if (value == null || value.length < 4 || value.length > 32) {
Text('termAcceptLink'.tr()), return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
const Gap(4), }
const Icon(Symbols.launch, size: 14), return null;
], },
autocorrect: false,
enableSuggestions: false,
controller: _nicknameController,
autofillHints: const [AutofillHints.nickname],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
if (!EmailValidator.validate(value)) {
return 'fieldEmailAddressMustBeValid'.tr();
}
return null;
},
autocorrect: false,
enableSuggestions: false,
controller: _emailController,
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldEmail'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
return null;
},
obscureText: true,
autocorrect: false,
enableSuggestions: false,
autofillHints: const [AutofillHints.password],
controller: _passwordController,
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldPassword'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
).padding(horizontal: 7),
),
const Gap(16),
Align(
alignment: Alignment.centerRight,
child: StyledWidget(
Container(
constraints: const BoxConstraints(maxWidth: 290),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'termAcceptNextWithAgree'.tr(),
textAlign: TextAlign.end,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
),
),
Material(
color: Colors.transparent,
child: InkWell(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('termAcceptLink'.tr()),
const Gap(4),
const Icon(Symbols.launch, size: 14),
],
),
onTap: () {
launchUrlString('https://solsynth.dev/terms');
},
), ),
onTap: () {
launchUrlString('https://solsynth.dev/terms');
},
), ),
), ],
),
),
).padding(horizontal: 16),
),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => _performAction(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('next').tr(),
const Icon(Symbols.chevron_right),
], ],
), ),
), ),
).padding(horizontal: 16),
),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => _performAction(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('next').tr(),
const Icon(Symbols.chevron_right),
],
),
), ),
), ],
], ),
), ),
), )).padding(all: 24).center(),
)).padding(all: 24).center(); );
} }
} }

View File

@ -13,6 +13,7 @@ import 'package:surface/widgets/account/account_select.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenChat').tr(), title: Text('screenChat').tr(),
@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenChat').tr(), title: Text('screenChat').tr(),
@ -195,22 +196,58 @@ class _ChatScreenState extends State<ChatScreen> {
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
Expanded( Expanded(
child: RefreshIndicator( child: MediaQuery.removePadding(
onRefresh: () => Future.sync(() => _refreshChannels()), context: context,
child: ListView.builder( removeTop: true,
itemCount: _channels?.length ?? 0, child: RefreshIndicator(
itemBuilder: (context, idx) { onRefresh: () => Future.sync(() => _refreshChannels()),
final channel = _channels![idx]; child: ListView.builder(
final lastMessage = _lastMessages?[channel.id]; itemCount: _channels?.length ?? 0,
itemBuilder: (context, idx) {
final channel = _channels![idx];
final lastMessage = _lastMessages?[channel.id];
if (channel.type == 1) { if (channel.type == 1) {
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere( final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
(ele) => ele?.accountId != ua.user?.id, (ele) => ele?.accountId != ua.user?.id,
orElse: () => null, orElse: () => null,
); );
return ListTile(
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
'channelDirectMessageDescription'.tr(args: [
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
),
onTap: () {
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
).then((value) {
if (mounted) _refreshChannels();
});
},
);
}
return ListTile( return ListTile(
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), title: Text(channel.name),
subtitle: lastMessage != null subtitle: lastMessage != null
? Text( ? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
@ -218,15 +255,14 @@ class _ChatScreenState extends State<ChatScreen> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
) )
: Text( : Text(
'channelDirectMessageDescription'.tr(args: [ channel.description,
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
]),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 16), contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage( leading: AccountImage(
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, content: null,
fallbackWidget: const Icon(Symbols.chat, size: 20),
), ),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
@ -240,39 +276,8 @@ class _ChatScreenState extends State<ChatScreen> {
}); });
}, },
); );
} },
),
return ListTile(
title: Text(channel.name),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
channel.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: null,
fallbackWidget: const Icon(Symbols.chat, size: 20),
),
onTap: () {
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
).then((value) {
if (value == true) _refreshChannels();
});
},
);
},
), ),
), ),
), ),

View File

@ -9,10 +9,12 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/chat_call.dart'; import 'package:surface/providers/chat_call.dart';
import 'package:surface/widgets/chat/call/call_controls.dart'; import 'package:surface/widgets/chat/call/call_controls.dart';
import 'package:surface/widgets/chat/call/call_participant.dart'; import 'package:surface/widgets/chat/call/call_participant.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class CallRoomScreen extends StatefulWidget { class CallRoomScreen extends StatefulWidget {
final String scope; final String scope;
final String alias; final String alias;
const CallRoomScreen({super.key, required this.scope, required this.alias}); const CallRoomScreen({super.key, required this.scope, required this.alias});
@override @override
@ -35,8 +37,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
return Stack( return Stack(
children: [ children: [
Container( Container(
color: color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
child: call.focusTrack != null child: call.focusTrack != null
? InteractiveParticipantWidget( ? InteractiveParticipantWidget(
isFixedAvatar: false, isFixedAvatar: false,
@ -71,8 +72,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
color: Theme.of(context).cardColor, color: Theme.of(context).cardColor,
participant: track, participant: track,
onTap: () { onTap: () {
if (track.participant.sid != if (track.participant.sid != call.focusTrack?.participant.sid) {
call.focusTrack?.participant.sid) {
call.setFocusTrack(track); call.setFocusTrack(track);
} }
}, },
@ -114,14 +114,10 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget( child: InteractiveParticipantWidget(
color: Theme.of(context) color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
.colorScheme
.surfaceContainerHigh
.withOpacity(0.75),
participant: track, participant: track,
onTap: () { onTap: () {
if (track.participant.sid != if (track.participant.sid != call.focusTrack?.participant.sid) {
call.focusTrack?.participant.sid) {
call.setFocusTrack(track); call.setFocusTrack(track);
} }
}, },
@ -152,157 +148,134 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
return ListenableBuilder( return ListenableBuilder(
listenable: call, listenable: call,
builder: (context, _) { builder: (context, _) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: RichText( title: RichText(
textAlign: TextAlign.center, textAlign: TextAlign.center,
text: TextSpan(children: [ text: TextSpan(children: [
TextSpan( TextSpan(
text: 'call'.tr(), text: 'call'.tr(),
style: Theme.of(context) style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
.textTheme
.titleLarge!
.copyWith(color: Colors.white),
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: call.lastDuration.toString(), text: call.lastDuration.toString(),
style: Theme.of(context) style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
.textTheme
.bodySmall!
.copyWith(color: Colors.white),
), ),
]), ]),
), ),
), ),
body: SafeArea( body: GestureDetector(
child: GestureDetector( behavior: HitTestBehavior.translucent,
behavior: HitTestBehavior.translucent, child: Column(
child: Column( children: [
children: [ SizedBox(
width: MediaQuery.of(context).size.width,
height: 64,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Builder(builder: (context) {
final call = context.read<ChatCallProvider>();
final connectionQuality =
call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown;
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
call.channel?.name ?? 'unknown'.tr(),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const Gap(6),
Text(call.lastDuration.toString())
],
),
Row(
children: [
Text(
{
livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(),
livekit.ConnectionState.connected: 'callStatusConnected'.tr(),
livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(),
livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(),
}[call.room.connectionState]!,
),
const Gap(6),
if (connectionQuality != livekit.ConnectionQuality.unknown)
Icon(
{
livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt,
livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
}[connectionQuality],
color: {
livekit.ConnectionQuality.excellent: Colors.green,
livekit.ConnectionQuality.good: Colors.orange,
livekit.ConnectionQuality.poor: Colors.red,
}[connectionQuality],
size: 16,
)
else
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
).padding(all: 3),
],
),
],
),
);
}),
Row(
children: [
IconButton(
icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view),
onPressed: () {
_switchLayout();
},
),
],
),
],
).padding(left: 20, right: 16),
),
Expanded(
child: Material(
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: Builder(
builder: (context) {
switch (_layoutMode) {
case 1:
return _buildGridLayout();
default:
return _buildListLayout();
}
},
),
),
),
if (call.room.localParticipant != null)
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
height: 64, child: ControlsWidget(
child: Row( call.room,
mainAxisAlignment: MainAxisAlignment.spaceBetween, call.room.localParticipant!,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Builder(builder: (context) {
final call = context.read<ChatCallProvider>();
final connectionQuality =
call.room.localParticipant?.connectionQuality ??
livekit.ConnectionQuality.unknown;
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
call.channel?.name ?? 'unknown'.tr(),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const Gap(6),
Text(call.lastDuration.toString())
],
),
Row(
children: [
Text(
{
livekit.ConnectionState.disconnected:
'callStatusDisconnected'.tr(),
livekit.ConnectionState.connected:
'callStatusConnected'.tr(),
livekit.ConnectionState.connecting:
'callStatusConnecting'.tr(),
livekit.ConnectionState.reconnecting:
'callStatusReconnecting'.tr(),
}[call.room.connectionState]!,
),
const Gap(6),
if (connectionQuality !=
livekit.ConnectionQuality.unknown)
Icon(
{
livekit.ConnectionQuality.excellent:
Icons.signal_cellular_alt,
livekit.ConnectionQuality.good:
Icons.signal_cellular_alt_2_bar,
livekit.ConnectionQuality.poor:
Icons.signal_cellular_alt_1_bar,
}[connectionQuality],
color: {
livekit.ConnectionQuality.excellent:
Colors.green,
livekit.ConnectionQuality.good:
Colors.orange,
livekit.ConnectionQuality.poor:
Colors.red,
}[connectionQuality],
size: 16,
)
else
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
).padding(all: 3),
],
),
],
),
);
}),
Row(
children: [
IconButton(
icon: _layoutMode == 0
? const Icon(Icons.view_list)
: const Icon(Icons.grid_view),
onPressed: () {
_switchLayout();
},
),
],
),
],
).padding(left: 20, right: 16),
),
Expanded(
child: Material(
color:
Theme.of(context).colorScheme.surfaceContainerLow,
child: Builder(
builder: (context) {
switch (_layoutMode) {
case 1:
return _buildGridLayout();
default:
return _buildListLayout();
}
},
),
), ),
), ),
if (call.room.localParticipant != null) ],
SizedBox(
width: MediaQuery.of(context).size.width,
child: ControlsWidget(
call.room,
call.room.localParticipant!,
),
),
],
),
onTap: () {},
), ),
onTap: () {},
), ),
); );
}); });

View File

@ -14,6 +14,7 @@ import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class ChannelDetailScreen extends StatefulWidget { class ChannelDetailScreen extends StatefulWidget {
@ -189,7 +190,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id; final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(), title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
), ),

View File

@ -12,6 +12,7 @@ import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatManageScreen extends StatefulWidget { class ChatManageScreen extends StatefulWidget {
@ -121,7 +122,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: widget.editingChannelAlias != null title: widget.editingChannelAlias != null
? Text('screenChatManage').tr() ? Text('screenChatManage').tr()

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -9,9 +10,12 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/chat_message_controller.dart'; import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart'; import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart'; import 'package:surface/providers/websocket.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/chat/call/call_prejoin.dart'; import 'package:surface/widgets/chat/call/call_prejoin.dart';
@ -20,16 +24,22 @@ import 'package:surface/widgets/chat/chat_message_input.dart';
import 'package:surface/widgets/chat/chat_typing_indicator.dart'; import 'package:surface/widgets/chat/chat_typing_indicator.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../../providers/user_directory.dart'; class ChatRoomScreenExtra {
import '../../providers/userinfo.dart'; final String? initialText;
final List<PostWriteMedia>? initialAttachments;
ChatRoomScreenExtra({this.initialText, this.initialAttachments});
}
class ChatRoomScreen extends StatefulWidget { class ChatRoomScreen extends StatefulWidget {
final String scope; final String scope;
final String alias; final String alias;
final ChatRoomScreenExtra? extra;
const ChatRoomScreen({super.key, required this.scope, required this.alias}); const ChatRoomScreen({super.key, required this.scope, required this.alias, this.extra});
@override @override
State<ChatRoomScreen> createState() => _ChatRoomScreenState(); State<ChatRoomScreen> createState() => _ChatRoomScreenState();
@ -176,8 +186,23 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
_messageController = ChatMessageController(context); _messageController = ChatMessageController(context);
_fetchChannel().then((_) async { _fetchChannel().then((_) async {
await _messageController.initialize(_channel!); await _messageController.initialize(_channel!);
await _messageController.checkUpdate();
await _fetchOngoingCall(); if (widget.extra != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
log('[ChatInput] Setting initial text and attachments...');
if (widget.extra!.initialText != null) {
_inputGlobalKey.currentState?.setInitialText(widget.extra!.initialText!);
}
if (widget.extra!.initialAttachments != null) {
_inputGlobalKey.currentState?.setInitialAttachments(widget.extra!.initialAttachments!);
}
});
}
await Future.wait([
_messageController.checkUpdate(),
_fetchOngoingCall(),
]);
}); });
final ws = context.read<WebSocketProvider>(); final ws = context.read<WebSocketProvider>();
@ -211,7 +236,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
final call = context.watch<ChatCallProvider>(); final call = context.watch<ChatCallProvider>();
final ud = context.read<UserDirectoryProvider>(); final ud = context.read<UserDirectoryProvider>();
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
_channel?.type == 1 _channel?.type == 1

View File

@ -1,3 +1,4 @@
import 'package:animations/animations.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
@ -6,11 +7,14 @@ import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/screens/post/post_detail.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -93,7 +97,9 @@ class _ExploreScreenState extends State<ExploreScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( final cfg = context.read<ConfigProvider>();
return AppScaffold(
floatingActionButtonLocation: ExpandableFab.location, floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab( floatingActionButton: ExpandableFab(
key: _fabKey, key: _fabKey,
@ -210,6 +216,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
), ),
), ),
), ),
const SliverGap(12),
SliverInfiniteList( SliverInfiniteList(
itemCount: _posts.length, itemCount: _posts.length,
isLoading: _isBusy, isLoading: _isBusy,
@ -217,27 +224,39 @@ class _ExploreScreenState extends State<ExploreScreen> {
hasReachedMax: _postCount != null && _posts.length >= _postCount!, hasReachedMax: _postCount != null && _posts.length >= _postCount!,
onFetchData: _fetchPosts, onFetchData: _fetchPosts,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return GestureDetector( return Center(
child: PostItem( child: OpenContainer(
data: _posts[idx], closedBuilder: (_, __) => Container(
maxWidth: 640, constraints: const BoxConstraints(maxWidth: 640),
onChanged: (data) { child: PostItem(
setState(() => _posts[idx] = data); data: _posts[idx],
}, maxWidth: 640,
onDeleted: () { onChanged: (data) {
_refreshPosts(); setState(() => _posts[idx] = data);
}, },
onDeleted: () {
_refreshPosts();
},
),
),
openBuilder: (_, close) => PostDetailScreen(
slug: _posts[idx].id.toString(),
preload: _posts[idx],
onBack: close,
),
openColor: Colors.transparent,
openElevation: 0,
transitionType: ContainerTransitionType.fade,
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
),
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
), ),
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {'slug': _posts[idx].id.toString()},
extra: _posts[idx],
);
},
); );
}, },
separatorBuilder: (context, index) => const Divider(height: 1), separatorBuilder: (_, __) => const Gap(8),
), ),
], ],
), ),

View File

@ -11,6 +11,7 @@ import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import '../providers/userinfo.dart'; import '../providers/userinfo.dart';
import '../widgets/unauthorized_hint.dart'; import '../widgets/unauthorized_hint.dart';
@ -180,7 +181,7 @@ class _FriendScreenState extends State<FriendScreen> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenFriend').tr(), title: Text('screenFriend').tr(),
@ -191,7 +192,7 @@ class _FriendScreenState extends State<FriendScreen> {
); );
} }
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenFriend').tr(), title: Text('screenFriend').tr(),
@ -233,52 +234,56 @@ class _FriendScreenState extends State<FriendScreen> {
if (_requests.isNotEmpty || _blocks.isNotEmpty) if (_requests.isNotEmpty || _blocks.isNotEmpty)
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: RefreshIndicator( child: MediaQuery.removePadding(
onRefresh: () => Future.wait([ context: context,
_fetchRelations(), removeTop: true,
_fetchRequests(), child: RefreshIndicator(
]), onRefresh: () => Future.wait([
child: ListView.builder( _fetchRelations(),
itemCount: _relations.length, _fetchRequests(),
itemBuilder: (context, index) { ]),
final relation = _relations[index]; child: ListView.builder(
final other = relation.related; itemCount: _relations.length,
return ListTile( itemBuilder: (context, index) {
contentPadding: const EdgeInsets.only(right: 24, left: 16), final relation = _relations[index];
leading: AccountImage(content: other?.avatar), final other = relation.related;
title: Text(other?.nick ?? 'unknown'), return ListTile(
subtitle: Text(other?.nick ?? 'unknown'), contentPadding: const EdgeInsets.only(right: 24, left: 16),
trailing: SizedBox( leading: AccountImage(content: other?.avatar),
height: 48, title: Text(other?.nick ?? 'unknown'),
width: 120, subtitle: Text(other?.nick ?? 'unknown'),
child: Column( trailing: SizedBox(
mainAxisSize: MainAxisSize.min, height: 48,
mainAxisAlignment: MainAxisAlignment.center, width: 120,
crossAxisAlignment: CrossAxisAlignment.end, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
Row( mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
InkWell( Row(
onTap: _isUpdating mainAxisAlignment: MainAxisAlignment.end,
? null children: [
: () => _changeRelation(relation, 2), InkWell(
child: Text('friendBlock').tr(), onTap: _isUpdating
), ? null
const Gap(8), : () => _changeRelation(relation, 2),
InkWell( child: Text('friendBlock').tr(),
onTap: _isUpdating ),
? null const Gap(8),
: () => _deleteRelation(relation), InkWell(
child: Text('friendDeleteAction').tr(), onTap: _isUpdating
), ? null
], : () => _deleteRelation(relation),
), child: Text('friendDeleteAction').tr(),
], ),
],
),
],
),
), ),
), );
); },
}, ),
), ),
), ),
), ),

View File

@ -9,6 +9,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:html/parser.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
@ -22,9 +23,11 @@ import 'package:surface/providers/special_day.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/widget.dart'; import 'package:surface/providers/widget.dart';
import 'package:surface/types/check_in.dart'; import 'package:surface/types/check_in.dart';
import 'package:surface/types/news.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
class HomeScreenDashEntry { class HomeScreenDashEntry {
@ -48,12 +51,12 @@ class HomeScreen extends StatefulWidget {
} }
class _HomeScreenState extends State<HomeScreen> { class _HomeScreenState extends State<HomeScreen> {
static const List<HomeScreenDashEntry> kCards = [ late final List<HomeScreenDashEntry> kCards = [
HomeScreenDashEntry( HomeScreenDashEntry(
name: 'dashEntryRecommendation', name: 'dashEntryRecommendation',
cols: 2,
rows: 2,
child: _HomeDashRecommendationPostWidget(), child: _HomeDashRecommendationPostWidget(),
rows: 2,
cols: 2,
), ),
HomeScreenDashEntry( HomeScreenDashEntry(
name: 'dashEntryCheckIn', name: 'dashEntryCheckIn',
@ -63,11 +66,16 @@ class _HomeScreenState extends State<HomeScreen> {
name: 'dashEntryNotification', name: 'dashEntryNotification',
child: _HomeDashNotificationWidget(), child: _HomeDashNotificationWidget(),
), ),
HomeScreenDashEntry(
name: 'dashEntryTodayNews',
child: _HomeDashTodayNews(),
cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2,
),
]; ];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text("screenHome").tr(), title: Text("screenHome").tr(),
@ -229,6 +237,106 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
} }
} }
class _HomeDashTodayNews extends StatefulWidget {
const _HomeDashTodayNews();
@override
State<_HomeDashTodayNews> createState() => _HomeDashTodayNewsState();
}
class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
SnNewsArticle? _article;
Future<void> _fetchArticle() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/re/news/today');
_article = SnNewsArticle.fromJson(resp.data['data']);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
rethrow;
} finally {
setState(() {});
}
}
@override
initState() {
super.initState();
_fetchArticle();
}
@override
Widget build(BuildContext context) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Symbols.newspaper),
const Gap(8),
Text(
'newsToday',
style: Theme.of(context).textTheme.titleLarge,
).tr()
],
).padding(horizontal: 18, top: 12, bottom: 8),
if (_article != null)
Expanded(
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
Text(
_article!.title,
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18),
maxLines: MediaQuery.of(context).size.width >= 640 ? 2 : 1,
overflow: TextOverflow.ellipsis,
),
Text(
parse(_article!.description).children.map((e) => e.text.trim()).join(),
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
Builder(builder: (context) {
final date = _article!.publishedAt ?? _article!.createdAt;
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 2,
children: [
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).opacity(0.75);
}),
],
).padding(horizontal: 16),
onTap: () {
GoRouter.of(context).pushNamed(
'newsDetail',
pathParameters: {'hash': _article!.hash},
);
},
),
)
else
Expanded(
child: Center(
child: CircularProgressIndicator(),
),
)
],
),
);
}
}
class _HomeDashCheckInWidget extends StatefulWidget { class _HomeDashCheckInWidget extends StatefulWidget {
const _HomeDashCheckInWidget(); const _HomeDashCheckInWidget();
@ -387,6 +495,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
Text( Text(
'dailyCheckInNone', 'dailyCheckInNone',
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
maxLines: 2,
overflow: TextOverflow.ellipsis,
).tr(), ).tr(),
], ],
) )
@ -404,6 +514,11 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
'+${_todayRecord!.resultExperience} EXP', '+${_todayRecord!.resultExperience} EXP',
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
), ),
if (_todayRecord!.resultCoin >= 0)
Text(
'+${_todayRecord!.resultCoin} ${'walletCurrencyShort'.tr()}',
style: Theme.of(context).textTheme.bodyLarge,
)
], ],
), ),
), ),

View File

@ -0,0 +1,241 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/news.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:url_launcher/url_launcher_string.dart';
class NewsDetailScreen extends StatefulWidget {
final String hash;
const NewsDetailScreen({super.key, required this.hash});
@override
State<NewsDetailScreen> createState() => _NewsDetailScreenState();
}
class _NewsDetailScreenState extends State<NewsDetailScreen> {
SnNewsArticle? _article;
dom.Document? _articleFragment;
Future<void> _fetchArticle() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
_article = SnNewsArticle.fromJson(resp.data);
_articleFragment = parse(_article!.content);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err).then((_) {
if (!mounted) return;
Navigator.pop(context);
});
} finally {
setState(() {});
}
}
List<Widget> _parseHtmlToWidgets(Iterable<dom.Element>? elements) {
if (elements == null) return [];
final List<Widget> widgets = [];
for (final node in elements) {
switch (node.localName) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
widgets.add(Text(node.text.trim(), style: Theme.of(context).textTheme.titleMedium));
break;
case 'p':
if (node.text.trim().isEmpty) continue;
widgets.add(
Text.rich(
TextSpan(
text: node.text.trim(),
children: [
for (final child in node.children)
switch (child.localName) {
'a' => TextSpan(
text: child.text.trim(),
style: const TextStyle(decoration: TextDecoration.underline),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrlString(child.attributes['href']!);
},
),
_ => TextSpan(text: child.text.trim()),
},
],
),
style: Theme.of(context).textTheme.bodyLarge,
),
);
break;
case 'a':
// drop single link
break;
case 'div':
// ignore div text, normally it is not meaningful
widgets.addAll(_parseHtmlToWidgets(node.children));
break;
case 'hr':
widgets.add(const Divider());
break;
case 'img':
var src = node.attributes['src'];
if (src == null) break;
final width = double.tryParse(node.attributes['width'] ?? 'null');
final height = double.tryParse(node.attributes['height'] ?? 'null');
final ratio = width != null && height != null ? width / height : 1.0;
if (src.startsWith('//')) {
src = 'https:$src';
} else if (!src.startsWith('http')) {
final baseUri = Uri.parse(_article!.url);
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
src = '$baseUrl/$src';
}
widgets.add(
AspectRatio(
aspectRatio: ratio,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
height: height ?? double.infinity,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(8)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: AutoResizeUniversalImage(
src,
fit: width != null && height != null ? BoxFit.cover : BoxFit.contain,
),
),
),
),
),
);
break;
default:
widgets.addAll(_parseHtmlToWidgets(node.children));
break;
}
}
return widgets;
}
@override
void initState() {
super.initState();
_fetchArticle();
}
bool _isReadingFromReader = true;
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text(_article?.title ?? 'loading'.tr()),
),
body: Column(
children: [
MaterialBanner(
dividerColor: Colors.transparent,
leading: const Icon(Icons.info),
content: Text(_isReadingFromReader ? 'newsReadingFromReader'.tr() : 'newsReadingFromOriginal'.tr()),
actions: [
TextButton(
child: Text('newsReadingProviderSwap').tr(),
onPressed: () {
setState(() => _isReadingFromReader = !_isReadingFromReader);
},
),
],
),
if (_articleFragment != null && _isReadingFromReader)
Expanded(
child: Container(
constraints: BoxConstraints(maxWidth: 640),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
Builder(builder: (context) {
final htmlDescription = parse(_article!.description);
return Text(
htmlDescription.children.map((ele) => ele.text.trim()).join(),
style: Theme.of(context).textTheme.bodyMedium,
);
}),
Builder(builder: (context) {
final date = _article!.publishedAt ?? _article!.createdAt;
return Row(
spacing: 2,
children: [
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).opacity(0.75);
}),
Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75),
const Divider(),
..._parseHtmlToWidgets(_articleFragment!.children),
const Divider(),
InkWell(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Reference from original website',
style: TextStyle(decoration: TextDecoration.underline),
),
const Gap(4),
Icon(Icons.launch, size: 16),
],
).opacity(0.85),
onTap: () {
launchUrlString(_article!.url);
},
),
Gap(MediaQuery.of(context).padding.bottom),
],
).padding(horizontal: 12, vertical: 16),
),
).center(),
)
else if (_article != null)
Expanded(
child: InAppWebView(
key: GlobalKey(),
initialUrlRequest: URLRequest(url: WebUri(_article!.url)),
),
),
],
),
);
}
}

View File

@ -0,0 +1,239 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:html/parser.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/news.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class NewsScreen extends StatefulWidget {
const NewsScreen({super.key});
@override
State<NewsScreen> createState() => _NewsScreenState();
}
class _NewsScreenState extends State<NewsScreen> {
List<SnNewsSource>? _sources;
@override
initState() {
super.initState();
_fetchSources();
}
Future<void> _fetchSources() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/re/well-known/sources');
_sources = List<SnNewsSource>.from(
resp.data?.map((e) => SnNewsSource.fromJson(e)) ?? [],
);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
if (_sources == null) {
return AppScaffold(
appBar: AppBar(
leading: AutoAppBarLeading(),
title: Text('screenNews').tr(),
),
body: Center(
child: CircularProgressIndicator(),
),
);
}
return DefaultTabController(
length: _sources!.length + 1,
child: AppScaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
leading: AutoAppBarLeading(),
title: Text('screenNews').tr(),
floating: true,
snap: true,
bottom: TabBar(
isScrollable: true,
tabs: [
Tab(child: Text('newsAllSources'.tr()).textColor(Theme.of(context).appBarTheme.foregroundColor)),
for (final source in _sources!)
Tab(
child: Text(source.label).textColor(Theme.of(context).appBarTheme.foregroundColor),
),
],
),
),
),
];
},
body: TabBarView(
children: [
_NewsArticleListWidget(allSources: _sources!),
for (final source in _sources!)
_NewsArticleListWidget(
source: source.id,
allSources: _sources!,
),
],
),
),
),
);
}
}
class _NewsArticleListWidget extends StatefulWidget {
final String? source;
final List<SnNewsSource> allSources;
const _NewsArticleListWidget({this.source, required this.allSources});
@override
State<_NewsArticleListWidget> createState() => _NewsArticleListWidgetState();
}
class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
bool _isBusy = false;
int? _totalCount;
final List<SnNewsArticle> _articles = List.empty(growable: true);
Future<void> _fetchArticles() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/re/news', queryParameters: {
'take': 10,
'offset': _articles.length,
if (widget.source != null) 'source': widget.source,
});
_totalCount = resp.data['count'];
_articles.addAll(List<SnNewsArticle>.from(
resp.data['data']?.map((e) => SnNewsArticle.fromJson(e)) ?? [],
));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchArticles();
}
@override
Widget build(BuildContext context) {
return MediaQuery.removePadding(
context: context,
removeTop: true,
child: Center(
child: Container(
constraints: BoxConstraints(maxWidth: 640),
child: RefreshIndicator(
onRefresh: _fetchArticles,
child: InfiniteList(
isLoading: _isBusy,
itemCount: _articles.length,
hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
onFetchData: () {
_fetchArticles();
},
itemBuilder: (context, index) {
final article = _articles[index];
final baseUri = Uri.parse(article.url);
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
final htmlDescription = parse(article.description);
final date = article.publishedAt ?? article.createdAt;
return Card(
child: InkWell(
radius: 8,
onTap: () {
GoRouter.of(context).pushNamed(
'newsDetail',
pathParameters: {'hash': article.hash},
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
ClipRRect(
borderRadius: BorderRadius.only(
topRight: Radius.circular(8),
topLeft: Radius.circular(8),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: AutoResizeUniversalImage(
article.thumbnail.startsWith('http')
? article.thumbnail
: '$baseUrl/${article.thumbnail}',
),
),
),
),
const Gap(16),
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
const Gap(8),
Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
.textStyle(Theme.of(context).textTheme.bodyMedium!)
.padding(horizontal: 16),
const Gap(8),
Row(
spacing: 2,
children: [
Text(widget.allSources.where((x) => x.id == article.source).first.label)
.textStyle(Theme.of(context).textTheme.bodySmall!),
],
).opacity(0.75).padding(horizontal: 16),
Row(
spacing: 2,
children: [
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).opacity(0.75).padding(horizontal: 16),
const Gap(16),
],
),
),
);
},
),
),
),
),
);
}
}

View File

@ -14,12 +14,23 @@ import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/markdown_content.dart'; import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../providers/userinfo.dart'; import '../providers/userinfo.dart';
import '../widgets/unauthorized_hint.dart'; import '../widgets/unauthorized_hint.dart';
const Map<String, IconData> kNotificationTopicIcons = {
'general': Symbols.notifications,
'passport.security.alert': Symbols.gpp_maybe,
'passport.security.otp': Symbols.password,
'interactive.subscription': Symbols.subscriptions,
'interactive.feedback': Symbols.add_reaction,
'messaging.callStart': Symbols.call_received,
'wallet.transaction.new': Symbols.receipt,
};
class NotificationScreen extends StatefulWidget { class NotificationScreen extends StatefulWidget {
const NotificationScreen({super.key}); const NotificationScreen({super.key});
@ -35,13 +46,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
final List<SnNotification> _notifications = List.empty(growable: true); final List<SnNotification> _notifications = List.empty(growable: true);
int? _totalCount; int? _totalCount;
static const Map<String, IconData> kNotificationTopicIcons = {
'passport.security.alert': Symbols.gpp_maybe,
'interactive.subscription': Symbols.subscriptions,
'interactive.feedback': Symbols.add_reaction,
'messaging.callStart': Symbols.call_received,
};
Future<void> _fetchNotifications() async { Future<void> _fetchNotifications() async {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) return; if (!ua.isAuthorized) return;
@ -137,7 +141,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenNotification').tr(), title: Text('screenNotification').tr(),
@ -148,7 +152,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
); );
} }
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenNotification').tr(), title: Text('screenNotification').tr(),
@ -206,10 +210,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
), ),
if (nty.subtitle != null) const Gap(4), if (nty.subtitle != null) const Gap(4),
MarkdownTextContent( SelectionArea(
content: nty.body, child: MarkdownTextContent(
isAutoWarp: true, content: nty.body,
isSelectable: true, isAutoWarp: true,
),
), ),
if ([ if ([
'interactive.feedback', 'interactive.feedback',

View File

@ -13,6 +13,8 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_comment_list.dart'; import 'package:surface/widgets/post/post_comment_list.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/post/post_mini_editor.dart'; import 'package:surface/widgets/post/post_mini_editor.dart';
@ -20,12 +22,9 @@ import 'package:surface/widgets/post/post_mini_editor.dart';
class PostDetailScreen extends StatefulWidget { class PostDetailScreen extends StatefulWidget {
final String slug; final String slug;
final SnPost? preload; final SnPost? preload;
final Function? onBack;
const PostDetailScreen({ const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
super.key,
required this.slug,
this.preload,
});
@override @override
State<PostDetailScreen> createState() => _PostDetailScreenState(); State<PostDetailScreen> createState() => _PostDetailScreenState();
@ -67,123 +66,129 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return Scaffold( return AppBackground(
appBar: AppBar( isRoot: widget.onBack != null,
leading: BackButton( child: AppScaffold(
onPressed: () { appBar: AppBar(
if (GoRouter.of(context).canPop()) { leading: BackButton(
GoRouter.of(context).pop(context); onPressed: () {
return; if (widget.onBack != null) {
} widget.onBack!.call();
GoRouter.of(context).replaceNamed('explore'); }
}, if (GoRouter.of(context).canPop()) {
), GoRouter.of(context).pop(context);
title: _data?.body['title'] != null return;
? RichText( }
textAlign: TextAlign.center, GoRouter.of(context).replaceNamed('explore');
text: TextSpan(children: [ },
TextSpan(
text: _data?.body['title'] ?? 'postNoun'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
const TextSpan(text: '\n'),
TextSpan(
text: 'postDetail'.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
]),
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
: Text('postDetail').tr(),
),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: LoadingIndicator(isActive: _isBusy),
), ),
if (_data != null) title: _data?.body['title'] != null
SliverToBoxAdapter( ? RichText(
child: PostItem( textAlign: TextAlign.center,
data: _data!, text: TextSpan(children: [
maxWidth: 640, TextSpan(
showComments: false, text: _data?.body['title'] ?? 'postNoun'.tr(),
showFullPost: true, style: Theme.of(context).textTheme.titleLarge!.copyWith(
onChanged: (data) { color: Theme.of(context).appBarTheme.foregroundColor!,
setState(() => _data = data);
},
onDeleted: () {
Navigator.pop(context);
},
),
),
const SliverToBoxAdapter(child: Divider(height: 1)),
if (_data != null)
SliverToBoxAdapter(
child: Container(
constraints: const BoxConstraints(maxWidth: 640),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.comment, size: 24),
const Gap(16),
Text('postCommentsDetailed')
.plural(_data!.metric.replyCount)
.textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, vertical: 12).center(),
),
),
if (_data != null && ua.isAuthorized)
SliverToBoxAdapter(
child: Container(
height: 240,
constraints: const BoxConstraints(maxWidth: 640),
margin:
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero,
decoration: BoxDecoration(
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? const BorderRadius.all(Radius.circular(8))
: BorderRadius.zero,
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? Border.all(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
)
: Border.symmetric(
horizontal: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
), ),
), ),
), const TextSpan(text: '\n'),
child: PostMiniEditor( TextSpan(
postReplyId: _data!.id, text: 'postDetail'.tr(),
onPost: () { style: Theme.of(context).textTheme.bodySmall!.copyWith(
setState(() { color: Theme.of(context).appBarTheme.foregroundColor!,
_data = _data!.copyWith( ),
metric: _data!.metric.copyWith( ),
replyCount: _data!.metric.replyCount + 1, ]),
), maxLines: 2,
); overflow: TextOverflow.ellipsis,
}); )
_childListKey.currentState!.refresh(); : Text('postDetail').tr(),
),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: LoadingIndicator(isActive: _isBusy),
),
if (_data != null)
SliverToBoxAdapter(
child: PostItem(
data: _data!,
maxWidth: 640,
showComments: false,
showFullPost: true,
onChanged: (data) {
setState(() => _data = data);
},
onDeleted: () {
Navigator.pop(context);
}, },
), ),
).center(), ),
), const SliverToBoxAdapter(child: Divider(height: 1)),
if (_data != null) if (_data != null)
PostCommentSliverList( SliverToBoxAdapter(
key: _childListKey, child: Container(
parentPostId: _data!.id, constraints: const BoxConstraints(maxWidth: 640),
maxWidth: 640, child: Row(
), crossAxisAlignment: CrossAxisAlignment.center,
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)), children: [
], const Icon(Symbols.comment, size: 24),
const Gap(16),
Text('postCommentsDetailed')
.plural(_data!.metric.replyCount)
.textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, vertical: 12).center(),
),
),
if (_data != null && ua.isAuthorized)
SliverToBoxAdapter(
child: Container(
height: 240,
constraints: const BoxConstraints(maxWidth: 640),
margin:
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero,
decoration: BoxDecoration(
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? const BorderRadius.all(Radius.circular(8))
: BorderRadius.zero,
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? Border.all(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
)
: Border.symmetric(
horizontal: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
),
),
child: PostMiniEditor(
postReplyId: _data!.id,
onPost: () {
setState(() {
_data = _data!.copyWith(
metric: _data!.metric.copyWith(
replyCount: _data!.metric.replyCount + 1,
),
);
});
_childListKey.currentState!.refresh();
},
),
).center(),
),
if (_data != null)
PostCommentSliverList(
key: _childListKey,
parentPostId: _data!.id,
maxWidth: 640,
),
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
],
),
), ),
); );
} }

View File

@ -13,19 +13,20 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/post/post_media_pending_list.dart'; import 'package:surface/widgets/post/post_media_pending_list.dart';
import 'package:surface/widgets/post/post_meta_editor.dart'; import 'package:surface/widgets/post/post_meta_editor.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class PostEditorExtraProps { class PostEditorExtra {
final String? text; final String? text;
final String? title; final String? title;
final String? description; final String? description;
final List<PostWriteMedia>? attachments; final List<PostWriteMedia>? attachments;
const PostEditorExtraProps({ const PostEditorExtra({
this.text, this.text,
this.title, this.title,
this.description, this.description,
@ -38,7 +39,7 @@ class PostEditorScreen extends StatefulWidget {
final int? postEditId; final int? postEditId;
final int? postReplyId; final int? postReplyId;
final int? postRepostId; final int? postRepostId;
final PostEditorExtraProps? extraProps; final PostEditorExtra? extraProps;
const PostEditorScreen({ const PostEditorScreen({
super.key, super.key,
@ -54,7 +55,9 @@ class PostEditorScreen extends StatefulWidget {
} }
class _PostEditorScreenState extends State<PostEditorScreen> { class _PostEditorScreenState extends State<PostEditorScreen> {
final PostWriteController _writeController = PostWriteController(); late final PostWriteController _writeController = PostWriteController(
doLoadFromTemporary: widget.postEditId == null,
);
bool _isFetching = false; bool _isFetching = false;
@ -126,7 +129,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
return ListenableBuilder( return ListenableBuilder(
listenable: _writeController, listenable: _writeController,
builder: (context, _) { builder: (context, _) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: BackButton( leading: BackButton(
onPressed: () { onPressed: () {
@ -301,19 +304,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
], ],
), ),
// Content Input Area // Content Input Area
TextField( Container(
controller: _writeController.contentController, constraints: const BoxConstraints(maxWidth: 640),
maxLines: null, child: TextField(
decoration: InputDecoration( controller: _writeController.contentController,
hintText: 'fieldPostContent'.tr(), maxLines: null,
hintStyle: TextStyle(fontSize: 14), decoration: InputDecoration(
isCollapsed: true, hintText: 'fieldPostContent'.tr(),
contentPadding: const EdgeInsets.symmetric( hintStyle: TextStyle(fontSize: 14),
horizontal: 16, isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
),
border: InputBorder.none,
), ),
border: InputBorder.none, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
] ]
.expandIndexed( .expandIndexed(
@ -364,6 +370,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
LoadingIndicator(isActive: _isLoading),
if (_writeController.isBusy && _writeController.progress != null)
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _writeController.progress),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
)
else if (_writeController.isBusy)
const LinearProgressIndicator(value: null, minHeight: 2),
Container( Container(
child: _writeController.temporaryRestored child: _writeController.temporaryRestored
? Container( ? Container(
@ -394,15 +409,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
) )
.height(_writeController.temporaryRestored ? 32 : 0, animate: true) .height(_writeController.temporaryRestored ? 32 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn), .animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
LoadingIndicator(isActive: _isLoading),
if (_writeController.isBusy && _writeController.progress != null)
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _writeController.progress),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
)
else if (_writeController.isBusy)
const LinearProgressIndicator(value: null, minHeight: 2),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

View File

@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/post/post_tags_field.dart'; import 'package:surface/widgets/post/post_tags_field.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -119,7 +120,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
), ),
]; ];
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text('screenPostSearch').tr(), title: Text('screenPostSearch').tr(),
actions: [ actions: [

View File

@ -17,6 +17,7 @@ import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -274,7 +275,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return Scaffold( return AppScaffold(
body: NestedScrollView( body: NestedScrollView(
controller: _scrollController, controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {

View File

@ -12,6 +12,7 @@ import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
@ -83,7 +84,7 @@ class _RealmScreenState extends State<RealmScreen> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenRealm').tr(), title: Text('screenRealm').tr(),
@ -94,7 +95,7 @@ class _RealmScreenState extends State<RealmScreen> {
); );
} }
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenRealm').tr(), title: Text('screenRealm').tr(),
@ -118,113 +119,61 @@ class _RealmScreenState extends State<RealmScreen> {
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
Expanded( Expanded(
child: RefreshIndicator( child: MediaQuery.removePadding(
onRefresh: _fetchRealms, context: context,
child: ListView.builder( removeTop: true,
itemCount: _realms?.length ?? 0, child: RefreshIndicator(
itemBuilder: (context, idx) { onRefresh: _fetchRealms,
final realm = _realms![idx]; child: ListView.builder(
if (_isCompactView) { itemCount: _realms?.length ?? 0,
return ListTile( itemBuilder: (context, idx) {
contentPadding: const EdgeInsets.symmetric(horizontal: 16), final realm = _realms![idx];
leading: AccountImage( if (_isCompactView) {
content: realm.avatar, return ListTile(
fallbackWidget: const Icon(Symbols.group, size: 20), contentPadding: const EdgeInsets.symmetric(horizontal: 16),
), leading: AccountImage(
title: Text(realm.name), content: realm.avatar,
subtitle: Text( fallbackWidget: const Icon(Symbols.group, size: 20),
realm.description, ),
maxLines: 1, title: Text(realm.name),
overflow: TextOverflow.ellipsis, subtitle: Text(
), realm.description,
trailing: PopupMenuButton( maxLines: 1,
itemBuilder: (BuildContext context) => [ overflow: TextOverflow.ellipsis,
PopupMenuItem( ),
child: Row( trailing: PopupMenuButton(
children: [ itemBuilder: (BuildContext context) => [
const Icon(Symbols.edit), PopupMenuItem(
const Gap(16), child: Row(
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': realm.alias},
).then((value) {
if (value != null) {
_fetchRealms();
}
});
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deleteRealm(realm);
},
),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': realm.alias},
);
},
);
}
return Container(
constraints: BoxConstraints(maxWidth: 640),
child: Card(
margin: const EdgeInsets.all(12),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [ children: [
Container( const Icon(Symbols.edit),
color: Theme.of(context).colorScheme.surfaceContainer, const Gap(16),
child: (realm.banner?.isEmpty ?? true) Text('edit').tr(),
? const SizedBox.shrink()
: AutoResizeUniversalImage(
sn.getAttachmentUrl(realm.banner!),
fit: BoxFit.cover,
),
),
Positioned(
bottom: -30,
left: 18,
child: AccountImage(
content: realm.avatar,
radius: 24,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
),
], ],
), ),
onTap: () {
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': realm.alias},
).then((value) {
if (value != null) {
_fetchRealms();
}
});
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deleteRealm(realm);
},
), ),
const Gap(20 + 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
], ],
), ),
onTap: () { onTap: () {
@ -233,10 +182,69 @@ class _RealmScreenState extends State<RealmScreen> {
pathParameters: {'alias': realm.alias}, pathParameters: {'alias': realm.alias},
); );
}, },
);
}
return Container(
constraints: BoxConstraints(maxWidth: 640),
child: Card(
margin: const EdgeInsets.all(12),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: (realm.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoResizeUniversalImage(
sn.getAttachmentUrl(realm.banner!),
fit: BoxFit.cover,
),
),
),
Positioned(
bottom: -30,
left: 18,
child: AccountImage(
content: realm.avatar,
radius: 24,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
),
],
),
),
const Gap(20 + 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': realm.alias},
);
},
),
), ),
), ).center();
).center(); },
}, ),
), ),
), ),
), ),

View File

@ -18,6 +18,7 @@ import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -179,7 +180,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: widget.editingRealmAlias != null title: widget.editingRealmAlias != null
? Text('screenRealmManage').tr() ? Text('screenRealmManage').tr()

View File

@ -8,13 +8,13 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../../types/post.dart';
class RealmDetailScreen extends StatefulWidget { class RealmDetailScreen extends StatefulWidget {
final String alias; final String alias;
@ -70,19 +70,11 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DefaultTabController( return DefaultTabController(
length: 3, length: 3,
child: Scaffold( child: AppScaffold(
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
// These are the slivers that show up in the "outer" scroll view.
return <Widget>[ return <Widget>[
SliverOverlapAbsorber( SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar( sliver: SliverAppBar(
title: Text(_realm?.name ?? 'loading'.tr()), title: Text(_realm?.name ?? 'loading'.tr()),
@ -428,7 +420,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
return Column( return Column(
children: [ children: [
const Gap(16), const Gap(8),
ListTile( ListTile(
leading: const Icon(Symbols.edit), leading: const Icon(Symbols.edit),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),

View File

@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/theme.dart'; import 'package:surface/providers/theme.dart';
import 'package:surface/theme.dart'; import 'package:surface/theme.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
const Map<String, Color> kColorSchemes = { const Map<String, Color> kColorSchemes = {
'colorSchemeIndigo': Colors.indigo, 'colorSchemeIndigo': Colors.indigo,
@ -67,7 +68,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return Scaffold( return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenSettings').tr(),
),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
spacing: 16, spacing: 16,
@ -77,6 +82,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('settingsAppearance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), Text('settingsAppearance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
ListTile(
title: Text('settingsDisplayLanguage').tr(),
subtitle: Text('settingsDisplayLanguageDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.translate),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<Locale?>(
isExpanded: true,
items: [
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
return DropdownMenuItem<Locale?>(
value: ele,
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
);
}),
DropdownMenuItem<Locale?>(
value: null,
child: Text('settingsDisplayLanguageSystem').tr().fontSize(14),
),
],
value: EasyLocalization.of(context)!.currentLocale,
onChanged: (Locale? value) {
if (value != null) {
EasyLocalization.of(context)!.setLocale(value);
} else {
EasyLocalization.of(context)!.resetLocale();
}
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 5,
),
height: 40,
width: 160,
),
menuItemStyleData: const MenuItemStyleData(
height: 40,
),
),
),
),
if (!kIsWeb) if (!kIsWeb)
ListTile( ListTile(
title: Text('settingsBackgroundImage').tr(), title: Text('settingsBackgroundImage').tr(),
@ -120,7 +167,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
subtitle: Text('settingsThemeMaterial3Description').tr(), subtitle: Text('settingsThemeMaterial3Description').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17), contentPadding: const EdgeInsets.only(left: 24, right: 17),
secondary: const Icon(Symbols.new_releases), secondary: const Icon(Symbols.new_releases),
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false, value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_prefs.setBool( _prefs.setBool(
@ -142,30 +189,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value); Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value);
final color = await showDialog<Color?>( final color = await showDialog<Color?>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) =>
content: SingleChildScrollView( AlertDialog(
child: ColorPicker( content: SingleChildScrollView(
pickerColor: pickerColor, child: ColorPicker(
onColorChanged: (color) => pickerColor = color, pickerColor: pickerColor,
enableAlpha: false, onColorChanged: (color) => pickerColor = color,
hexInputBar: true, enableAlpha: false,
hexInputBar: true,
),
),
actions: <Widget>[
TextButton(
child: const Text('dialogDismiss').tr(),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('dialogConfirm').tr(),
onPressed: () {
Navigator.of(context).pop(pickerColor);
},
),
],
), ),
),
actions: <Widget>[
TextButton(
child: const Text('dialogDismiss').tr(),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('dialogConfirm').tr(),
onPressed: () {
Navigator.of(context).pop(pickerColor);
},
),
],
),
); );
if (color == null || !context.mounted) return; if (color == null || !context.mounted) return;
@ -201,11 +249,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
value: _prefs.getInt(kAppColorSchemeStoreKey) == null value: _prefs.getInt(kAppColorSchemeStoreKey) == null
? 1 ? 1
: kColorSchemes.values : kColorSchemes.values
.toList() .toList()
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)), .indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
onChanged: (int? value) { onChanged: (int? value) {
if (value != null && value != -1) { if (value != null && value != -1) {
_prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values.elementAt(value).value); _prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values
.elementAt(value)
.value);
final th = context.read<ThemeProvider>(); final th = context.read<ThemeProvider>();
th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value)); th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value));
setState(() {}); setState(() {});
@ -240,6 +290,61 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {}); setState(() {});
}, },
), ),
CheckboxListTile(
secondary: const Icon(Symbols.left_panel_close),
title: Text('settingsDrawerPreferCollapse').tr(),
subtitle: Text('settingsDrawerPreferCollapseDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
onChanged: (value) {
_prefs.setBool(kAppDrawerPreferCollapse, value ?? false);
final cfg = context.read<ConfigProvider>();
cfg.calcDrawerSize(context);
setState(() {});
},
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('settingsFeatures').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
CheckboxListTile(
secondary: const Icon(Symbols.vibration),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
title: Text('settingsNotifyWithHaptic').tr(),
subtitle: Text('settingsNotifyWithHapticDescription').tr(),
value: _prefs.getBool(kAppNotifyWithHaptic) ?? true,
onChanged: (value) {
setState(() {
_prefs.setBool(kAppNotifyWithHaptic, value ?? false);
});
},
),
CheckboxListTile(
secondary: const Icon(Symbols.link),
title: Text('settingsExpandPostLink').tr(),
subtitle: Text('settingsExpandPostLinkDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
value: _prefs.getBool(kAppExpandPostLink) ?? true,
onChanged: (value) {
setState(() {
_prefs.setBool(kAppExpandPostLink, value ?? false);
});
},
),
CheckboxListTile(
secondary: const Icon(Symbols.chat),
title: Text('settingsExpandChatLink').tr(),
subtitle: Text('settingsExpandChatLinkDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
value: _prefs.getBool(kAppExpandChatLink) ?? true,
onChanged: (value) {
setState(() {
_prefs.setBool(kAppExpandChatLink, value ?? false);
});
},
),
], ],
), ),
Column( Column(
@ -282,7 +387,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
('Custom', _serverUrlController.text), ('Custom', _serverUrlController.text),
] ]
.map( .map(
(item) => DropdownMenuItem<String>( (item) =>
DropdownMenuItem<String>(
value: item.$2, value: item.$2,
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
@ -294,7 +400,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
], ],
), ),
), ),
) )
.toList(), .toList(),
value: _serverUrlController.text, value: _serverUrlController.text,
onChanged: (String? value) { onChanged: (String? value) {
@ -349,11 +455,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
isExpanded: true, isExpanded: true,
items: kImageQualityLevel.entries items: kImageQualityLevel.entries
.map( .map(
(item) => DropdownMenuItem<FilterQuality>( (item) =>
DropdownMenuItem<FilterQuality>(
value: item.value, value: item.value,
child: Text(item.key).tr().fontSize(14), child: Text(item.key).tr().fontSize(14),
), ),
) )
.toList(), .toList(),
onChanged: (FilterQuality? value) { onChanged: (FilterQuality? value) {
if (value == null) return; if (value == null) return;

View File

@ -8,9 +8,20 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:provider/provider.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/post_write_controller.dart'; import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/channel.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/chat/room.dart';
import 'package:surface/screens/post/post_editor.dart'; import 'package:surface/screens/post/post_editor.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
class AppSharingListener extends StatefulWidget { class AppSharingListener extends StatefulWidget {
final Widget child; final Widget child;
@ -51,20 +62,39 @@ class _AppSharingListenerState extends State<AppSharingListener> {
pathParameters: { pathParameters: {
'mode': 'stories', 'mode': 'stories',
}, },
extra: PostEditorExtraProps( extra: PostEditorExtra(
text: value text: value
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
.map((e) => e.path).join('\n'), .map((e) => e.path)
.join('\n'),
attachments: value attachments: value
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type)) .where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
.map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(), .contains(e.type))
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
.toList(),
), ),
); );
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
leading: Icon(Icons.chat_outlined),
trailing: const Icon(Icons.chevron_right),
title: Text('shareIntentSendChannel').tr(),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => _ShareIntentChannelSelect(value: value),
).then((val) {
if (!context.mounted) return;
if (val == true) Navigator.pop(context);
});
},
),
], ],
), ).width(280),
) )
], ],
), ),
@ -103,7 +133,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
_initialize(); _initialize();
_initialHandle(); _initialHandle();
} }
@ -120,3 +150,193 @@ class _AppSharingListenerState extends State<AppSharingListener> {
return widget.child; return widget.child;
} }
} }
class _ShareIntentChannelSelect extends StatefulWidget {
final Iterable<SharedMediaFile> value;
const _ShareIntentChannelSelect({required this.value});
@override
State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState();
}
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
bool _isBusy = true;
List<SnChannel>? _channels;
Map<int, SnChatMessage>? _lastMessages;
void _refreshChannels() {
final ua = context.read<UserProvider>();
if (!ua.isAuthorized) {
setState(() => _isBusy = false);
return;
}
final chan = context.read<ChatChannelProvider>();
chan.fetchChannels().listen((channels) async {
final lastMessages = await chan.getLastMessages(channels);
_lastMessages = {for (final val in lastMessages) val.channelId: val};
channels.sort((a, b) {
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) {
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt);
}
if (_lastMessages!.containsKey(a.id)) return -1;
if (_lastMessages!.containsKey(b.id)) return 1;
return 0;
});
if (!mounted) return;
final ud = context.read<UserDirectoryProvider>();
for (final channel in channels) {
if (channel.type == 1) {
await ud.listAccount(
channel.members
?.cast<SnChannelMember?>()
.map((ele) => ele?.accountId)
.where((ele) => ele != null)
.toSet() ??
{},
);
}
}
if (mounted) setState(() => _channels = channels);
})
..onError((err) {
if (!mounted) return;
context.showErrorDialog(err);
setState(() => _isBusy = false);
})
..onDone(() {
if (!mounted) return;
setState(() => _isBusy = false);
});
}
@override
void initState() {
super.initState();
_refreshChannels();
}
@override
Widget build(BuildContext context) {
final ua = context.read<UserProvider>();
final ud = context.read<UserDirectoryProvider>();
return Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.chat, size: 24),
const Gap(16),
Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
LoadingIndicator(isActive: _isBusy),
Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: () => Future.sync(() => _refreshChannels()),
child: ListView.builder(
itemCount: _channels?.length ?? 0,
itemBuilder: (context, idx) {
final channel = _channels![idx];
final lastMessage = _lastMessages?[channel.id];
if (channel.type == 1) {
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
(ele) => ele?.accountId != ua.user?.id,
orElse: () => null,
);
return ListTile(
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
'channelDirectMessageDescription'.tr(args: [
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
),
onTap: () {
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
).then((value) {
if (mounted) _refreshChannels();
});
},
);
}
return ListTile(
title: Text(channel.name),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
channel.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: null,
fallbackWidget: const Icon(Symbols.chat, size: 20),
),
onTap: () {
Navigator.pop(context, true);
GoRouter.of(context)
.pushNamed(
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
extra: ChatRoomScreenExtra(
initialText: widget.value
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
.map((e) => e.path)
.join('\n'),
initialAttachments: widget.value
.where((e) =>
[SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
.toList(),
),
)
.then((value) {
if (value == true) _refreshChannels();
});
},
);
},
),
),
),
),
],
);
}
}

279
lib/screens/wallet.dart Normal file
View 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),
),
),
);
}
}

View File

@ -20,7 +20,7 @@ Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3
Future<ThemeData> createAppTheme( Future<ThemeData> createAppTheme(
Brightness brightness, { Brightness brightness, {
Color? seedColorOverride, Color? seedColorOverride,
bool? useMaterial3, bool? useMaterial3,
}) async { }) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@ -34,9 +34,10 @@ Future<ThemeData> createAppTheme(
); );
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
return ThemeData( return ThemeData(
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false), useMaterial3: useM3,
colorScheme: colorScheme, colorScheme: colorScheme,
brightness: brightness, brightness: brightness,
iconTheme: IconThemeData( iconTheme: IconThemeData(
@ -45,12 +46,24 @@ Future<ThemeData> createAppTheme(
opticalSize: 20, opticalSize: 20,
color: colorScheme.onSurface, color: colorScheme.onSurface,
), ),
snackBarTheme: SnackBarThemeData(
behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
),
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
centerTitle: true, centerTitle: true,
elevation: hasAppBarBlurry ? 0 : null, elevation: hasAppBarBlurry ? 0 : null,
backgroundColor: hasAppBarBlurry ? colorScheme.surfaceContainer.withAlpha(200) : colorScheme.primary, backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
), ),
scaffoldBackgroundColor: Colors.transparent, pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
},
),
); );
} }

View File

@ -15,12 +15,13 @@ class SnAccount with _$SnAccount {
required DateTime? deletedAt, required DateTime? deletedAt,
required DateTime? confirmedAt, required DateTime? confirmedAt,
required List<SnAccountContact>? contacts, required List<SnAccountContact>? contacts,
required String avatar, @Default("") String avatar,
required String banner, @Default("") String banner,
required String description, required String description,
required String name, required String name,
required String nick, required String nick,
required Map<String, dynamic> permNodes, required Map<String, dynamic> permNodes,
required String language,
required SnAccountProfile? profile, required SnAccountProfile? profile,
@Default([]) List<SnAccountBadge> badges, @Default([]) List<SnAccountBadge> badges,
required DateTime? suspendedAt, required DateTime? suspendedAt,

View File

@ -33,6 +33,7 @@ mixin _$SnAccount {
String get name => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError;
String get nick => throw _privateConstructorUsedError; String get nick => throw _privateConstructorUsedError;
Map<String, dynamic> get permNodes => throw _privateConstructorUsedError; Map<String, dynamic> get permNodes => throw _privateConstructorUsedError;
String get language => throw _privateConstructorUsedError;
SnAccountProfile? get profile => throw _privateConstructorUsedError; SnAccountProfile? get profile => throw _privateConstructorUsedError;
List<SnAccountBadge> get badges => throw _privateConstructorUsedError; List<SnAccountBadge> get badges => throw _privateConstructorUsedError;
DateTime? get suspendedAt => throw _privateConstructorUsedError; DateTime? get suspendedAt => throw _privateConstructorUsedError;
@ -69,6 +70,7 @@ abstract class $SnAccountCopyWith<$Res> {
String name, String name,
String nick, String nick,
Map<String, dynamic> permNodes, Map<String, dynamic> permNodes,
String language,
SnAccountProfile? profile, SnAccountProfile? profile,
List<SnAccountBadge> badges, List<SnAccountBadge> badges,
DateTime? suspendedAt, DateTime? suspendedAt,
@ -107,6 +109,7 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
Object? name = null, Object? name = null,
Object? nick = null, Object? nick = null,
Object? permNodes = null, Object? permNodes = null,
Object? language = null,
Object? profile = freezed, Object? profile = freezed,
Object? badges = null, Object? badges = null,
Object? suspendedAt = freezed, Object? suspendedAt = freezed,
@ -164,6 +167,10 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
? _value.permNodes ? _value.permNodes
: permNodes // ignore: cast_nullable_to_non_nullable : permNodes // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>, as Map<String, dynamic>,
language: null == language
? _value.language
: language // ignore: cast_nullable_to_non_nullable
as String,
profile: freezed == profile profile: freezed == profile
? _value.profile ? _value.profile
: profile // ignore: cast_nullable_to_non_nullable : profile // ignore: cast_nullable_to_non_nullable
@ -231,6 +238,7 @@ abstract class _$$SnAccountImplCopyWith<$Res>
String name, String name,
String nick, String nick,
Map<String, dynamic> permNodes, Map<String, dynamic> permNodes,
String language,
SnAccountProfile? profile, SnAccountProfile? profile,
List<SnAccountBadge> badges, List<SnAccountBadge> badges,
DateTime? suspendedAt, DateTime? suspendedAt,
@ -268,6 +276,7 @@ class __$$SnAccountImplCopyWithImpl<$Res>
Object? name = null, Object? name = null,
Object? nick = null, Object? nick = null,
Object? permNodes = null, Object? permNodes = null,
Object? language = null,
Object? profile = freezed, Object? profile = freezed,
Object? badges = null, Object? badges = null,
Object? suspendedAt = freezed, Object? suspendedAt = freezed,
@ -325,6 +334,10 @@ class __$$SnAccountImplCopyWithImpl<$Res>
? _value._permNodes ? _value._permNodes
: permNodes // ignore: cast_nullable_to_non_nullable : permNodes // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>, as Map<String, dynamic>,
language: null == language
? _value.language
: language // ignore: cast_nullable_to_non_nullable
as String,
profile: freezed == profile profile: freezed == profile
? _value.profile ? _value.profile
: profile // ignore: cast_nullable_to_non_nullable : profile // ignore: cast_nullable_to_non_nullable
@ -367,12 +380,13 @@ class _$SnAccountImpl extends _SnAccount {
required this.deletedAt, required this.deletedAt,
required this.confirmedAt, required this.confirmedAt,
required final List<SnAccountContact>? contacts, required final List<SnAccountContact>? contacts,
required this.avatar, this.avatar = "",
required this.banner, this.banner = "",
required this.description, required this.description,
required this.name, required this.name,
required this.nick, required this.nick,
required final Map<String, dynamic> permNodes, required final Map<String, dynamic> permNodes,
required this.language,
required this.profile, required this.profile,
final List<SnAccountBadge> badges = const [], final List<SnAccountBadge> badges = const [],
required this.suspendedAt, required this.suspendedAt,
@ -410,8 +424,10 @@ class _$SnAccountImpl extends _SnAccount {
} }
@override @override
@JsonKey()
final String avatar; final String avatar;
@override @override
@JsonKey()
final String banner; final String banner;
@override @override
final String description; final String description;
@ -427,6 +443,8 @@ class _$SnAccountImpl extends _SnAccount {
return EqualUnmodifiableMapView(_permNodes); return EqualUnmodifiableMapView(_permNodes);
} }
@override
final String language;
@override @override
final SnAccountProfile? profile; final SnAccountProfile? profile;
final List<SnAccountBadge> _badges; final List<SnAccountBadge> _badges;
@ -451,7 +469,7 @@ class _$SnAccountImpl extends _SnAccount {
@override @override
String toString() { String toString() {
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)'; return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
} }
@override @override
@ -477,6 +495,8 @@ class _$SnAccountImpl extends _SnAccount {
(identical(other.nick, nick) || other.nick == nick) && (identical(other.nick, nick) || other.nick == nick) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other._permNodes, _permNodes) && .equals(other._permNodes, _permNodes) &&
(identical(other.language, language) ||
other.language == language) &&
(identical(other.profile, profile) || other.profile == profile) && (identical(other.profile, profile) || other.profile == profile) &&
const DeepCollectionEquality().equals(other._badges, _badges) && const DeepCollectionEquality().equals(other._badges, _badges) &&
(identical(other.suspendedAt, suspendedAt) || (identical(other.suspendedAt, suspendedAt) ||
@ -507,6 +527,7 @@ class _$SnAccountImpl extends _SnAccount {
name, name,
nick, nick,
const DeepCollectionEquality().hash(_permNodes), const DeepCollectionEquality().hash(_permNodes),
language,
profile, profile,
const DeepCollectionEquality().hash(_badges), const DeepCollectionEquality().hash(_badges),
suspendedAt, suspendedAt,
@ -540,12 +561,13 @@ abstract class _SnAccount extends SnAccount {
required final DateTime? deletedAt, required final DateTime? deletedAt,
required final DateTime? confirmedAt, required final DateTime? confirmedAt,
required final List<SnAccountContact>? contacts, required final List<SnAccountContact>? contacts,
required final String avatar, final String avatar,
required final String banner, final String banner,
required final String description, required final String description,
required final String name, required final String name,
required final String nick, required final String nick,
required final Map<String, dynamic> permNodes, required final Map<String, dynamic> permNodes,
required final String language,
required final SnAccountProfile? profile, required final SnAccountProfile? profile,
final List<SnAccountBadge> badges, final List<SnAccountBadge> badges,
required final DateTime? suspendedAt, required final DateTime? suspendedAt,
@ -584,6 +606,8 @@ abstract class _SnAccount extends SnAccount {
@override @override
Map<String, dynamic> get permNodes; Map<String, dynamic> get permNodes;
@override @override
String get language;
@override
SnAccountProfile? get profile; SnAccountProfile? get profile;
@override @override
List<SnAccountBadge> get badges; List<SnAccountBadge> get badges;

View File

@ -20,12 +20,13 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
contacts: (json['contacts'] as List<dynamic>?) contacts: (json['contacts'] as List<dynamic>?)
?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>)) ?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
avatar: json['avatar'] as String, avatar: json['avatar'] as String? ?? "",
banner: json['banner'] as String, banner: json['banner'] as String? ?? "",
description: json['description'] as String, description: json['description'] as String,
name: json['name'] as String, name: json['name'] as String,
nick: json['nick'] as String, nick: json['nick'] as String,
permNodes: json['perm_nodes'] as Map<String, dynamic>, permNodes: json['perm_nodes'] as Map<String, dynamic>,
language: json['language'] as String,
profile: json['profile'] == null profile: json['profile'] == null
? null ? null
: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>), : SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
@ -56,6 +57,7 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
'name': instance.name, 'name': instance.name,
'nick': instance.nick, 'nick': instance.nick,
'perm_nodes': instance.permNodes, 'perm_nodes': instance.permNodes,
'language': instance.language,
'profile': instance.profile?.toJson(), 'profile': instance.profile?.toJson(),
'badges': instance.badges.map((e) => e.toJson()).toList(), 'badges': instance.badges.map((e) => e.toJson()).toList(),
'suspended_at': instance.suspendedAt?.toIso8601String(), 'suspended_at': instance.suspendedAt?.toIso8601String(),

View File

@ -16,6 +16,7 @@ class SnCheckInRecord with _$SnCheckInRecord {
required DateTime? deletedAt, required DateTime? deletedAt,
required int resultTier, required int resultTier,
required int resultExperience, required int resultExperience,
required double resultCoin,
required List<int> resultModifiers, required List<int> resultModifiers,
required int accountId, required int accountId,
}) = _SnCheckInRecord; }) = _SnCheckInRecord;

View File

@ -26,6 +26,7 @@ mixin _$SnCheckInRecord {
DateTime? get deletedAt => throw _privateConstructorUsedError; DateTime? get deletedAt => throw _privateConstructorUsedError;
int get resultTier => throw _privateConstructorUsedError; int get resultTier => throw _privateConstructorUsedError;
int get resultExperience => throw _privateConstructorUsedError; int get resultExperience => throw _privateConstructorUsedError;
double get resultCoin => throw _privateConstructorUsedError;
List<int> get resultModifiers => throw _privateConstructorUsedError; List<int> get resultModifiers => throw _privateConstructorUsedError;
int get accountId => throw _privateConstructorUsedError; int get accountId => throw _privateConstructorUsedError;
@ -52,6 +53,7 @@ abstract class $SnCheckInRecordCopyWith<$Res> {
DateTime? deletedAt, DateTime? deletedAt,
int resultTier, int resultTier,
int resultExperience, int resultExperience,
double resultCoin,
List<int> resultModifiers, List<int> resultModifiers,
int accountId}); int accountId});
} }
@ -77,6 +79,7 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
Object? deletedAt = freezed, Object? deletedAt = freezed,
Object? resultTier = null, Object? resultTier = null,
Object? resultExperience = null, Object? resultExperience = null,
Object? resultCoin = null,
Object? resultModifiers = null, Object? resultModifiers = null,
Object? accountId = null, Object? accountId = null,
}) { }) {
@ -105,6 +108,10 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
? _value.resultExperience ? _value.resultExperience
: resultExperience // ignore: cast_nullable_to_non_nullable : resultExperience // ignore: cast_nullable_to_non_nullable
as int, as int,
resultCoin: null == resultCoin
? _value.resultCoin
: resultCoin // ignore: cast_nullable_to_non_nullable
as double,
resultModifiers: null == resultModifiers resultModifiers: null == resultModifiers
? _value.resultModifiers ? _value.resultModifiers
: resultModifiers // ignore: cast_nullable_to_non_nullable : resultModifiers // ignore: cast_nullable_to_non_nullable
@ -132,6 +139,7 @@ abstract class _$$SnCheckInRecordImplCopyWith<$Res>
DateTime? deletedAt, DateTime? deletedAt,
int resultTier, int resultTier,
int resultExperience, int resultExperience,
double resultCoin,
List<int> resultModifiers, List<int> resultModifiers,
int accountId}); int accountId});
} }
@ -155,6 +163,7 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
Object? deletedAt = freezed, Object? deletedAt = freezed,
Object? resultTier = null, Object? resultTier = null,
Object? resultExperience = null, Object? resultExperience = null,
Object? resultCoin = null,
Object? resultModifiers = null, Object? resultModifiers = null,
Object? accountId = null, Object? accountId = null,
}) { }) {
@ -183,6 +192,10 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
? _value.resultExperience ? _value.resultExperience
: resultExperience // ignore: cast_nullable_to_non_nullable : resultExperience // ignore: cast_nullable_to_non_nullable
as int, as int,
resultCoin: null == resultCoin
? _value.resultCoin
: resultCoin // ignore: cast_nullable_to_non_nullable
as double,
resultModifiers: null == resultModifiers resultModifiers: null == resultModifiers
? _value._resultModifiers ? _value._resultModifiers
: resultModifiers // ignore: cast_nullable_to_non_nullable : resultModifiers // ignore: cast_nullable_to_non_nullable
@ -205,6 +218,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
required this.deletedAt, required this.deletedAt,
required this.resultTier, required this.resultTier,
required this.resultExperience, required this.resultExperience,
required this.resultCoin,
required final List<int> resultModifiers, required final List<int> resultModifiers,
required this.accountId}) required this.accountId})
: _resultModifiers = resultModifiers, : _resultModifiers = resultModifiers,
@ -225,6 +239,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
final int resultTier; final int resultTier;
@override @override
final int resultExperience; final int resultExperience;
@override
final double resultCoin;
final List<int> _resultModifiers; final List<int> _resultModifiers;
@override @override
List<int> get resultModifiers { List<int> get resultModifiers {
@ -238,7 +254,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
@override @override
String toString() { String toString() {
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultModifiers: $resultModifiers, accountId: $accountId)'; return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
} }
@override @override
@ -257,6 +273,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
other.resultTier == resultTier) && other.resultTier == resultTier) &&
(identical(other.resultExperience, resultExperience) || (identical(other.resultExperience, resultExperience) ||
other.resultExperience == resultExperience) && other.resultExperience == resultExperience) &&
(identical(other.resultCoin, resultCoin) ||
other.resultCoin == resultCoin) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other._resultModifiers, _resultModifiers) && .equals(other._resultModifiers, _resultModifiers) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
@ -273,6 +291,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
deletedAt, deletedAt,
resultTier, resultTier,
resultExperience, resultExperience,
resultCoin,
const DeepCollectionEquality().hash(_resultModifiers), const DeepCollectionEquality().hash(_resultModifiers),
accountId); accountId);
@ -301,6 +320,7 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
required final DateTime? deletedAt, required final DateTime? deletedAt,
required final int resultTier, required final int resultTier,
required final int resultExperience, required final int resultExperience,
required final double resultCoin,
required final List<int> resultModifiers, required final List<int> resultModifiers,
required final int accountId}) = _$SnCheckInRecordImpl; required final int accountId}) = _$SnCheckInRecordImpl;
const _SnCheckInRecord._() : super._(); const _SnCheckInRecord._() : super._();
@ -321,6 +341,8 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
@override @override
int get resultExperience; int get resultExperience;
@override @override
double get resultCoin;
@override
List<int> get resultModifiers; List<int> get resultModifiers;
@override @override
int get accountId; int get accountId;

View File

@ -17,6 +17,7 @@ _$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson(
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
resultTier: (json['result_tier'] as num).toInt(), resultTier: (json['result_tier'] as num).toInt(),
resultExperience: (json['result_experience'] as num).toInt(), resultExperience: (json['result_experience'] as num).toInt(),
resultCoin: (json['result_coin'] as num).toDouble(),
resultModifiers: (json['result_modifiers'] as List<dynamic>) resultModifiers: (json['result_modifiers'] as List<dynamic>)
.map((e) => (e as num).toInt()) .map((e) => (e as num).toInt())
.toList(), .toList(),
@ -32,6 +33,7 @@ Map<String, dynamic> _$$SnCheckInRecordImplToJson(
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
'result_tier': instance.resultTier, 'result_tier': instance.resultTier,
'result_experience': instance.resultExperience, 'result_experience': instance.resultExperience,
'result_coin': instance.resultCoin,
'result_modifiers': instance.resultModifiers, 'result_modifiers': instance.resultModifiers,
'account_id': instance.accountId, 'account_id': instance.accountId,
}; };

38
lib/types/news.dart Normal file
View File

@ -0,0 +1,38 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'news.freezed.dart';
part 'news.g.dart';
@freezed
class SnNewsSource with _$SnNewsSource {
const factory SnNewsSource({
required String id,
required String label,
required String type,
required String source,
required int depth,
required bool enabled,
}) = _SnNewsSource;
factory SnNewsSource.fromJson(Map<String, dynamic> json) => _$SnNewsSourceFromJson(json);
}
@freezed
class SnNewsArticle with _$SnNewsArticle {
const factory SnNewsArticle({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required dynamic deletedAt,
required String thumbnail,
required String title,
required String description,
required String content,
required String url,
required String hash,
required String source,
required DateTime? publishedAt,
}) = _SnNewsArticle;
factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json);
}

660
lib/types/news.freezed.dart Normal file
View File

@ -0,0 +1,660 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'news.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) {
return _SnNewsSource.fromJson(json);
}
/// @nodoc
mixin _$SnNewsSource {
String get id => throw _privateConstructorUsedError;
String get label => throw _privateConstructorUsedError;
String get type => throw _privateConstructorUsedError;
String get source => throw _privateConstructorUsedError;
int get depth => throw _privateConstructorUsedError;
bool get enabled => throw _privateConstructorUsedError;
/// Serializes this SnNewsSource to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnNewsSource
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnNewsSourceCopyWith<SnNewsSource> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnNewsSourceCopyWith<$Res> {
factory $SnNewsSourceCopyWith(
SnNewsSource value, $Res Function(SnNewsSource) then) =
_$SnNewsSourceCopyWithImpl<$Res, SnNewsSource>;
@useResult
$Res call(
{String id,
String label,
String type,
String source,
int depth,
bool enabled});
}
/// @nodoc
class _$SnNewsSourceCopyWithImpl<$Res, $Val extends SnNewsSource>
implements $SnNewsSourceCopyWith<$Res> {
_$SnNewsSourceCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnNewsSource
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? label = null,
Object? type = null,
Object? source = null,
Object? depth = null,
Object? enabled = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
label: null == label
? _value.label
: label // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _value.source
: source // ignore: cast_nullable_to_non_nullable
as String,
depth: null == depth
? _value.depth
: depth // ignore: cast_nullable_to_non_nullable
as int,
enabled: null == enabled
? _value.enabled
: enabled // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$SnNewsSourceImplCopyWith<$Res>
implements $SnNewsSourceCopyWith<$Res> {
factory _$$SnNewsSourceImplCopyWith(
_$SnNewsSourceImpl value, $Res Function(_$SnNewsSourceImpl) then) =
__$$SnNewsSourceImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String id,
String label,
String type,
String source,
int depth,
bool enabled});
}
/// @nodoc
class __$$SnNewsSourceImplCopyWithImpl<$Res>
extends _$SnNewsSourceCopyWithImpl<$Res, _$SnNewsSourceImpl>
implements _$$SnNewsSourceImplCopyWith<$Res> {
__$$SnNewsSourceImplCopyWithImpl(
_$SnNewsSourceImpl _value, $Res Function(_$SnNewsSourceImpl) _then)
: super(_value, _then);
/// Create a copy of SnNewsSource
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? label = null,
Object? type = null,
Object? source = null,
Object? depth = null,
Object? enabled = null,
}) {
return _then(_$SnNewsSourceImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
label: null == label
? _value.label
: label // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _value.source
: source // ignore: cast_nullable_to_non_nullable
as String,
depth: null == depth
? _value.depth
: depth // ignore: cast_nullable_to_non_nullable
as int,
enabled: null == enabled
? _value.enabled
: enabled // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnNewsSourceImpl implements _SnNewsSource {
const _$SnNewsSourceImpl(
{required this.id,
required this.label,
required this.type,
required this.source,
required this.depth,
required this.enabled});
factory _$SnNewsSourceImpl.fromJson(Map<String, dynamic> json) =>
_$$SnNewsSourceImplFromJson(json);
@override
final String id;
@override
final String label;
@override
final String type;
@override
final String source;
@override
final int depth;
@override
final bool enabled;
@override
String toString() {
return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnNewsSourceImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.label, label) || other.label == label) &&
(identical(other.type, type) || other.type == type) &&
(identical(other.source, source) || other.source == source) &&
(identical(other.depth, depth) || other.depth == depth) &&
(identical(other.enabled, enabled) || other.enabled == enabled));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
Object.hash(runtimeType, id, label, type, source, depth, enabled);
/// Create a copy of SnNewsSource
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith =>
__$$SnNewsSourceImplCopyWithImpl<_$SnNewsSourceImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnNewsSourceImplToJson(
this,
);
}
}
abstract class _SnNewsSource implements SnNewsSource {
const factory _SnNewsSource(
{required final String id,
required final String label,
required final String type,
required final String source,
required final int depth,
required final bool enabled}) = _$SnNewsSourceImpl;
factory _SnNewsSource.fromJson(Map<String, dynamic> json) =
_$SnNewsSourceImpl.fromJson;
@override
String get id;
@override
String get label;
@override
String get type;
@override
String get source;
@override
int get depth;
@override
bool get enabled;
/// Create a copy of SnNewsSource
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith =>
throw _privateConstructorUsedError;
}
SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) {
return _SnNewsArticle.fromJson(json);
}
/// @nodoc
mixin _$SnNewsArticle {
int get id => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
dynamic get deletedAt => throw _privateConstructorUsedError;
String get thumbnail => throw _privateConstructorUsedError;
String get title => throw _privateConstructorUsedError;
String get description => throw _privateConstructorUsedError;
String get content => throw _privateConstructorUsedError;
String get url => throw _privateConstructorUsedError;
String get hash => throw _privateConstructorUsedError;
String get source => throw _privateConstructorUsedError;
DateTime? get publishedAt => throw _privateConstructorUsedError;
/// Serializes this SnNewsArticle to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnNewsArticle
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnNewsArticleCopyWith<SnNewsArticle> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnNewsArticleCopyWith<$Res> {
factory $SnNewsArticleCopyWith(
SnNewsArticle value, $Res Function(SnNewsArticle) then) =
_$SnNewsArticleCopyWithImpl<$Res, SnNewsArticle>;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
dynamic deletedAt,
String thumbnail,
String title,
String description,
String content,
String url,
String hash,
String source,
DateTime? publishedAt});
}
/// @nodoc
class _$SnNewsArticleCopyWithImpl<$Res, $Val extends SnNewsArticle>
implements $SnNewsArticleCopyWith<$Res> {
_$SnNewsArticleCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnNewsArticle
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? thumbnail = null,
Object? title = null,
Object? description = null,
Object? content = null,
Object? url = null,
Object? hash = null,
Object? source = null,
Object? publishedAt = freezed,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as dynamic,
thumbnail: null == thumbnail
? _value.thumbnail
: thumbnail // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
content: null == content
? _value.content
: content // ignore: cast_nullable_to_non_nullable
as String,
url: null == url
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as String,
hash: null == hash
? _value.hash
: hash // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _value.source
: source // ignore: cast_nullable_to_non_nullable
as String,
publishedAt: freezed == publishedAt
? _value.publishedAt
: publishedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
) as $Val);
}
}
/// @nodoc
abstract class _$$SnNewsArticleImplCopyWith<$Res>
implements $SnNewsArticleCopyWith<$Res> {
factory _$$SnNewsArticleImplCopyWith(
_$SnNewsArticleImpl value, $Res Function(_$SnNewsArticleImpl) then) =
__$$SnNewsArticleImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
dynamic deletedAt,
String thumbnail,
String title,
String description,
String content,
String url,
String hash,
String source,
DateTime? publishedAt});
}
/// @nodoc
class __$$SnNewsArticleImplCopyWithImpl<$Res>
extends _$SnNewsArticleCopyWithImpl<$Res, _$SnNewsArticleImpl>
implements _$$SnNewsArticleImplCopyWith<$Res> {
__$$SnNewsArticleImplCopyWithImpl(
_$SnNewsArticleImpl _value, $Res Function(_$SnNewsArticleImpl) _then)
: super(_value, _then);
/// Create a copy of SnNewsArticle
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? thumbnail = null,
Object? title = null,
Object? description = null,
Object? content = null,
Object? url = null,
Object? hash = null,
Object? source = null,
Object? publishedAt = freezed,
}) {
return _then(_$SnNewsArticleImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as dynamic,
thumbnail: null == thumbnail
? _value.thumbnail
: thumbnail // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
content: null == content
? _value.content
: content // ignore: cast_nullable_to_non_nullable
as String,
url: null == url
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as String,
hash: null == hash
? _value.hash
: hash // ignore: cast_nullable_to_non_nullable
as String,
source: null == source
? _value.source
: source // ignore: cast_nullable_to_non_nullable
as String,
publishedAt: freezed == publishedAt
? _value.publishedAt
: publishedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnNewsArticleImpl implements _SnNewsArticle {
const _$SnNewsArticleImpl(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.thumbnail,
required this.title,
required this.description,
required this.content,
required this.url,
required this.hash,
required this.source,
required this.publishedAt});
factory _$SnNewsArticleImpl.fromJson(Map<String, dynamic> json) =>
_$$SnNewsArticleImplFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final dynamic deletedAt;
@override
final String thumbnail;
@override
final String title;
@override
final String description;
@override
final String content;
@override
final String url;
@override
final String hash;
@override
final String source;
@override
final DateTime? publishedAt;
@override
String toString() {
return 'SnNewsArticle(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, source: $source, publishedAt: $publishedAt)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnNewsArticleImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
(identical(other.thumbnail, thumbnail) ||
other.thumbnail == thumbnail) &&
(identical(other.title, title) || other.title == title) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.content, content) || other.content == content) &&
(identical(other.url, url) || other.url == url) &&
(identical(other.hash, hash) || other.hash == hash) &&
(identical(other.source, source) || other.source == source) &&
(identical(other.publishedAt, publishedAt) ||
other.publishedAt == publishedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
createdAt,
updatedAt,
const DeepCollectionEquality().hash(deletedAt),
thumbnail,
title,
description,
content,
url,
hash,
source,
publishedAt);
/// Create a copy of SnNewsArticle
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith =>
__$$SnNewsArticleImplCopyWithImpl<_$SnNewsArticleImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnNewsArticleImplToJson(
this,
);
}
}
abstract class _SnNewsArticle implements SnNewsArticle {
const factory _SnNewsArticle(
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final dynamic deletedAt,
required final String thumbnail,
required final String title,
required final String description,
required final String content,
required final String url,
required final String hash,
required final String source,
required final DateTime? publishedAt}) = _$SnNewsArticleImpl;
factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =
_$SnNewsArticleImpl.fromJson;
@override
int get id;
@override
DateTime get createdAt;
@override
DateTime get updatedAt;
@override
dynamic get deletedAt;
@override
String get thumbnail;
@override
String get title;
@override
String get description;
@override
String get content;
@override
String get url;
@override
String get hash;
@override
String get source;
@override
DateTime? get publishedAt;
/// Create a copy of SnNewsArticle
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith =>
throw _privateConstructorUsedError;
}

61
lib/types/news.g.dart Normal file
View File

@ -0,0 +1,61 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'news.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$SnNewsSourceImpl _$$SnNewsSourceImplFromJson(Map<String, dynamic> json) =>
_$SnNewsSourceImpl(
id: json['id'] as String,
label: json['label'] as String,
type: json['type'] as String,
source: json['source'] as String,
depth: (json['depth'] as num).toInt(),
enabled: json['enabled'] as bool,
);
Map<String, dynamic> _$$SnNewsSourceImplToJson(_$SnNewsSourceImpl instance) =>
<String, dynamic>{
'id': instance.id,
'label': instance.label,
'type': instance.type,
'source': instance.source,
'depth': instance.depth,
'enabled': instance.enabled,
};
_$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map<String, dynamic> json) =>
_$SnNewsArticleImpl(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'],
thumbnail: json['thumbnail'] as String,
title: json['title'] as String,
description: json['description'] as String,
content: json['content'] as String,
url: json['url'] as String,
hash: json['hash'] as String,
source: json['source'] as String,
publishedAt: json['published_at'] == null
? null
: DateTime.parse(json['published_at'] as String),
);
Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt,
'thumbnail': instance.thumbnail,
'title': instance.title,
'description': instance.description,
'content': instance.content,
'url': instance.url,
'hash': instance.hash,
'source': instance.source,
'published_at': instance.publishedAt?.toIso8601String(),
};

37
lib/types/wallet.dart Normal file
View 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);
}

View 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
View 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,
};

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class AboutScreen extends StatelessWidget { class AboutScreen extends StatelessWidget {
@ -12,97 +13,103 @@ class AboutScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4)); const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
return SizedBox( return AppScaffold(
width: double.infinity, appBar: AppBar(
child: Column( leading: const PageBackButton(),
mainAxisAlignment: MainAxisAlignment.center, title: Text('screenAbout').tr(),
children: [ ),
ClipRRect( body: SizedBox(
borderRadius: const BorderRadius.all(Radius.circular(16)), width: double.infinity,
child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120), child: Column(
), mainAxisAlignment: MainAxisAlignment.center,
const Gap(8), children: [
Text( ClipRRect(
'Solian', borderRadius: const BorderRadius.all(Radius.circular(16)),
style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36), child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
), ),
const Text( const Gap(8),
'The Solar Network', Text(
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), 'Solian',
), style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
const Gap(8), ),
FutureBuilder( const Text(
future: PackageInfo.fromPlatform(), 'The Solar Network',
builder: (context, snapshot) { style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
if (!snapshot.hasData) { ),
return const SizedBox.shrink(); const Gap(8),
} FutureBuilder(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
return Text( return Text(
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}', 'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
style: const TextStyle(fontFamily: 'monospace'), style: const TextStyle(fontFamily: 'monospace'),
); );
}, },
), ),
Text('Copyright © ${DateTime.now().year} Solsynth LLC'), Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
const Gap(16), const Gap(16),
Container( Container(
constraints: const BoxConstraints(maxWidth: 280), constraints: const BoxConstraints(maxWidth: 280),
child: Wrap( child: Wrap(
spacing: 4, spacing: 4,
runSpacing: 4, runSpacing: 4,
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
children: [ children: [
TextButton( TextButton(
style: denseButtonStyle, style: denseButtonStyle,
child: Text('appDetails').tr(), child: Text('appDetails').tr(),
onPressed: () async { onPressed: () async {
final info = await PackageInfo.fromPlatform(); final info = await PackageInfo.fromPlatform();
if (!context.mounted) return; if (!context.mounted) return;
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationName: 'Solian', applicationName: 'Solian',
applicationVersion: '${info.version}+${info.buildNumber}', applicationVersion: '${info.version}+${info.buildNumber}',
applicationLegalese: applicationLegalese:
'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.', 'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
applicationIcon: ClipRRect( applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)), borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Image.asset( child: Image.asset(
'assets/icon/icon-light-radius.png', 'assets/icon/icon-light-radius.png',
width: 60, width: 60,
height: 60, height: 60,
),
), ),
), );
); },
}, ),
), TextButton(
TextButton( style: denseButtonStyle,
style: denseButtonStyle, child: Text('termRelated').tr(),
child: Text('termRelated').tr(), onPressed: () {
onPressed: () { launchUrlString('https://solsynth.dev/terms');
launchUrlString('https://solsynth.dev/terms'); },
}, ),
), TextButton(
TextButton( style: denseButtonStyle,
style: denseButtonStyle, child: Text('serviceStatus').tr(),
child: Text('serviceStatus').tr(), onPressed: () {
onPressed: () { launchUrlString('https://status.solsynth.dev');
launchUrlString('https://status.solsynth.dev'); },
}, ),
), ],
], ),
).center(),
const Gap(16),
const Text(
'Open-sourced under AGPLv3',
style: TextStyle(
fontWeight: FontWeight.w300,
fontSize: 12,
),
), ),
).center(), ],
const Gap(16), ),
const Text(
'Open-sourced under AGPLv3',
style: TextStyle(
fontWeight: FontWeight.w300,
fontSize: 12,
),
),
],
), ),
); );
} }

View File

@ -0,0 +1,164 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/experience.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/types/account.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/universal_image.dart';
class AccountPopoverCard extends StatelessWidget {
final SnAccount data;
const AccountPopoverCard({super.key, required this.data});
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (data.banner.isNotEmpty)
Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: AspectRatio(
aspectRatio: 16 / 7,
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(data.banner),
fit: BoxFit.cover,
),
),
),
// Top padding
Gap(16),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AccountImage(
content: data.avatar,
radius: 20,
),
Gap(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(data.nick).bold(),
Text('@${data.name}').fontSize(13).opacity(0.75),
],
),
),
IconButton(
onPressed: () {
Navigator.pop(context);
GoRouter.of(context).pushNamed(
'accountProfilePage',
pathParameters: {'name': data.name},
);
},
icon: const Icon(Symbols.chevron_right),
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
),
const Gap(8)
],
).padding(horizontal: 16),
const Gap(16),
Wrap(
children: data.badges
.map(
(ele) => Tooltip(
richMessage: TextSpan(
children: [
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
if (ele.metadata['title'] != null)
TextSpan(
text: '\n${ele.metadata['title']}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: '\n'),
TextSpan(
text: DateFormat.yMEd().format(ele.createdAt),
),
],
),
child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3,
fill: 1,
),
),
)
.toList(),
).padding(horizontal: 24),
const Gap(8),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.star),
const Gap(8),
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
const Gap(8),
Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
const Gap(8),
Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 160),
child: LinearProgressIndicator(
value: calcLevelUpProgress(data.profile?.experience ?? 0),
borderRadius: BorderRadius.circular(8),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
).alignment(Alignment.centerLeft),
),
],
).padding(horizontal: 24),
FutureBuilder(
future: sn.client.get('/cgi/id/users/${data.name}/status'),
builder: (context, snapshot) {
final SnAccountStatusInfo? status =
snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null;
return Row(
children: [
Icon(
Symbols.circle,
fill: 1,
size: 16,
color: (status?.isOnline ?? false) ? Colors.green : Colors.grey,
).padding(all: 4),
const Gap(8),
Text(
status != null
? status.isOnline
? 'accountStatusOnline'.tr()
: 'accountStatusOffline'.tr()
: 'loading'.tr(),
),
if (status != null && !status.isOnline && status.lastSeenAt != null)
Text(
'accountStatusLastSeen'.tr(args: [
status.lastSeenAt != null
? RelativeTime(context).format(
status.lastSeenAt!.toLocal(),
)
: 'unknown',
]),
).padding(left: 6).opacity(0.75),
],
).padding(horizontal: 24);
},
),
// Bottom padding
const Gap(16),
],
);
}
}

View File

@ -15,6 +15,7 @@ class AttachmentList extends StatefulWidget {
final List<SnAttachment?> data; final List<SnAttachment?> data;
final bool bordered; final bool bordered;
final bool gridded; final bool gridded;
final bool columned;
final BoxFit fit; final BoxFit fit;
final double? maxHeight; final double? maxHeight;
final double? minWidth; final double? minWidth;
@ -26,6 +27,7 @@ class AttachmentList extends StatefulWidget {
required this.data, required this.data,
this.bordered = false, this.bordered = false,
this.gridded = false, this.gridded = false,
this.columned = false,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.maxHeight, this.maxHeight,
this.minWidth, this.minWidth,
@ -105,7 +107,10 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
if (widget.gridded) { final fullOfImage =
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
if (widget.gridded && fullOfImage) {
return Container( return Container(
margin: widget.padding ?? EdgeInsets.zero, margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -153,68 +158,109 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
return Container( if ((!fullOfImage && widget.gridded) || widget.columned) {
constraints: BoxConstraints(maxHeight: constraints.maxHeight), return Container(
child: ScrollConfiguration( margin: widget.padding ?? EdgeInsets.zero,
behavior: _AttachmentListScrollBehavior(), decoration: BoxDecoration(
child: ListView.separated( color: backgroundColor,
padding: widget.padding, border: Border(
shrinkWrap: true, top: borderSide,
itemCount: widget.data.length, bottom: borderSide,
itemBuilder: (context, idx) { ),
return Container( borderRadius: AttachmentList.kDefaultRadius,
constraints: constraints.copyWith(maxWidth: widget.maxWidth), ),
child: AspectRatio( child: ClipRRect(
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(), borderRadius: AttachmentList.kDefaultRadius,
child: GestureDetector( child: Column(
onTap: () { children: widget.data
if (widget.data[idx]?.mediaType != SnMediaType.image) return; .mapIndexed(
context.pushTransparentRoute( (idx, ele) => GestureDetector(
AttachmentZoomView( child: AspectRatio(
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(), aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
initialIndex: idx, child: Container(
heroTags: heroTags, constraints: constraints,
), child: AttachmentItem(
backgroundColor: Colors.black.withOpacity(0.7), data: ele,
rootNavigator: true, heroTag: heroTags[idx],
); fit: BoxFit.cover,
},
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], .expand((ele) => [ele, const Divider(height: 1)])
.toList()
..removeLast(),
),
),
);
}
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,
),
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(
Positioned( right: 8,
right: 8, bottom: 8,
bottom: 8, child: Chip(
child: Chip( label: Text('${idx + 1}/${widget.data.length}'),
label: Text('${idx + 1}/${widget.data.length}'), ),
), ),
), ],
], ),
), ),
), ),
), );
); },
}, separatorBuilder: (context, index) => const Gap(8),
separatorBuilder: (context, index) => const Gap(8), physics: const BouncingScrollPhysics(),
physics: const BouncingScrollPhysics(), scrollDirection: Axis.horizontal,
scrollDirection: Axis.horizontal, ),
), ),
), ),
); );

View File

@ -129,6 +129,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
bool _showDetail = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
@ -144,218 +146,351 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
onDismissed: () { onDismissed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
direction: DismissiblePageDismissDirection.down, direction: DismissiblePageDismissDirection.none,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
isFullScreen: true, isFullScreen: true,
child: Scaffold( child: GestureDetector(
body: Stack( behavior: HitTestBehavior.translucent,
children: [ child: Scaffold(
Builder(builder: (context) { backgroundColor: Colors.transparent,
if (widget.data.length == 1) { body: Stack(
final heroTag = widget.heroTags?.first ?? uuid.v4(); children: [
return Hero( Builder(builder: (context) {
tag: 'attachment-${widget.data.first.rid}-$heroTag', if (widget.data.length == 1) {
child: PhotoView( final heroTag = widget.heroTags?.first ?? uuid.v4();
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'), return Hero(
backgroundDecoration: BoxDecoration(color: Colors.transparent), tag: 'attachment-${widget.data.first.rid}-$heroTag',
imageProvider: UniversalImage.provider( child: PhotoView(
sn.getAttachmentUrl(widget.data.first.rid), key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
), backgroundDecoration: BoxDecoration(color: Colors.transparent),
), imageProvider: UniversalImage.provider(
); sn.getAttachmentUrl(widget.data.first.rid),
} ),
return PhotoViewGallery.builder(
pageController: _pageController,
scrollPhysics: const BouncingScrollPhysics(),
builder: (context, idx) {
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
return PhotoViewGalleryPageOptions(
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
),
heroAttributes: PhotoViewHeroAttributes(
tag: 'attachment-${widget.data.first.rid}-$heroTag',
), ),
); );
}, }
itemCount: widget.data.length,
loadingBuilder: (context, event) => Center( return PhotoViewGallery.builder(
child: SizedBox( pageController: _pageController,
width: 20.0, scrollPhysics: const BouncingScrollPhysics(),
height: 20.0, builder: (context, idx) {
child: CircularProgressIndicator( final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1), return PhotoViewGalleryPageOptions(
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
),
heroAttributes: PhotoViewHeroAttributes(
tag: 'attachment-${widget.data.first.rid}-$heroTag',
),
);
},
itemCount: widget.data.length,
loadingBuilder: (context, event) => Center(
child: SizedBox(
width: 20.0,
height: 20.0,
child: CircularProgressIndicator(
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
),
), ),
), ),
), backgroundDecoration: BoxDecoration(color: Colors.transparent),
backgroundDecoration: BoxDecoration(color: Colors.transparent), );
); }),
}), Align(
Align( alignment: Alignment.bottomCenter,
alignment: Alignment.bottomCenter, child: IgnorePointer(
child: IgnorePointer( child: Container(
child: Container( height: 300,
height: 300, decoration: BoxDecoration(
decoration: BoxDecoration( gradient: LinearGradient(
gradient: LinearGradient( begin: Alignment.bottomCenter,
begin: Alignment.bottomCenter, end: Alignment.topCenter,
end: Alignment.topCenter, colors: [
colors: [ Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surface, Colors.transparent,
Colors.transparent, ],
], ),
), ),
), ),
), ),
), ),
), Positioned(
Positioned( left: 16,
left: 16, right: 16,
right: 16, bottom: 16 + MediaQuery.of(context).padding.bottom,
bottom: 16 + MediaQuery.of(context).padding.bottom, child: Material(
child: Material( color: Colors.transparent,
color: Colors.transparent, child: Builder(builder: (context) {
child: Builder(builder: (context) { final ud = context.read<UserDirectoryProvider>();
final ud = context.read<UserDirectoryProvider>(); final item = widget.data.elementAt(
final item = widget.data.elementAt( widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0, );
); final account = ud.getAccountFromCache(item.accountId);
final account = ud.getAccountFromCache(item.accountId);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (item.accountId > 0) if (item.accountId > 0)
Row( Row(
children: [ children: [
IgnorePointer( IgnorePointer(
child: AccountImage( child: AccountImage(
content: account?.avatar, content: account?.avatar,
radius: 19, radius: 19,
),
),
const Gap(8),
Expanded(
child: IgnorePointer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'attachmentUploadBy'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
Text(
account?.nick ?? 'unknown'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
), ),
), ),
), const Gap(8),
if (widget.data.length > 1) Expanded(
IgnorePointer( child: IgnorePointer(
child: Text( child: Column(
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}', crossAxisAlignment: CrossAxisAlignment.start,
style: GoogleFonts.robotoMono(fontSize: 13), children: [
).padding(right: 8), Text(
), 'attachmentUploadBy'.tr(),
InkWell( style: Theme.of(context).textTheme.bodySmall,
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: _isDownloading
? null
: () => _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
child: Container(
padding: const EdgeInsets.all(6),
child: !_isDownloading
? !_isCompletedDownload
? const Icon(Symbols.save_alt)
: const Icon(Symbols.download_done)
: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _progressOfDownload,
strokeWidth: 3,
),
), ),
Text(
account?.nick ?? 'unknown'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
), ),
if (widget.data.length > 1)
IgnorePointer(
child: Text(
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
style: GoogleFonts.robotoMono(fontSize: 13),
).padding(right: 8),
),
InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: _isDownloading
? null
: () =>
_saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
child: Container(
padding: const EdgeInsets.all(6),
child: !_isDownloading
? !_isCompletedDownload
? const Icon(Symbols.save_alt)
: const Icon(Symbols.download_done)
: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _progressOfDownload,
strokeWidth: 3,
),
),
),
),
],
),
const Gap(4),
IgnorePointer(
child: Text(
item.alt,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
), ),
],
),
const Gap(4),
IgnorePointer(
child: Text(
item.alt,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
), ),
), ),
), const Gap(2),
const Gap(2), IgnorePointer(
IgnorePointer( child: Wrap(
child: Wrap( spacing: 6,
spacing: 6, children: [
children: [ if (item.metadata['exif'] == null)
if (item.metadata['exif'] == null) Text(
'#${item.rid}',
style: metaTextStyle,
),
if (item.metadata['exif']?['Model'] != null)
Text(
'attachmentShotOn'.tr(args: [
item.metadata['exif']?['Model'],
]),
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['ISO'] != null)
Text(
'ISO${item.metadata['exif']?['ISO']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Aperture'] != null)
Text(
'f/${item.metadata['exif']?['Aperture']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Megapixels'] != null &&
item.metadata['exif']?['Model'] != null)
Text(
'${item.metadata['exif']?['Megapixels']}MP',
style: metaTextStyle,
)
else
Text(
item.size.formatBytes(),
style: metaTextStyle,
),
if (item.metadata['width'] != null && item.metadata['height'] != null)
Text(
'${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle,
),
if (item.metadata['ratio'] != null)
Text(
(item.metadata['ratio'] as num).toStringAsFixed(2),
style: metaTextStyle,
),
Text( Text(
'#${item.rid}', item.mimetype,
style: metaTextStyle, style: metaTextStyle,
), ),
if (item.metadata['exif']?['Model'] != null) ],
Text( ),
'attachmentShotOn'.tr(args: [
item.metadata['exif']?['Model'],
]),
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['ISO'] != null)
Text(
'ISO${item.metadata['exif']?['ISO']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Aperture'] != null)
Text(
'f/${item.metadata['exif']?['Aperture']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Megapixels'] != null && item.metadata['exif']?['Model'] != null)
Text(
'${item.metadata['exif']?['Megapixels']}MP',
style: metaTextStyle,
)
else
Text(
'${item.size} Bytes',
style: metaTextStyle,
),
if (item.metadata['width'] != null && item.metadata['height'] != null)
Text(
'${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle,
),
if (item.metadata['ratio'] != null)
Text(
(item.metadata['ratio'] as num).toStringAsFixed(2),
style: metaTextStyle,
),
Text(
item.mimetype,
style: metaTextStyle,
),
],
), ),
), ],
], );
); }),
}), ),
), ),
), ],
], ),
), ),
onVerticalDragUpdate: (details) {
if (_showDetail) return;
if (details.delta.dy <= -40) {
_showDetail = true;
showModalBottomSheet(
context: context,
builder: (context) => _AttachmentZoomDetailPopup(
data: widget.data.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
),
).then((_) {
_showDetail = false;
});
}
},
onTap: () {
Navigator.of(context).pop();
},
),
);
}
}
class _AttachmentZoomDetailPopup extends StatelessWidget {
final SnAttachment data;
const _AttachmentZoomDetailPopup({required this.data});
@override
Widget build(BuildContext context) {
final ud = context.read<UserDirectoryProvider>();
final account = ud.getAccountFromCache(data.accountId);
const tableGap = TableRow(
children: [
TableCell(child: SizedBox(height: 16)),
TableCell(child: SizedBox(height: 16)),
],
);
return SizedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.info, size: 24),
const Gap(16),
Text('attachmentDetailInfo').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Expanded(
child: SingleChildScrollView(
child: Table(
columnWidths: {
0: IntrinsicColumnWidth(),
1: FlexColumnWidth(),
},
children: [
TableRow(
children: [
TableCell(
child: Text('attachmentUploadBy').tr().padding(right: 16),
),
TableCell(
child: Row(
children: [
if (data.accountId > 0)
AccountImage(
content: account?.avatar,
radius: 8,
),
const Gap(8),
Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()),
const Gap(8),
Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75),
],
),
),
],
),
tableGap,
TableRow(
children: [
TableCell(child: Text('Mimetype').padding(right: 16)),
TableCell(child: Text(data.mimetype)),
],
),
TableRow(
children: [
TableCell(child: Text('Size').padding(right: 16)),
TableCell(
child: Row(
children: [
Text(data.size.formatBytes()),
const Gap(12),
Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75),
],
)),
],
),
TableRow(
children: [
TableCell(child: Text('Name').padding(right: 16)),
TableCell(child: Text(data.name)),
],
),
if (data.hash.isNotEmpty)
TableRow(
children: [
TableCell(child: Text('Hash').padding(right: 16)),
TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
],
),
tableGap,
...(data.metadata['exif']?.keys.map((k) => TableRow(
children: [
TableCell(child: Text(k).padding(right: 16)),
TableCell(child: Text(data.metadata['exif'][k].toString())),
],
)) ??
[]),
],
).padding(horizontal: 20, vertical: 8),
),
),
],
), ),
); );
} }

View File

@ -1,14 +1,19 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:popover/popover.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_popover.dart';
import 'package:surface/widgets/attachment/attachment_list.dart'; import 'package:surface/widgets/attachment/attachment_list.dart';
import 'package:surface/widgets/context_menu.dart'; import 'package:surface/widgets/context_menu.dart';
import 'package:surface/widgets/link_preview.dart'; import 'package:surface/widgets/link_preview.dart';
@ -49,6 +54,8 @@ class ChatMessage extends StatelessWidget {
final dateFormatter = DateFormat('MM/dd HH:mm'); final dateFormatter = DateFormat('MM/dd HH:mm');
final cfg = context.read<ConfigProvider>();
return SwipeTo( return SwipeTo(
key: Key('chat-message-${data.id}'), key: Key('chat-message-${data.id}'),
iconOnLeftSwipe: Symbols.reply, iconOnLeftSwipe: Symbols.reply,
@ -95,8 +102,28 @@ class ChatMessage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (!isMerged && !isCompact) if (!isMerged && !isCompact)
AccountImage( GestureDetector(
content: user?.avatar, child: AccountImage(
content: user?.avatar,
),
onTap: () {
if (user == null) return;
showPopover(
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
transition: PopoverTransition.other,
bodyBuilder: (context) => SizedBox(
width: math.min(400, MediaQuery.of(context).size.width - 10),
child: AccountPopoverCard(
data: user,
),
),
direction: PopoverDirection.bottom,
arrowHeight: 5,
arrowWidth: 15,
arrowDxOffset: -190,
);
},
) )
else if (isMerged) else if (isMerged)
const Gap(40), const Gap(40),
@ -124,10 +151,13 @@ class ChatMessage extends StatelessWidget {
dateFormatter.format(data.createdAt.toLocal()), dateFormatter.format(data.createdAt.toLocal()),
).fontSize(13), ).fontSize(13),
], ],
), ).height(21),
if (isCompact) const Gap(8), if (isCompact) const Gap(8),
if (data.preload?.quoteEvent != null) if (data.preload?.quoteEvent != null)
StyledWidget(Container( StyledWidget(Container(
constraints: BoxConstraints(
maxWidth: 480,
),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all( border: Border.all(
@ -150,7 +180,12 @@ class ChatMessage extends StatelessWidget {
), ),
)).padding(bottom: 4, top: 4), )).padding(bottom: 4, top: 4),
switch (data.type) { switch (data.type) {
'messages.new' => _ChatMessageText(data: data), 'messages.new' => _ChatMessageText(
data: data,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
_ => _ChatMessageSystemNotify(data: data), _ => _ChatMessageSystemNotify(data: data),
}, },
], ],
@ -160,7 +195,10 @@ class ChatMessage extends StatelessWidget {
], ],
).opacity(isPending ? 0.5 : 1), ).opacity(isPending ? 0.5 : 1),
), ),
if (data.body['text'] != null && data.type == 'messages.new' && (data.body['text']?.isNotEmpty ?? false)) if (data.body['text'] != null &&
data.type == 'messages.new' &&
(data.body['text']?.isNotEmpty ?? false) &&
(cfg.prefs.getBool(kAppExpandChatLink) ?? true))
LinkPreviewWidget(text: data.body['text']!), LinkPreviewWidget(text: data.body['text']!),
if (data.preload?.attachments?.isNotEmpty ?? false) if (data.preload?.attachments?.isNotEmpty ?? false)
AttachmentList( AttachmentList(
@ -181,19 +219,75 @@ class ChatMessage extends StatelessWidget {
class _ChatMessageText extends StatelessWidget { class _ChatMessageText extends StatelessWidget {
final SnChatMessage data; final SnChatMessage data;
final Function(SnChatMessage)? onReply;
final Function(SnChatMessage)? onEdit;
final Function(SnChatMessage)? onDelete;
const _ChatMessageText({required this.data}); const _ChatMessageText({required this.data, this.onReply, this.onEdit, this.onDelete});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.read<UserProvider>();
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;
if (data.body['text'] != null && data.body['text'].isNotEmpty) { if (data.body['text'] != null && data.body['text'].isNotEmpty) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MarkdownTextContent( SelectionArea(
content: data.body['text'], contextMenuBuilder: (context, editableTextState) {
isSelectable: true, final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
isAutoWarp: true,
if (onReply != null) {
items.insert(
0,
ContextMenuButtonItem(
label: 'reply'.tr(),
onPressed: () {
ContextMenuController.removeAny();
onReply?.call(data);
},
),
);
}
if (isOwner && onEdit != null) {
items.insert(
1,
ContextMenuButtonItem(
label: 'edit'.tr(),
onPressed: () {
ContextMenuController.removeAny();
onEdit?.call(data);
},
),
);
}
if (isOwner && onDelete != null) {
items.insert(
2,
ContextMenuButtonItem(
label: 'delete'.tr(),
onPressed: () {
ContextMenuController.removeAny();
onDelete?.call(data);
},
),
);
}
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: items,
);
},
child: Container(
constraints: const BoxConstraints(maxWidth: 480),
child: MarkdownTextContent(
content: data.body['text'],
isAutoWarp: true,
),
),
), ),
if (data.updatedAt != data.createdAt) if (data.updatedAt != data.createdAt)
Text( Text(

View File

@ -46,8 +46,20 @@ class ChatMessageInputState extends State<ChatMessageInput> {
setState(() => _replyingMessage = value); setState(() => _replyingMessage = value);
} }
void setInitialText(String? value) {
_contentController.text = value ?? '';
setState(() {});
}
void setInitialAttachments(List<PostWriteMedia>? value) {
_attachments.addAll(value ?? []);
setState(() {});
}
void setEdit(SnChatMessage? value) { void setEdit(SnChatMessage? value) {
_contentController.text = value?.body['text'] ?? ''; _contentController.text = value?.body['text'] ?? '';
_attachments.clear();
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
setState(() => _editingMessage = value); setState(() => _editingMessage = value);
} }
@ -101,7 +113,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
}, },
); );
_attachments[i] = PostWriteMedia(item); setState(() {
_attachments[i] = PostWriteMedia(item);
});
} }
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
@ -113,7 +127,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
// Send the message // Send the message
// NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type // NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type
widget.controller.sendMessage( widget.controller.sendMessage(
'messages.new', _editingMessage != null ? 'messages.edit' : 'messages.new',
_contentController.text, _contentController.text,
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(), attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
relatedId: _editingMessage?.id, relatedId: _editingMessage?.id,
@ -197,6 +211,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
InkWell( InkWell(
child: Text('cancel'.tr()), child: Text('cancel'.tr()),
onTap: () { onTap: () {
_attachments.clear();
setState(() => _replyingMessage = null); setState(() => _replyingMessage = null);
}, },
), ),
@ -236,6 +251,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
InkWell( InkWell(
child: Text('cancel'.tr()), child: Text('cancel'.tr()),
onTap: () { onTap: () {
_attachments.clear();
_contentController.clear(); _contentController.clear();
setState(() => _editingMessage = null); setState(() => _editingMessage = null);
}, },

View File

@ -1,7 +1,10 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart'; import 'package:surface/providers/websocket.dart';
@ -11,50 +14,62 @@ class ConnectionIndicator extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ws = context.watch<WebSocketProvider>(); final ws = context.watch<WebSocketProvider>();
final cfg = context.watch<ConfigProvider>();
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
return ListenableBuilder( return ListenableBuilder(
listenable: ws, listenable: ws,
builder: (context, _) { builder: (context, _) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized;
return GestureDetector( return IgnorePointer(
child: Container( ignoring: !show,
padding: EdgeInsets.only( child: Center(
bottom: 8, child: GestureDetector(
top: MediaQuery.of(context).padding.top + 8, child: Material(
left: 24, elevation: 2,
right: 24, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
color: Theme.of(context).colorScheme.secondaryContainer,
child: ua.isAuthorized
? Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (ws.isBusy)
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
else if (!ws.isConnected)
Text('serverDisconnected')
.tr()
.textColor(Theme.of(context).colorScheme.onSecondaryContainer)
else
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
const Gap(8),
if (ws.isBusy)
const CircularProgressIndicator(strokeWidth: 2.5)
.width(12)
.height(12)
.padding(horizontal: 4, right: 4)
else if (!ws.isConnected)
const Icon(Symbols.power_off, size: 18)
else
const Icon(Symbols.power, size: 18),
],
).padding(horizontal: 8, vertical: 4)
: const SizedBox.shrink(),
).opacity(show ? 1 : 0, animate: true).animate(
const Duration(milliseconds: 300),
Curves.easeInOut,
),
onTap: () {
if (!ws.isConnected && !ws.isBusy) {
ws.connect();
}
},
), ),
color: Theme.of(context).colorScheme.secondaryContainer, ).padding(left: marginLeft),
child: ua.isAuthorized
? Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (ws.isBusy)
Text('serverConnecting').tr().textColor(
Theme.of(context).colorScheme.onSecondaryContainer)
else if (!ws.isConnected)
Text('serverDisconnected').tr().textColor(
Theme.of(context).colorScheme.onSecondaryContainer),
],
)
: const SizedBox.shrink(),
)
.height(
(ws.isBusy || !ws.isConnected) && ua.isAuthorized
? MediaQuery.of(context).padding.top + 36
: 0,
animate: true)
.animate(
const Duration(milliseconds: 300),
Curves.easeInOut,
),
onTap: () {
if (!ws.isConnected && !ws.isBusy) {
ws.connect();
}
},
); );
}, },
); );

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/config.dart';
class ContextMenuArea extends StatelessWidget { class ContextMenuArea extends StatelessWidget {
final ContextMenu contextMenu; final ContextMenu contextMenu;
@ -22,13 +23,12 @@ class ContextMenuArea extends StatelessWidget {
return Listener( return Listener(
onPointerDown: (event) { onPointerDown: (event) {
mousePosition = event.position; mousePosition = event.position;
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); final cfg = context.read<ConfigProvider>();
if (!isCollapseDrawer) { if (!cfg.drawerIsCollapsed) {
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
// Leave padding for side navigation // Leave padding for side navigation
mousePosition = isExpandDrawer mousePosition = cfg.drawerIsExpanded
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2) ? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2); : mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
} }
}, },
child: GestureDetector( child: GestureDetector(

View File

@ -2,7 +2,9 @@ import 'dart:math' as math;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher_string.dart';
extension AppPromptExtension on BuildContext { extension AppPromptExtension on BuildContext {
void showSnackbar(String content, {SnackBarAction? action}) { void showSnackbar(String content, {SnackBarAction? action}) {
@ -111,7 +113,34 @@ extension AppPromptExtension on BuildContext {
context: this, context: this,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: Text('dialogError').tr(), 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: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), onPressed: () => Navigator.pop(ctx),
@ -128,17 +157,7 @@ extension ByteFormatter on int {
if (this == 0) return '0 Bytes'; if (this == 0) return '0 Bytes';
const k = 1024; const k = 1024;
final dm = decimals < 0 ? 0 : decimals; final dm = decimals < 0 ? 0 : decimals;
final sizes = [ final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB'
];
final i = (math.log(this) / math.log(k)).floor().toInt(); final i = (math.log(this) / math.log(k)).floor().toInt();
return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}'; return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
} }

View File

@ -7,12 +7,11 @@ import 'package:marquee/marquee.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/link_preview.dart';
import 'package:surface/types/link.dart'; import 'package:surface/types/link.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import '../providers/link_preview.dart';
class LinkPreviewWidget extends StatefulWidget { class LinkPreviewWidget extends StatefulWidget {
final String text; final String text;
@ -81,8 +80,9 @@ class _LinkPreviewEntry extends StatelessWidget {
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
meta.image!, meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
), ),
@ -94,11 +94,14 @@ class _LinkPreviewEntry extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if (meta.icon?.isNotEmpty ?? false) if (meta.icon?.isNotEmpty ?? false)
StyledWidget( SizedBox(
meta.icon!.endsWith('.svg') width: 36,
? SvgPicture.network(meta.icon!) height: 36,
child: meta.icon!.endsWith('.svg')
? SvgPicture.network(meta.icon!, width: 36, height: 36)
: UniversalImage( : UniversalImage(
meta.icon!, meta.icon!,
noErrorWidget: true,
width: 36, width: 36,
height: 36, height: 36,
cacheHeight: 36, cacheHeight: 36,

View File

@ -17,23 +17,24 @@ import 'attachment/attachment_zoom.dart';
class MarkdownTextContent extends StatelessWidget { class MarkdownTextContent extends StatelessWidget {
final String content; final String content;
final bool isSelectable;
final bool isAutoWarp; final bool isAutoWarp;
final bool isEnlargeSticker; final bool isEnlargeSticker;
final TextScaler? textScaler; final TextScaler? textScaler;
final Color? textColor;
final List<SnAttachment?>? attachments; final List<SnAttachment?>? attachments;
const MarkdownTextContent({ const MarkdownTextContent({
super.key, super.key,
required this.content, required this.content,
this.isSelectable = false,
this.isAutoWarp = false, this.isAutoWarp = false,
this.isEnlargeSticker = false, this.isEnlargeSticker = false,
this.textScaler, this.textScaler,
this.textColor,
this.attachments, this.attachments,
}); });
Widget _buildContent(BuildContext context) { @override
Widget build(BuildContext context) {
return Markdown( return Markdown(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@ -43,6 +44,7 @@ class MarkdownTextContent extends StatelessWidget {
Theme.of(context), Theme.of(context),
).copyWith( ).copyWith(
textScaler: textScaler, textScaler: textScaler,
p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null,
blockquote: TextStyle( blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
@ -68,8 +70,7 @@ class MarkdownTextContent extends StatelessWidget {
), ),
code: GoogleFonts.robotoMono(height: 1), code: GoogleFonts.robotoMono(height: 1),
), ),
builders: { builders: {},
},
softLineBreak: true, softLineBreak: true,
extensionSet: markdown.ExtensionSet( extensionSet: markdown.ExtensionSet(
<markdown.BlockSyntax>[ <markdown.BlockSyntax>[
@ -144,7 +145,7 @@ class MarkdownTextContent extends StatelessWidget {
); );
case 'attachments': case 'attachments':
final attachment = attachments?.firstWhere( final attachment = attachments?.firstWhere(
(ele) => ele?.rid == segments[1], (ele) => ele?.rid == segments[1],
orElse: () => null, orElse: () => null,
); );
if (attachment != null) { if (attachment != null) {
@ -202,14 +203,6 @@ class MarkdownTextContent extends StatelessWidget {
}, },
); );
} }
@override
Widget build(BuildContext context) {
if (isSelectable) {
return SelectionArea(child: _buildContent(context));
}
return _buildContent(context);
}
} }
class _UserNameCardInlineSyntax extends markdown.InlineSyntax { class _UserNameCardInlineSyntax extends markdown.InlineSyntax {

View File

@ -1,9 +1,13 @@
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/version_label.dart'; import 'package:surface/widgets/version_label.dart';
@ -28,8 +32,9 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final nav = context.watch<NavigationProvider>(); final nav = context.watch<NavigationProvider>();
final cfg = context.watch<ConfigProvider>();
final backgroundColor = ResponsiveBreakpoints.of(context).largerThan(TABLET) ? Colors.transparent : null; final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
return ListenableBuilder( return ListenableBuilder(
listenable: nav, listenable: nav,
@ -44,6 +49,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
selectedIndex: nav.currentIndex, selectedIndex: nav.currentIndex,
children: [ children: [
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS) && !cfg.drawerIsExpanded)
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: WindowTitleBarBox(),
),
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -18,9 +18,7 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
.read<NavigationProvider>()
.autoDetectIndex(GoRouter.maybeOf(context));
}); });
} }
@ -31,36 +29,39 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
return ListenableBuilder( return ListenableBuilder(
listenable: nav, listenable: nav,
builder: (context, _) { builder: (context, _) {
final destinations = final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
nav.destinations.where((ele) => ele.isPinned).toList();
return NavigationRail( return SizedBox(
selectedIndex: nav.currentIndex, width: 80,
destinations: [ child: NavigationRail(
...destinations.where((ele) => ele.isPinned).map((ele) { selectedIndex:
return NavigationRailDestination( nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
icon: ele.icon, destinations: [
label: Text(ele.label).tr(), ...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( trailing: Expanded(
IconButton( child: Align(
icon: const Icon(Symbols.menu), alignment: Alignment.bottomCenter,
onPressed: () { child: StyledWidget(
Scaffold.of(context).openDrawer(); IconButton(
}, icon: const Icon(Symbols.menu),
), onPressed: () {
).padding(bottom: 16), 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);
},
); );
}, },
); );

View File

@ -1,51 +1,94 @@
import 'dart:io'; import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/connection_indicator.dart'; import 'package:surface/widgets/connection_indicator.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_background.dart'; import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_bottom_navigation.dart'; import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
import 'package:surface/widgets/navigation/app_drawer_navigation.dart'; import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
import 'package:surface/widgets/navigation/app_rail_navigation.dart'; import 'package:surface/widgets/navigation/app_rail_navigation.dart';
import 'package:surface/widgets/notify_indicator.dart';
final globalRootScaffoldKey = GlobalKey<ScaffoldState>(); final globalRootScaffoldKey = GlobalKey<ScaffoldState>();
class AppPageScaffold extends StatelessWidget { class AppScaffold extends StatelessWidget {
final String? title;
final Widget? body; final Widget? body;
final bool showAppBar; final PreferredSizeWidget? bottomNavigationBar;
final bool showBottomNavigation; final PreferredSizeWidget? bottomSheet;
final Drawer? drawer;
final Widget? endDrawer;
final FloatingActionButtonAnimator? floatingActionButtonAnimator;
final FloatingActionButtonLocation? floatingActionButtonLocation;
final Widget? floatingActionButton;
final AppBar? appBar;
final DrawerCallback? onDrawerChanged;
final DrawerCallback? onEndDrawerChanged;
const AppPageScaffold({ const AppScaffold({
super.key, super.key,
this.title, this.appBar,
this.body, this.body,
this.showAppBar = true, this.floatingActionButton,
this.showBottomNavigation = false, this.floatingActionButtonLocation,
this.floatingActionButtonAnimator,
this.bottomNavigationBar,
this.bottomSheet,
this.drawer,
this.endDrawer,
this.onDrawerChanged,
this.onEndDrawerChanged,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final state = GoRouter.maybeOf(context); final appBarHeight = appBar?.preferredSize.height ?? 0;
final routeName = state?.routerDelegate.currentConfiguration.last.route.name; final safeTop = MediaQuery.of(context).padding.top;
final autoTitle = state != null ? 'screen${routeName?.capitalize()}' : 'screen';
return Scaffold( return Scaffold(
appBar: showAppBar extendBody: true,
? AppBar( extendBodyBehindAppBar: true,
title: Text(title ?? autoTitle.tr()), backgroundColor: Theme.of(context).scaffoldBackgroundColor,
) body: SizedBox.expand(
: null, child: AppBackground(
body: body, child: Column(
children: [
IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)),
if (body != null) Expanded(child: body!),
],
),
),
),
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
drawer: drawer,
endDrawer: endDrawer,
floatingActionButton: floatingActionButton,
floatingActionButtonAnimator: floatingActionButtonAnimator,
floatingActionButtonLocation: floatingActionButtonLocation,
onDrawerChanged: onDrawerChanged,
onEndDrawerChanged: onEndDrawerChanged,
);
}
}
class PageBackButton extends StatelessWidget {
const PageBackButton({super.key});
@override
Widget build(BuildContext context) {
return BackButton(
onPressed: () {
GoRouter.of(context).pop();
},
); );
} }
} }
@ -57,10 +100,11 @@ class AppRootScaffold extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.watch<ConfigProvider>();
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); final isCollapseDrawer = cfg.drawerIsCollapsed;
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET); final isExpandedDrawer = cfg.drawerIsExpanded;
final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name; final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name;
final isShowBottomNavigation = NavigationProvider.kShowBottomNavScreen.contains(routeName) final isShowBottomNavigation = NavigationProvider.kShowBottomNavScreen.contains(routeName)
@ -81,7 +125,7 @@ class AppRootScaffold extends StatelessWidget {
), ),
), ),
), ),
child: isExpandDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(), child: isExpandedDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(),
), ),
Expanded(child: body), Expanded(child: body),
], ],
@ -95,62 +139,68 @@ class AppRootScaffold extends StatelessWidget {
iconMouseDown: Theme.of(context).colorScheme.primary, iconMouseDown: Theme.of(context).colorScheme.primary,
); );
return AppBackground( final safeTop = MediaQuery.of(context).padding.top;
isRoot: true, final safeBottom = MediaQuery.of(context).padding.bottom;
child: Scaffold(
key: globalRootScaffoldKey, return Scaffold(
body: Column( key: globalRootScaffoldKey,
children: [ backgroundColor: Theme.of(context).colorScheme.surface,
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) body: Stack(
Container( children: [
decoration: BoxDecoration( Column(
border: Border( children: [
bottom: BorderSide( if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
color: Theme.of(context).dividerColor, WindowTitleBarBox(
width: 1 / devicePixelRatio, child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
),
),
child: MoveWindow(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
Text(
'Solar Network',
style: GoogleFonts.spaceGrotesk(),
).padding(horizontal: 12, vertical: 5),
if (!Platform.isMacOS)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(child: MoveWindow()),
Row(
children: [
MinimizeWindowButton(colors: windowButtonColor),
MaximizeWindowButton(colors: windowButtonColor),
CloseWindowButton(colors: windowButtonColor),
],
),
],
),
],
),
), ),
), ),
), ),
child: Row( Expanded(child: innerWidget),
crossAxisAlignment: CrossAxisAlignment.center, ],
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start, ),
children: [ Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
WindowTitleBarBox( if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE))
child: MoveWindow( Positioned(bottom: safeBottom > 0 ? safeBottom : 16, left: 0, right: 0, child: ConnectionIndicator())
child: Text( else
'Solar Network', Positioned(top: safeTop > 0 ? safeTop : 16, left: 0, right: 0, child: ConnectionIndicator()),
style: GoogleFonts.spaceGrotesk(), ],
).padding(horizontal: 12, vertical: 5),
),
),
if (!Platform.isMacOS)
Expanded(
child: WindowTitleBarBox(
child: Row(
children: [
Expanded(child: MoveWindow()),
Row(
children: [
MinimizeWindowButton(colors: windowButtonColor),
MaximizeWindowButton(colors: windowButtonColor),
CloseWindowButton(colors: windowButtonColor),
],
),
],
),
),
),
],
),
),
ConnectionIndicator(),
Expanded(child: innerWidget),
],
),
drawer: !isExpandDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null,
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
), ),
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null,
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
); );
} }
} }

View File

@ -0,0 +1,184 @@
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/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';
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 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: 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);
}
},
),
);
});
}
}

View File

@ -1,10 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart'; import 'package:file_saver/file_saver.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@ -34,6 +36,7 @@ import 'package:surface/widgets/post/post_meta_editor.dart';
import 'package:surface/widgets/post/post_reaction.dart'; import 'package:surface/widgets/post/post_reaction.dart';
import 'package:surface/widgets/post/publisher_popover.dart'; import 'package:surface/widgets/post/publisher_popover.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:xml/xml.dart';
class PostItem extends StatelessWidget { class PostItem extends StatelessWidget {
final SnPost data; final SnPost data;
@ -113,7 +116,7 @@ class PostItem extends StatelessWidget {
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
); );
} else { } else {
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}', file: imageFile); await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile);
} }
await imageFile.delete(); await imageFile.delete();
@ -186,6 +189,7 @@ class PostItem extends StatelessWidget {
), ),
), ),
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8), Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
_PostBottomAction( _PostBottomAction(
data: data, data: data,
showComments: showComments, showComments: showComments,
@ -203,6 +207,8 @@ class PostItem extends StatelessWidget {
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article') ?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
.toList(); .toList();
final cfg = context.read<ConfigProvider>();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@ -256,16 +262,16 @@ class PostItem extends StatelessWidget {
AttachmentList( AttachmentList(
data: displayableAttachments!, data: displayableAttachments!,
bordered: true, bordered: true,
gridded: true,
maxHeight: showFullPost ? null : 480, maxHeight: showFullPost ? null : 480,
minWidth: 640, maxWidth: MediaQuery.of(context).size.width - 20,
fit: showFullPost ? BoxFit.cover : BoxFit.contain, fit: showFullPost ? BoxFit.cover : BoxFit.contain,
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
), ),
if (data.body['content'] != null) if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
LinkPreviewWidget( LinkPreviewWidget(
text: data.body['content'], text: data.body['content'],
).padding(horizontal: 4), ).padding(horizontal: 4),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
Container( Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Column( child: Column(
@ -344,7 +350,7 @@ class PostShareImageWidget extends StatelessWidget {
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false)) if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
StyledWidget(AttachmentList( StyledWidget(AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
gridded: true, columned: true,
)).padding(horizontal: 16, bottom: 8), )).padding(horizontal: 16, bottom: 8),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -816,6 +822,22 @@ class _PostContentHeader extends StatelessWidget {
}, },
), ),
const PopupMenuDivider(), const PopupMenuDivider(),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.book_4_spark),
const Gap(16),
Text('postGetInsight').tr(),
],
),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => _PostGetInsightSheet(postId: data.id),
);
},
),
const PopupMenuDivider(),
PopupMenuItem( PopupMenuItem(
onTap: onShare, onTap: onShare,
child: Row( child: Row(
@ -879,13 +901,18 @@ class _PostContentBody extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (data.body['content'] == null) return const SizedBox.shrink(); if (data.body['content'] == null) return const SizedBox.shrink();
return MarkdownTextContent( final content = MarkdownTextContent(
isSelectable: isSelectable, isAutoWarp: data.type == 'story',
isEnlargeSticker: true, isEnlargeSticker: true,
textScaler: isEnlarge ? TextScaler.linear(1.1) : null, textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: data.body['content'], content: data.body['content'],
attachments: data.preload?.attachments, attachments: data.preload?.attachments,
); );
if (isSelectable) {
return SelectionArea(child: content);
}
return content;
} }
} }
@ -1100,6 +1127,95 @@ 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;
Future<void> _fetchComments() async {
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();
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: Theme.of(context).colorScheme.surfaceContainerHigh,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
builder: (context) => PostCommentListPopup(
postId: widget.data.id,
commentCount: widget.data.metric.replyCount,
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('postFeaturedComment', style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 16)).tr(),
const Gap(4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircleAvatar(
radius: 12,
backgroundImage: UniversalImage.provider(
_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 { class _PostAbuseReportDialog extends StatefulWidget {
final SnPost data; final SnPost data;
@ -1175,3 +1291,96 @@ class _PostAbuseReportDialogState extends State<_PostAbuseReportDialog> {
); );
} }
} }
class _PostGetInsightSheet extends StatefulWidget {
final int postId;
const _PostGetInsightSheet({required this.postId});
@override
State<_PostGetInsightSheet> createState() => _PostGetInsightSheetState();
}
class _PostGetInsightSheetState extends State<_PostGetInsightSheet> {
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;
final document = XmlDocument.parse(out);
_thinkingProcess = document.getElement('think')?.innerText.trim();
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),
],
),
),
),
],
);
}
}

View File

@ -25,7 +25,7 @@ class PostMiniEditor extends StatefulWidget {
} }
class _PostMiniEditorState extends State<PostMiniEditor> { class _PostMiniEditorState extends State<PostMiniEditor> {
final PostWriteController _writeController = PostWriteController(); final PostWriteController _writeController = PostWriteController(doLoadFromTemporary: false);
bool _isFetching = false; bool _isFetching = false;

View File

@ -1,5 +1,6 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -12,6 +13,7 @@ import 'package:surface/widgets/dialog.dart';
class PostReactionPopup extends StatefulWidget { class PostReactionPopup extends StatefulWidget {
final SnPost data; final SnPost data;
final Function(Map<String, int> value, int attr, int delta)? onChanged; final Function(Map<String, int> value, int attr, int delta)? onChanged;
const PostReactionPopup({super.key, required this.data, this.onChanged}); const PostReactionPopup({super.key, required this.data, this.onChanged});
@override @override
@ -59,6 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
); );
} }
} }
HapticFeedback.heavyImpact();
} catch (err) { } catch (err) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
if (context.mounted) context.showErrorDialog(err); if (context.mounted) context.showErrorDialog(err);
@ -84,9 +87,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
children: [ children: [
const Icon(Symbols.mood, size: 24), const Icon(Symbols.mood, size: 24),
const Gap(16), const Gap(16),
Text('postReactions') Text('postReactions').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
.tr()
.textStyle(Theme.of(context).textTheme.titleLarge!),
], ],
).padding(horizontal: 20, top: 16, bottom: 12), ).padding(horizontal: 20, top: 16, bottom: 12),
Container( Container(
@ -102,9 +103,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
Text('postReactionDownvote').plural(widget.data.totalDownvote), Text('postReactionDownvote').plural(widget.data.totalDownvote),
const Gap(24), const Gap(24),
Icon( Icon(
widget.data.totalUpvote >= widget.data.totalDownvote widget.data.totalUpvote >= widget.data.totalDownvote ? Symbols.trending_up : Symbols.trending_down,
? Symbols.trending_up
: Symbols.trending_down,
size: 16, size: 16,
), ),
const Gap(8), const Gap(8),

View File

@ -5,10 +5,9 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:surface/providers/config.dart';
// Keep this import to make the web image render work // 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:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:surface/providers/config.dart';
class UniversalImage extends StatelessWidget { class UniversalImage extends StatelessWidget {
final String url; final String url;
@ -55,17 +54,20 @@ class UniversalImage extends StatelessWidget {
? null ? null
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { : (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child; if (loadingProgress == null) return child;
return Center( return Container(
child: TweenAnimationBuilder( constraints: BoxConstraints(maxHeight: 80),
tween: Tween( child: Center(
begin: 0, child: TweenAnimationBuilder(
end: loadingProgress.expectedTotalBytes != null tween: Tween(
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! begin: 0,
: 0, end: loadingProgress.expectedTotalBytes != null
), ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
duration: const Duration(milliseconds: 300), : 0,
builder: (context, value, _) => CircularProgressIndicator( ),
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null, duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
),
), ),
), ),
); );

View File

@ -8,11 +8,13 @@ import Foundation
import bitsdojo_window_macos import bitsdojo_window_macos
import connectivity_plus import connectivity_plus
import device_info_plus import device_info_plus
import file_picker
import file_saver import file_saver
import file_selector_macos import file_selector_macos
import firebase_analytics import firebase_analytics
import firebase_core import firebase_core
import firebase_messaging import firebase_messaging
import flutter_inappwebview_macos
import flutter_udid import flutter_udid
import flutter_webrtc import flutter_webrtc
import gal import gal
@ -35,11 +37,13 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))

View File

@ -8,63 +8,65 @@ PODS:
- FlutterMacOS - FlutterMacOS
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- file_picker (0.0.1):
- FlutterMacOS
- file_saver (0.0.1): - file_saver (0.0.1):
- FlutterMacOS - FlutterMacOS
- file_selector_macos (0.0.1): - file_selector_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- Firebase/Analytics (11.4.0): - Firebase/Analytics (11.6.0):
- Firebase/Core - Firebase/Core
- Firebase/Core (11.4.0): - Firebase/Core (11.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseAnalytics (~> 11.4.0) - FirebaseAnalytics (~> 11.6.0)
- Firebase/CoreOnly (11.4.0): - Firebase/CoreOnly (11.6.0):
- FirebaseCore (= 11.4.0) - FirebaseCore (~> 11.6.0)
- Firebase/Messaging (11.4.0): - Firebase/Messaging (11.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.4.0) - FirebaseMessaging (~> 11.6.0)
- firebase_analytics (11.3.6): - firebase_analytics (11.4.1):
- Firebase/Analytics (= 11.4.0) - Firebase/Analytics (= 11.6.0)
- firebase_core - firebase_core
- FlutterMacOS - FlutterMacOS
- firebase_core (3.9.0): - firebase_core (3.10.1):
- Firebase/CoreOnly (~> 11.4.0) - Firebase/CoreOnly (~> 11.6.0)
- FlutterMacOS - FlutterMacOS
- firebase_messaging (15.1.6): - firebase_messaging (15.2.1):
- Firebase/CoreOnly (~> 11.4.0) - Firebase/CoreOnly (~> 11.6.0)
- Firebase/Messaging (~> 11.4.0) - Firebase/Messaging (~> 11.6.0)
- firebase_core - firebase_core
- FlutterMacOS - FlutterMacOS
- FirebaseAnalytics (11.4.0): - FirebaseAnalytics (11.6.0):
- FirebaseAnalytics/AdIdSupport (= 11.4.0) - FirebaseAnalytics/AdIdSupport (= 11.6.0)
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseAnalytics/AdIdSupport (11.4.0): - FirebaseAnalytics/AdIdSupport (11.6.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.4.0) - GoogleAppMeasurement (= 11.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseCore (11.4.0): - FirebaseCore (11.6.0):
- FirebaseCoreInternal (~> 11.0) - FirebaseCoreInternal (~> 11.6.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0) - GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreInternal (11.6.0): - FirebaseCoreInternal (11.6.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseInstallations (11.4.0): - FirebaseInstallations (11.6.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseMessaging (11.4.0): - FirebaseMessaging (11.6.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0) - GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -72,31 +74,34 @@ PODS:
- GoogleUtilities/Reachability (~> 8.0) - GoogleUtilities/Reachability (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- flutter_inappwebview_macos (0.0.1):
- FlutterMacOS
- OrderedSet (~> 6.0.3)
- flutter_udid (0.0.1): - flutter_udid (0.0.1):
- FlutterMacOS - FlutterMacOS
- SAMKeychain - SAMKeychain
- flutter_webrtc (0.12.2): - flutter_webrtc (0.12.6):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- GoogleAppMeasurement (11.4.0): - GoogleAppMeasurement (11.6.0):
- GoogleAppMeasurement/AdIdSupport (= 11.4.0) - GoogleAppMeasurement/AdIdSupport (= 11.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/AdIdSupport (11.4.0): - GoogleAppMeasurement/AdIdSupport (11.6.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0) - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/WithoutAdIdSupport (11.4.0): - GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
@ -134,7 +139,7 @@ PODS:
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- in_app_review (2.0.0): - in_app_review (2.0.0):
- FlutterMacOS - FlutterMacOS
- livekit_client (2.3.4): - livekit_client (2.3.5):
- flutter_webrtc - flutter_webrtc
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
@ -149,6 +154,7 @@ PODS:
- nanopb/encode (= 3.30910.0) - nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0) - nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0) - nanopb/encode (3.30910.0)
- OrderedSet (6.0.3)
- package_info_plus (0.0.1): - package_info_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- pasteboard (0.0.1): - pasteboard (0.0.1):
@ -181,11 +187,13 @@ DEPENDENCIES:
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`) - croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/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_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`) - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`) - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`) - flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
@ -218,6 +226,7 @@ SPEC REPOS:
- GoogleDataTransport - GoogleDataTransport
- GoogleUtilities - GoogleUtilities
- nanopb - nanopb
- OrderedSet
- PromisesObjC - PromisesObjC
- SAMKeychain - SAMKeychain
- WebRTC-SDK - WebRTC-SDK
@ -231,6 +240,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos :path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
device_info_plus: device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
file_picker:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
file_saver: file_saver:
:path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
file_selector_macos: file_selector_macos:
@ -241,6 +252,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
firebase_messaging: firebase_messaging:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos :path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
flutter_inappwebview_macos:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
flutter_udid: flutter_udid:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos :path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
flutter_webrtc: flutter_webrtc:
@ -285,30 +298,33 @@ SPEC CHECKSUMS:
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
croppy: 25a638bd7d05411d8c697f481568f261037694fc croppy: 25a638bd7d05411d8c697f481568f261037694fc
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
file_saver: 44e6fbf666677faf097302460e214e977fdd977b file_saver: 44e6fbf666677faf097302460e214e977fdd977b
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
firebase_analytics: a80b3d6645f2f12d626fde928b61dae12e5ea2ef firebase_analytics: 91efc58e8e37964469fdd59ad11ba36bc97e75d6
firebase_core: 1dfe1f4d02ad78be0277e320aa3d8384cf46231f firebase_core: 75e003524565fb5bd80c9960bc5892e8475821cd
firebase_messaging: 61f678060b69a7ae1013e3a939ec8e1c56ef6fcf firebase_messaging: 082a385eb98b5bb843a566cb30404859c4bd6e25
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49 FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52 flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
flutter_webrtc: 53c9e1285ab32dfb58afb1e1471416a877e23d7a flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
livekit_client: b7ab91e79e657d7d40da16cb2f90d517cb72d406 livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _flutterfire_internals name: _flutterfire_internals
sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe sha256: e4f2a7ef31b0ab2c89d2bde35ef3e6e6aff1dce5e66069c6540b0e9cfe33ee6b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.48" version: "1.3.50"
_macros: _macros:
dependency: transitive dependency: transitive
description: dart description: dart
@ -266,10 +266,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: connectivity_plus name: connectivity_plus
sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.1" version: "6.1.2"
connectivity_plus_platform_interface: connectivity_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -290,10 +290,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: croppy name: croppy
sha256: "14bb40fd6c1771b093a907ddbf24df9aa49a4e6e379dd630602eb446e30ec629" sha256: bf99b00023df0d7d047e04d27d496d87cbefd968f578d0bd30f342ff75570a12
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.3"
cross_file: cross_file:
dependency: "direct main" dependency: "direct main"
description: description:
@ -338,10 +338,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.7" version: "2.3.8"
dart_webrtc: dart_webrtc:
dependency: "direct main" dependency: "direct main"
description: description:
@ -354,18 +354,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dbus name: dbus
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.11"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
name: device_info_plus name: device_info_plus
sha256: "4fa68e53e26ab17b70ca39f072c285562cfc1589df5bb1e9295db90f6645f431" sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.2.0" version: "11.2.2"
device_info_plus_platform_interface: device_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -378,10 +378,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio name: dio
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.7.0" version: "5.8.0+1"
dio_smart_retry: dio_smart_retry:
dependency: "direct main" dependency: "direct main"
description: description:
@ -394,10 +394,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dio_web_adapter name: dio_web_adapter
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.1.0"
dismissible_page: dismissible_page:
dependency: "direct main" dependency: "direct main"
description: description:
@ -418,10 +418,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: easy_localization name: easy_localization
sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7+1"
easy_localization_loader: easy_localization_loader:
dependency: "direct main" dependency: "direct main"
description: description:
@ -490,10 +490,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204 sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.7" version: "8.3.1"
file_saver: file_saver:
dependency: "direct main" dependency: "direct main"
description: description:
@ -538,34 +538,34 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_analytics name: firebase_analytics
sha256: "366140abb55418ea23060b779893fa997c2d8e1974a4d1cc4d9590933b65c5fd" sha256: eac382bbcd5ae78c1d1ce5619d13f5a7424429f4bf55df9e3ad5110da34d1060
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.3.6" version: "11.4.1"
firebase_analytics_platform_interface: firebase_analytics_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_analytics_platform_interface name: firebase_analytics_platform_interface
sha256: "8e987cf977c0c8f4ad02d9950a9b25b1a9606899f37b66a322a43af05be0246b" sha256: a34db46c367265c4c961626e4b128bfb7b7e50958e7add4c27ba103f5f81b9b0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.8" version: "4.3.1"
firebase_analytics_web: firebase_analytics_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_analytics_web name: firebase_analytics_web
sha256: "0b64ef9060d394bba3d3b4777f49ee098efeeea7b0afb04663c956de6a3da170" sha256: b6b4cef08e45e4c7d48476d9fc49fe9577081809a59026fe95b1a1b1eea165fa
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.10+5" version: "0.5.10+7"
firebase_core: firebase_core:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde" sha256: d851c1ca98fd5a4c07c747f8c65dacc2edd84a4d9ac055d32a5f0342529069f5
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.9.0" version: "3.10.1"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -586,26 +586,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_messaging name: firebase_messaging
sha256: "151a3ee68736abf293aab66d1317ade53c88abe1db09c75a0460aebf7767bbdf" sha256: e20ea2a0ecf9b0971575ab3ab42a6e285a94e50092c555b090c1a588a81b4d54
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.1.6" version: "15.2.1"
firebase_messaging_platform_interface: firebase_messaging_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_platform_interface name: firebase_messaging_platform_interface
sha256: f331ee51e40c243f90cc7bc059222dfec4e5df53125b08d31fb28961b00d2a9d sha256: c57a92b5ae1857ef4fe4ae2e73452b44d32e984e15ab8b53415ea1bb514bdabd
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.49" version: "4.6.1"
firebase_messaging_web: firebase_messaging_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_web name: firebase_messaging_web
sha256: efaf3fdc54cd77e0eedb8e75f7f01c808828c64d052ddbf94d3009974e47d30f sha256: "83694a990d8525d6b01039240b97757298369622ca0253ad0ebcfed221bf8ee0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.9.5" version: "3.10.1"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -618,10 +618,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_chart name: fl_chart
sha256: c724234b05e378383e958f3e82ca84a3e1e3c06a0898462044dd8a24b1ee9864 sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.70.0" version: "0.70.2"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -675,14 +675,78 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
flutter_inappwebview:
dependency: "direct main"
description:
name: flutter_inappwebview
sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5"
url: "https://pub.dev"
source: hosted
version: "6.1.5"
flutter_inappwebview_android:
dependency: transitive
description:
name: flutter_inappwebview_android
sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
flutter_inappwebview_internal_annotations:
dependency: transitive
description:
name: flutter_inappwebview_internal_annotations
sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
flutter_inappwebview_ios:
dependency: transitive
description:
name: flutter_inappwebview_ios
sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_inappwebview_macos:
dependency: transitive
description:
name: flutter_inappwebview_macos
sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_inappwebview_platform_interface:
dependency: transitive
description:
name: flutter_inappwebview_platform_interface
sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500
url: "https://pub.dev"
source: hosted
version: "1.3.0+1"
flutter_inappwebview_web:
dependency: transitive
description:
name: flutter_inappwebview_web
sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_inappwebview_windows:
dependency: transitive
description:
name: flutter_inappwebview_windows
sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_launcher_icons name: flutter_launcher_icons
sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5" sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.14.2" version: "0.14.3"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -700,10 +764,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_markdown name: flutter_markdown
sha256: "255b00afa1a7bad19727da6a7780cf3db6c3c12e68d302d85e0ff1fdf173db9e" sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.4+3" version: "0.7.5"
flutter_native_splash: flutter_native_splash:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -740,10 +804,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_svg name: flutter_svg
sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123" sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.16" version: "2.0.17"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -766,10 +830,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_webrtc name: flutter_webrtc
sha256: e82ffd0d0b79621c5554eed73509d7f5bd286d57fef29a573846785c65237fb1 sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.5+hotfix.2" version: "0.12.7"
freezed: freezed:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -814,18 +878,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: glob name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.3"
go_router: go_router:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539" sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.6.2" version: "14.7.2"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
@ -875,7 +939,7 @@ packages:
source: hosted source: hosted
version: "0.7.0" version: "0.7.0"
html: html:
dependency: transitive dependency: "direct main"
description: description:
name: html name: html
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
@ -886,10 +950,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: http name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.2" version: "1.3.0"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -934,10 +998,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_android name: image_picker_android
sha256: aa6f1280b670861ac45220cc95adc59bb6ae130259d36f980ccb62220dc5e59f sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.12+19" version: "0.8.12+20"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
@ -966,18 +1030,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_macos name: image_picker_macos
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.1+1" version: "0.2.1+2"
image_picker_platform_interface: image_picker_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: image_picker_platform_interface name: image_picker_platform_interface
sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.10.0" version: "2.10.1"
image_picker_windows: image_picker_windows:
dependency: transitive dependency: transitive
description: description:
@ -1086,10 +1150,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: livekit_client name: livekit_client
sha256: a19bcf8640b45e0730b1e3e3e78be7882dad680c6ebe8ae75294fd8d4612450d sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.4+hotfix.2" version: "2.3.5"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@ -1110,10 +1174,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: markdown name: markdown
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.2.2" version: "7.3.0"
marquee: marquee:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1270,10 +1334,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d" sha256: b15fad91c4d3d1f2b48c053dd41cb82da007c27407dc9ab5f9aa59881d0e39d4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.2" version: "8.1.4"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1502,10 +1566,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: pubspec_parse name: pubspec_parse
sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.5.0"
qr: qr:
dependency: transitive dependency: transitive
description: description:
@ -1630,10 +1694,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: share_plus name: share_plus
sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400" sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.3" version: "10.1.4"
share_plus_platform_interface: share_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1646,18 +1710,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.5" version: "2.5.1"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d" sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.4"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
@ -1971,18 +2035,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.3" version: "2.4.0"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.3" version: "3.1.4"
uuid: uuid:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1995,18 +2059,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_graphics name: vector_graphics
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" sha256: "7ed22c21d7fdcc88dd6ba7860384af438cd220b251ad65dfc142ab722fabef61"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.15" version: "1.1.16"
vector_graphics_codec: vector_graphics_codec:
dependency: transitive dependency: transitive
description: description:
name: vector_graphics_codec name: vector_graphics_codec
sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.12" version: "1.1.13"
vector_graphics_compiler: vector_graphics_compiler:
dependency: transitive dependency: transitive
description: description:
@ -2107,10 +2171,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: web_socket_channel name: web_socket_channel
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
webrtc_interface: webrtc_interface:
dependency: transitive dependency: transitive
description: description:
@ -2123,10 +2187,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.10.0" version: "5.10.1"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:
@ -2152,7 +2216,7 @@ packages:
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml: xml:
dependency: transitive dependency: "direct main"
description: description:
name: xml name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2.2.1+48 version: 2.2.2+61
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@ -115,6 +115,9 @@ dependencies:
slide_countdown: ^2.0.2 slide_countdown: ^2.0.2
video_compress: ^3.1.3 video_compress: ^3.1.3
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
flutter_inappwebview: ^6.1.5
html: ^0.15.5
xml: ^6.5.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -1,9 +1,9 @@
id = "solian-next" id = "solian"
[[locations]] [[locations]]
id = "solian-next" id = "solian"
host = ["sn-next.solsynth.dev"] hosts = ["sn.solsynth.dev"]
path = ["/"] paths = ["/"]
[[locations.destinations]] [[locations.destinations]]
id = "solian-next-web" id = "solian-web"
uri = "files:///workdir/solian-next?fallback=index.html&index=index.html" uri = "files:///workdir/solian?fallback=index.html&index=index.html"

View File

@ -16,6 +16,8 @@
--> -->
<base href="$FLUTTER_BASE_HREF"> <base href="$FLUTTER_BASE_HREF">
<script type="application/javascript" src="/assets/packages/flutter_inappwebview_web/assets/web/web_support.js" defer></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project."> <meta name="description" content="A new Flutter project.">

View File

@ -11,6 +11,7 @@
#include <file_saver/file_saver_plugin.h> #include <file_saver/file_saver_plugin.h>
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <firebase_core/firebase_core_plugin_c_api.h> #include <firebase_core/firebase_core_plugin_c_api.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_udid/flutter_udid_plugin_c_api.h> #include <flutter_udid/flutter_udid_plugin_c_api.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h> #include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <gal/gal_plugin_c_api.h> #include <gal/gal_plugin_c_api.h>
@ -34,6 +35,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
FirebaseCorePluginCApiRegisterWithRegistrar( FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
FlutterUdidPluginCApiRegisterWithRegistrar( FlutterUdidPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi")); registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
FlutterWebRTCPluginRegisterWithRegistrar( FlutterWebRTCPluginRegisterWithRegistrar(

View File

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_saver file_saver
file_selector_windows file_selector_windows
firebase_core firebase_core
flutter_inappwebview_windows
flutter_udid flutter_udid
flutter_webrtc flutter_webrtc
gal gal