Compare commits
74 Commits
Author | SHA1 | Date | |
---|---|---|---|
db8871a455 | |||
38dcaa6066 | |||
03275b46ca | |||
cf3b482fef | |||
aa4c04d4ef | |||
73b82f65e4 | |||
9471fe40fe | |||
0d1e18735e | |||
8bb62b5992 | |||
1e8a6dea5b | |||
5c2804cc4d | |||
0dbb8f132a | |||
3395f3dbd0 | |||
d258ba776e | |||
0dcfcaad56 | |||
687e720956 | |||
180876949e | |||
9718965809 | |||
5377161fb0 | |||
963e538ae5 | |||
a355e3bf90 | |||
cb4a2598c8 | |||
950612dc07 | |||
cbd1eaf1af | |||
ac41cbd99f | |||
9f9c90abc4 | |||
87029e3538 | |||
127d9adc09 | |||
c82dc7ad85 | |||
36bcff7a7c | |||
38201b547a | |||
ed0334fcda | |||
fbb486b90b | |||
9b34f385d5 | |||
bb7b731602 | |||
19076f8136 | |||
dc77a936ce | |||
7f58710c6f | |||
068ddcdcdc | |||
f4e9252ca0 | |||
3b1e918117 | |||
ed7981fdaf | |||
9698ca53e4 | |||
ddc1dc7daf | |||
1625a957f8 | |||
2dc50d627e | |||
2ffde9a3dd | |||
5967a91ae1 | |||
32c1effcb5 | |||
9d0e19c56f | |||
acf4e634fe | |||
25942c2338 | |||
a4f81f6ba1 | |||
c1b9090e51 | |||
f494f70003 | |||
fb2a55a909 | |||
4edfa7fd50 | |||
d699cac9b1 | |||
c0428e12c1 | |||
55f434ff05 | |||
f2b3bdda2d | |||
1f6bf33b0e | |||
e2027b1a32 | |||
2b3a58b55e | |||
6ac536412a | |||
52f8ffe4e4 | |||
aca81431aa | |||
1fadd850b7 | |||
ed2a9a21b6 | |||
57279eb3e4 | |||
c403a2914a | |||
bcb176344c | |||
ecf362cffc | |||
f4ab7671d8 |
@ -1,12 +1,12 @@
|
||||
{
|
||||
"sync": {
|
||||
"region": "solian-next",
|
||||
"region": "solian",
|
||||
"configPath": "roadsign.toml"
|
||||
},
|
||||
"deployments": [
|
||||
{
|
||||
"region": "solian-next",
|
||||
"site": "solian-next-web",
|
||||
"region": "solian",
|
||||
"site": "solian-web",
|
||||
"path": "build/web"
|
||||
}
|
||||
]
|
||||
|
@ -17,7 +17,12 @@
|
||||
android:label="Solian"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
@ -15,11 +15,11 @@ body:json {
|
||||
"client_id": "{{third_client_id}}",
|
||||
"client_secret":"{{third_client_tk}}",
|
||||
"type": "general",
|
||||
"subject": "Merry Christmas!",
|
||||
"subject": "新年快乐!",
|
||||
"subtitle": "一条来自 Solar Network 团队的信息",
|
||||
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
|
||||
"content": "今天是农历正月初一,小羊祝您新年快乐 🎉",
|
||||
"metadata": {
|
||||
"image": "6EqsYQwmFRCkbmhR"
|
||||
"image": "D2EDbcrsTugs3xk5"
|
||||
},
|
||||
"priority": 10
|
||||
}
|
||||
|
11
api/Reader/List News Sources.bru
Normal file
11
api/Reader/List News Sources.bru
Normal file
@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: List News Sources
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/cgi/re/well-known/sources
|
||||
body: none
|
||||
auth: none
|
||||
}
|
17
api/Reader/List News.bru
Normal file
17
api/Reader/List News.bru
Normal file
@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: List News
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/cgi/re/news?take=10&offset=0&source=shadiao
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
params:query {
|
||||
take: 10
|
||||
offset: 0
|
||||
source: shadiao
|
||||
}
|
18
api/Reader/Trigger Scan News.bru
Normal file
18
api/Reader/Trigger Scan News.bru
Normal file
@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: Trigger Scan News
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/re/admin/scan
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"sources": ["taiwan-ltn"],
|
||||
"eager": true
|
||||
}
|
||||
}
|
11
api/WatchTower/Run Database Maintenance.bru
Normal file
11
api/WatchTower/Run Database Maintenance.bru
Normal file
@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Run Database Maintenance
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/wt/maintenance/database
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
@ -17,6 +17,10 @@
|
||||
"screenAccountProfileEdit": "Edit Profile",
|
||||
"screenAbuseReport": "Abuse Reports",
|
||||
"screenSettings": "Settings",
|
||||
"screenAccountSettings": "Account Settings",
|
||||
"screenFactorSettings": "Auth Factors",
|
||||
"screenAccountWallet": "Wallet",
|
||||
"screenNews": "News",
|
||||
"screenAlbum": "Album",
|
||||
"screenChat": "Chat",
|
||||
"screenChatManage": "Edit Channel",
|
||||
@ -103,8 +107,18 @@
|
||||
},
|
||||
"loginEnterPassword": "Enter the code",
|
||||
"loginSuccess": "Logged in as {}",
|
||||
"authFactorDelete": "Delete Auth Factor",
|
||||
"authFactorDeleteDescription": "Are you sure you want delete auth factor {}?",
|
||||
"authFactorPassword": "Password",
|
||||
"authFactorPasswordDescription": "The password you set when you registered.",
|
||||
"authFactorEmail": "Email verification code",
|
||||
"authFactorEmailDescription": "An one-time code sent to the email address you set when you registered.",
|
||||
"authFactorTOTP": "Time-based OTP",
|
||||
"authFactorTOTPDescription": "A one-time code generated by a TOTP authenticator such as Google Authenticator or Authy.",
|
||||
"authFactorInAppNotify": "In-app notification",
|
||||
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
|
||||
"authFactorAdd": "Add a factor",
|
||||
"authFactorAddSubtitle": "Provide another way to login your account.",
|
||||
"accountIntroTitle": "Hello there!",
|
||||
"accountIntroSubtitle": "Pick an option below to get started.",
|
||||
"accountLogout": "Logout",
|
||||
@ -113,8 +127,14 @@
|
||||
"accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.",
|
||||
"accountPublishers": "Your publishers",
|
||||
"accountPublishersSubtitle": "Manage your publish identities.",
|
||||
"accountSettings": "Account Settings",
|
||||
"accountSettingsSubtitle": "Manage your account and make it yours.",
|
||||
"accountProfileEdit": "Edit your profile",
|
||||
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
|
||||
"accountWallet": "Wallet",
|
||||
"accountWalletSubtitle": "View your balance and transactions.",
|
||||
"factorSettings": "Auth Factors",
|
||||
"factorSettingsSubtitle": "Manage your authentication factors.",
|
||||
"accountProfileEditApplied": "Profile modification applied.",
|
||||
"publishersNew": "New Publisher",
|
||||
"publisherNewSubtitle": "Create a new publisher identity.",
|
||||
@ -179,8 +199,13 @@
|
||||
"other": "{} comments"
|
||||
},
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsDisplayLanguage": "Display Language",
|
||||
"settingsDisplayLanguageDescription": "Set the application language.",
|
||||
"settingsDisplayLanguageSystem": "Follow System",
|
||||
"settingsAppBarTransparent": "Transparent App Bar",
|
||||
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
|
||||
"settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
|
||||
"settingsDrawerPreferCollapseDescription": "Make the drawer to collapse even when the screen is wide enough.",
|
||||
"settingsBackgroundImage": "Background Image",
|
||||
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
|
||||
"settingsBackgroundImageClear": "Clear Existing Background Image",
|
||||
@ -191,6 +216,13 @@
|
||||
"settingsColorSchemeDescription": "Set the application primary color.",
|
||||
"settingsColorSeed": "Color Seed",
|
||||
"settingsColorSeedDescription": "Select one of the present color schemes.",
|
||||
"settingsFeatures": "Features",
|
||||
"settingsNotifyWithHaptic": "Haptic when Notified",
|
||||
"settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.",
|
||||
"settingsExpandPostLink": "Expand Post Link",
|
||||
"settingsExpandPostLinkDescription": "Expand the post link in the post list.",
|
||||
"settingsExpandChatLink": "Expand Chat Link",
|
||||
"settingsExpandChatLinkDescription": "Expand the chat link in the chat list.",
|
||||
"settingsNetwork": "Network",
|
||||
"settingsNetworkServer": "HyperNet Server",
|
||||
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
|
||||
@ -213,8 +245,9 @@
|
||||
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
||||
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
||||
"sensitiveContentReveal": "Reveal",
|
||||
"serverConnecting": "Connecting to server...",
|
||||
"serverDisconnected": "Lost connection from server",
|
||||
"serverConnecting": "Connecting...",
|
||||
"serverDisconnected": "Connection Lost",
|
||||
"serverConnected": "Connected",
|
||||
"fieldChatAlias": "Channel Alias",
|
||||
"fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.",
|
||||
"fieldChatName": "Name",
|
||||
@ -292,6 +325,7 @@
|
||||
"addAttachmentFromCameraPhoto": "Take photo",
|
||||
"addAttachmentFromCameraVideo": "Take video",
|
||||
"addAttachmentFromRandomId": "Link via RID",
|
||||
"attachmentDetailInfo": "Attachment details",
|
||||
"attachmentPastedImage": "Pasted Image",
|
||||
"attachmentInsertLink": "Insert Link",
|
||||
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
|
||||
@ -520,6 +554,9 @@
|
||||
"postImageShareAds": "Explore posts on the Solar Network",
|
||||
"postShare": "Share",
|
||||
"postShareImage": "Share via Image",
|
||||
"postGetInsight": "Get Insight",
|
||||
"postGetInsightTitle": "AI Insight",
|
||||
"postGetInsightDescription": "AI may make mistakes, check important information.",
|
||||
"appInitializing": "Initializing",
|
||||
"poweredBy": "Powered by {}",
|
||||
"shareIntent": "Share",
|
||||
@ -547,5 +584,25 @@
|
||||
"postCategoryKnowledge": "Knowledge",
|
||||
"postCategoryLiterature": "Literature",
|
||||
"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"
|
||||
}
|
||||
|
@ -15,6 +15,10 @@
|
||||
"screenAccountProfileEdit": "编辑资料",
|
||||
"screenAbuseReport": "滥用检举",
|
||||
"screenSettings": "设置",
|
||||
"screenAccountSettings": "账号设置",
|
||||
"screenFactorSettings": "验证因子",
|
||||
"screenAccountWallet": "钱包",
|
||||
"screenNews": "新闻",
|
||||
"screenAlbum": "相册",
|
||||
"screenChat": "聊天",
|
||||
"screenChatManage": "编辑聊天频道",
|
||||
@ -87,8 +91,18 @@
|
||||
},
|
||||
"loginEnterPassword": "验证代码",
|
||||
"loginSuccess": "登录为 {}",
|
||||
"authFactorDelete": "删除验证因子",
|
||||
"authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
|
||||
"authFactorPassword": "密码",
|
||||
"authFactorPasswordDescription": "注册时选择设置的密码。",
|
||||
"authFactorEmail": "电邮一次性验证码",
|
||||
"authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
|
||||
"authFactorTOTP": "时序验证码",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
|
||||
"authFactorInAppNotify": "应用内通知验证码",
|
||||
"authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
|
||||
"authFactorAdd": "添加新验证因子",
|
||||
"authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
|
||||
"accountIntroTitle": "喜欢您来!",
|
||||
"accountIntroSubtitle": "登陆以探索更广大的世界。",
|
||||
"accountLogout": "退出登录",
|
||||
@ -97,8 +111,14 @@
|
||||
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
|
||||
"accountPublishers": "你的发布者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帐号设置",
|
||||
"accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
|
||||
"accountProfileEdit": "编辑资料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
|
||||
"accountWallet": "钱包",
|
||||
"accountWalletSubtitle": "查看你的余额和交易记录。",
|
||||
"factorSettings": "验证因子",
|
||||
"factorSettingsSubtitle": "管理你的登陆验证方式。",
|
||||
"accountProfileEditApplied": "个人资料修改已被应用。",
|
||||
"publishersNew": "新发布者",
|
||||
"publisherNewSubtitle": "创建一个新的公共身份。",
|
||||
@ -177,6 +197,9 @@
|
||||
"other": "{} 条评论"
|
||||
},
|
||||
"settingsAppearance": "外观",
|
||||
"settingsDisplayLanguage": "显示语言",
|
||||
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
|
||||
"settingsDisplayLanguageSystem": "跟随系统",
|
||||
"settingsBackgroundImage": "背景图片",
|
||||
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
|
||||
"settingsBackgroundImageClear": "清除现存背景图",
|
||||
@ -185,10 +208,19 @@
|
||||
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
|
||||
"settingsAppBarTransparent": "透明顶栏",
|
||||
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
|
||||
"settingsDrawerPreferCollapse": "侧边栏偏好折叠",
|
||||
"settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
|
||||
"settingsColorScheme": "主题色",
|
||||
"settingsColorSchemeDescription": "设置应用主题色。",
|
||||
"settingsColorSeed": "预设色彩主题",
|
||||
"settingsColorSeedDescription": "选择一个预设色彩主题。",
|
||||
"settingsFeatures": "功能",
|
||||
"settingsNotifyWithHaptic": "新通知时振动",
|
||||
"settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
|
||||
"settingsExpandPostLink": "展开帖子链接",
|
||||
"settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。",
|
||||
"settingsExpandChatLink": "展开聊天链接",
|
||||
"settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。",
|
||||
"settingsNetwork": "网络",
|
||||
"settingsNetworkServer": "HyperNet 服务器",
|
||||
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
|
||||
@ -211,8 +243,9 @@
|
||||
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
||||
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
||||
"sensitiveContentReveal": "显示内容",
|
||||
"serverConnecting": "正在连接服务器…",
|
||||
"serverDisconnected": "已与服务器断开连接",
|
||||
"serverConnecting": "正在连接…",
|
||||
"serverDisconnected": "已断开连接",
|
||||
"serverConnected": "已连接",
|
||||
"fieldChatAlias": "频道别名",
|
||||
"fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
|
||||
"fieldChatName": "名称",
|
||||
@ -290,6 +323,7 @@
|
||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||
"addAttachmentFromCameraVideo": "拍摄视频",
|
||||
"addAttachmentFromRandomId": "通过访问 ID 链接",
|
||||
"attachmentDetailInfo": "附件详细信息",
|
||||
"attachmentPastedImage": "粘贴的图片",
|
||||
"attachmentInsertLink": "插入连接",
|
||||
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
||||
@ -518,6 +552,9 @@
|
||||
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖图",
|
||||
"postGetInsight": "获取见解",
|
||||
"postGetInsightTitle": "AI 见解",
|
||||
"postGetInsightDescription": "AI 可能会出错,检查信息真实性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
@ -545,5 +582,25 @@
|
||||
"postCategoryKnowledge": "知识",
|
||||
"postCategoryLiterature": "文学",
|
||||
"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 思考过程"
|
||||
}
|
||||
|
@ -15,6 +15,10 @@
|
||||
"screenAccountProfileEdit": "編輯資料",
|
||||
"screenAbuseReport": "濫用檢舉",
|
||||
"screenSettings": "設置",
|
||||
"screenAccountSettings": "賬號設置",
|
||||
"screenFactorSettings": "驗證因子",
|
||||
"screenAccountWallet": "錢包",
|
||||
"screenNews": "新聞",
|
||||
"screenAlbum": "相冊",
|
||||
"screenChat": "聊天",
|
||||
"screenChatManage": "編輯聊天頻道",
|
||||
@ -87,8 +91,18 @@
|
||||
},
|
||||
"loginEnterPassword": "驗證代碼",
|
||||
"loginSuccess": "登錄為 {}",
|
||||
"authFactorDelete": "刪除驗證因子",
|
||||
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||
"authFactorPassword": "密碼",
|
||||
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||
"authFactorEmail": "電郵一次性驗證碼",
|
||||
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||
"authFactorTOTP": "時序驗證碼",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||
"authFactorAdd": "添加新驗證因子",
|
||||
"authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。",
|
||||
"accountIntroTitle": "喜歡您來!",
|
||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||
"accountLogout": "退出登錄",
|
||||
@ -97,8 +111,14 @@
|
||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||
"accountPublishers": "你的發佈者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帳號設置",
|
||||
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||
"accountProfileEdit": "編輯資料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
|
||||
"accountWallet": "錢包",
|
||||
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||
"factorSettings": "驗證因子",
|
||||
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||
"publishersNew": "新發布者",
|
||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||
@ -177,6 +197,9 @@
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsDisplayLanguage": "顯示語言",
|
||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||
@ -185,10 +208,19 @@
|
||||
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||
"settingsAppBarTransparent": "透明頂欄",
|
||||
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
|
||||
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
|
||||
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
|
||||
"settingsColorScheme": "主題色",
|
||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||
"settingsColorSeed": "預設色彩主題",
|
||||
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||
"settingsFeatures": "功能",
|
||||
"settingsNotifyWithHaptic": "新通知時振動",
|
||||
"settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
|
||||
"settingsExpandPostLink": "展開帖子鏈接",
|
||||
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
|
||||
"settingsExpandChatLink": "展開聊天鏈接",
|
||||
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
|
||||
"settingsNetwork": "網絡",
|
||||
"settingsNetworkServer": "HyperNet 服務器",
|
||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||
@ -211,8 +243,9 @@
|
||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||
"sensitiveContentReveal": "顯示內容",
|
||||
"serverConnecting": "正在連接服務器…",
|
||||
"serverDisconnected": "已與服務器斷開連接",
|
||||
"serverConnecting": "正在連接…",
|
||||
"serverDisconnected": "已斷開連接",
|
||||
"serverConnected": "已連接",
|
||||
"fieldChatAlias": "頻道別名",
|
||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||
"fieldChatName": "名稱",
|
||||
@ -290,6 +323,7 @@
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||
"attachmentDetailInfo": "附件詳細信息",
|
||||
"attachmentPastedImage": "粘貼的圖片",
|
||||
"attachmentInsertLink": "插入連接",
|
||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||
@ -518,6 +552,9 @@
|
||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖圖",
|
||||
"postGetInsight": "獲取見解",
|
||||
"postGetInsightTitle": "AI 見解",
|
||||
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
@ -545,5 +582,25 @@
|
||||
"postCategoryKnowledge": "知識",
|
||||
"postCategoryLiterature": "文學",
|
||||
"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 思考過程"
|
||||
}
|
||||
|
@ -15,6 +15,10 @@
|
||||
"screenAccountProfileEdit": "編輯資料",
|
||||
"screenAbuseReport": "濫用檢舉",
|
||||
"screenSettings": "設置",
|
||||
"screenAccountSettings": "賬號設置",
|
||||
"screenFactorSettings": "驗證因子",
|
||||
"screenAccountWallet": "錢包",
|
||||
"screenNews": "新聞",
|
||||
"screenAlbum": "相冊",
|
||||
"screenChat": "聊天",
|
||||
"screenChatManage": "編輯聊天頻道",
|
||||
@ -87,8 +91,18 @@
|
||||
},
|
||||
"loginEnterPassword": "驗證代碼",
|
||||
"loginSuccess": "登錄為 {}",
|
||||
"authFactorDelete": "刪除驗證因子",
|
||||
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||
"authFactorPassword": "密碼",
|
||||
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||
"authFactorEmail": "電郵一次性驗證碼",
|
||||
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||
"authFactorTOTP": "時序驗證碼",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||
"authFactorAdd": "添加新驗證因子",
|
||||
"authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。",
|
||||
"accountIntroTitle": "喜歡您來!",
|
||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||
"accountLogout": "退出登錄",
|
||||
@ -97,8 +111,14 @@
|
||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||
"accountPublishers": "你的發佈者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帳號設置",
|
||||
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||
"accountProfileEdit": "編輯資料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
|
||||
"accountWallet": "錢包",
|
||||
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||
"factorSettings": "驗證因子",
|
||||
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||
"publishersNew": "新發布者",
|
||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||
@ -177,6 +197,9 @@
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsDisplayLanguage": "顯示語言",
|
||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||
@ -185,10 +208,19 @@
|
||||
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||
"settingsAppBarTransparent": "透明頂欄",
|
||||
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
|
||||
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
|
||||
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
|
||||
"settingsColorScheme": "主題色",
|
||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||
"settingsColorSeed": "預設色彩主題",
|
||||
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||
"settingsFeatures": "功能",
|
||||
"settingsNotifyWithHaptic": "新通知時振動",
|
||||
"settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
|
||||
"settingsExpandPostLink": "展開帖子鏈接",
|
||||
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
|
||||
"settingsExpandChatLink": "展開聊天鏈接",
|
||||
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
|
||||
"settingsNetwork": "網絡",
|
||||
"settingsNetworkServer": "HyperNet 服務器",
|
||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||
@ -211,8 +243,9 @@
|
||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||
"sensitiveContentReveal": "顯示內容",
|
||||
"serverConnecting": "正在連接服務器…",
|
||||
"serverDisconnected": "已與服務器斷開連接",
|
||||
"serverConnecting": "正在連接…",
|
||||
"serverDisconnected": "已斷開連接",
|
||||
"serverConnected": "已連接",
|
||||
"fieldChatAlias": "頻道別名",
|
||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||
"fieldChatName": "名稱",
|
||||
@ -290,6 +323,7 @@
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||
"attachmentDetailInfo": "附件詳細信息",
|
||||
"attachmentPastedImage": "粘貼的圖片",
|
||||
"attachmentInsertLink": "插入連接",
|
||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||
@ -518,6 +552,9 @@
|
||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖圖",
|
||||
"postGetInsight": "獲取見解",
|
||||
"postGetInsightTitle": "AI 見解",
|
||||
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
@ -545,5 +582,25 @@
|
||||
"postCategoryKnowledge": "知識",
|
||||
"postCategoryLiterature": "文學",
|
||||
"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 思考過程"
|
||||
}
|
||||
|
102
ios/Podfile.lock
102
ios/Podfile.lock
@ -43,58 +43,58 @@ PODS:
|
||||
- Flutter
|
||||
- file_saver (0.0.1):
|
||||
- Flutter
|
||||
- Firebase/Analytics (11.4.0):
|
||||
- Firebase/Analytics (11.6.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (11.4.0):
|
||||
- Firebase/Core (11.6.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 11.4.0)
|
||||
- Firebase/CoreOnly (11.4.0):
|
||||
- FirebaseCore (= 11.4.0)
|
||||
- Firebase/Messaging (11.4.0):
|
||||
- FirebaseAnalytics (~> 11.6.0)
|
||||
- Firebase/CoreOnly (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- Firebase/Messaging (11.6.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.4.0)
|
||||
- firebase_analytics (11.3.6):
|
||||
- Firebase/Analytics (= 11.4.0)
|
||||
- FirebaseMessaging (~> 11.6.0)
|
||||
- firebase_analytics (11.4.1):
|
||||
- Firebase/Analytics (= 11.6.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_core (3.9.0):
|
||||
- Firebase/CoreOnly (= 11.4.0)
|
||||
- firebase_core (3.10.1):
|
||||
- Firebase/CoreOnly (= 11.6.0)
|
||||
- Flutter
|
||||
- firebase_messaging (15.1.6):
|
||||
- Firebase/Messaging (= 11.4.0)
|
||||
- firebase_messaging (15.2.1):
|
||||
- Firebase/Messaging (= 11.6.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseAnalytics (11.4.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.4.0)
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseAnalytics (11.6.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.4.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleAppMeasurement (= 11.4.0)
|
||||
- GoogleAppMeasurement (= 11.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (11.4.0):
|
||||
- FirebaseCoreInternal (~> 11.0)
|
||||
- FirebaseCore (11.6.0):
|
||||
- FirebaseCoreInternal (~> 11.6.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- FirebaseCoreInternal (11.6.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseInstallations (11.4.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseInstallations (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (11.4.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseMessaging (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -105,32 +105,39 @@ PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_app_update (0.0.1):
|
||||
- Flutter
|
||||
- flutter_inappwebview_ios (0.0.1):
|
||||
- Flutter
|
||||
- flutter_inappwebview_ios/Core (= 0.0.1)
|
||||
- OrderedSet (~> 6.0.3)
|
||||
- flutter_inappwebview_ios/Core (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (~> 6.0.3)
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- flutter_webrtc (0.12.2):
|
||||
- flutter_webrtc (0.12.6):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleAppMeasurement (11.4.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.4.0)
|
||||
- GoogleAppMeasurement (11.6.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.4.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.6.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
@ -173,7 +180,7 @@ PODS:
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- Kingfisher (8.1.3)
|
||||
- livekit_client (2.3.4):
|
||||
- livekit_client (2.3.5):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@ -188,6 +195,7 @@ PODS:
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- OrderedSet (6.0.3)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- pasteboard (0.0.1):
|
||||
@ -239,6 +247,7 @@ DEPENDENCIES:
|
||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
|
||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||
@ -282,6 +291,7 @@ SPEC REPOS:
|
||||
- GoogleUtilities
|
||||
- Kingfisher
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- SAMKeychain
|
||||
- SDWebImage
|
||||
@ -309,6 +319,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
flutter_app_update:
|
||||
:path: ".symlinks/plugins/flutter_app_update/ios"
|
||||
flutter_inappwebview_ios:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_udid:
|
||||
@ -367,35 +379,37 @@ SPEC CHECKSUMS:
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
|
||||
firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75
|
||||
firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10
|
||||
firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9
|
||||
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
|
||||
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
|
||||
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
|
||||
firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e
|
||||
firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b
|
||||
firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e
|
||||
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
|
||||
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
|
||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
||||
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
|
||||
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
|
||||
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
|
||||
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
|
||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||
flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563
|
||||
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
|
||||
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
|
||||
livekit_client: 4eaa7a2968fc7e7c57888f43d90394547cc8d9e9
|
||||
livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
|
@ -74,6 +74,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
_wsSubscription = _ws.stream.stream.listen((event) {
|
||||
switch (event.method) {
|
||||
case 'events.new':
|
||||
if (event.payload?['channel_id'] != channel?.id) break;
|
||||
final payload = SnChatMessage.fromJson(event.payload!);
|
||||
_addMessage(payload);
|
||||
break;
|
||||
@ -83,7 +84,6 @@ class ChatMessageController extends ChangeNotifier {
|
||||
if (member.id == profile?.id) break;
|
||||
if (!typingMembers.any((x) => x.id == member.id)) {
|
||||
typingMembers.add(member);
|
||||
print('Typing member: ${typingMembers.map((ele) => member.id)}');
|
||||
notifyListeners();
|
||||
}
|
||||
typingInactiveTimer[member.id]?.cancel();
|
||||
|
@ -154,7 +154,10 @@ class PostWriteController extends ChangeNotifier {
|
||||
final TextEditingController descriptionController = TextEditingController();
|
||||
final TextEditingController aliasController = TextEditingController();
|
||||
|
||||
PostWriteController() {
|
||||
bool _temporarySaveActive = false;
|
||||
|
||||
PostWriteController({bool doLoadFromTemporary = true}) {
|
||||
_temporarySaveActive = doLoadFromTemporary;
|
||||
titleController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
@ -166,7 +169,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
contentController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
});
|
||||
_temporaryLoad();
|
||||
if (doLoadFromTemporary) _temporaryLoad();
|
||||
}
|
||||
|
||||
String mode = kTitleMap.keys.first;
|
||||
@ -317,6 +320,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
Timer? _temporarySaveTimer;
|
||||
|
||||
void _temporaryPlanSave() {
|
||||
if (!_temporarySaveActive) return;
|
||||
_temporarySaveTimer?.cancel();
|
||||
_temporarySaveTimer = Timer(const Duration(seconds: 1), () {
|
||||
_temporarySave();
|
||||
@ -623,6 +627,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
void reset() {
|
||||
publishedAt = null;
|
||||
publishedUntil = null;
|
||||
thumbnail = null;
|
||||
visibility = 0;
|
||||
titleController.clear();
|
||||
descriptionController.clear();
|
||||
contentController.clear();
|
||||
|
@ -10,7 +10,6 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@ -18,7 +17,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/firebase_options.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
@ -42,7 +40,6 @@ import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/version_label.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
@ -261,6 +258,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
|
||||
Future<void> _initialize() async {
|
||||
try {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
cfg.calcDrawerSize(context);
|
||||
});
|
||||
final home = context.read<HomeWidgetProvider>();
|
||||
await home.initialize();
|
||||
if (!mounted) return;
|
||||
@ -278,6 +279,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
await ws.tryConnect();
|
||||
if (!mounted) return;
|
||||
final notify = context.read<NotificationProvider>();
|
||||
notify.listen();
|
||||
await notify.registerPushNotifications();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
@ -301,6 +303,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
|
||||
@override
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/providers/widget.dart';
|
||||
|
||||
@ -12,6 +13,10 @@ const kNetworkServerStoreKey = 'app_server_url';
|
||||
const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
||||
const kAppBackgroundStoreKey = 'app_has_background';
|
||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
||||
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
|
||||
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
|
||||
const kAppExpandPostLink = 'app_expand_post_link';
|
||||
const kAppExpandChatLink = 'app_expand_chat_link';
|
||||
|
||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||
'settingsImageQualityLowest': FilterQuality.none,
|
||||
@ -33,6 +38,32 @@ class ConfigProvider extends ChangeNotifier {
|
||||
prefs = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
bool drawerIsCollapsed = false;
|
||||
bool drawerIsExpanded = false;
|
||||
|
||||
void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
|
||||
bool newDrawerIsCollapsed = false;
|
||||
bool newDrawerIsExpanded = false;
|
||||
if (withMediaQuery) {
|
||||
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450;
|
||||
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451;
|
||||
} else {
|
||||
final rpb = ResponsiveBreakpoints.of(context);
|
||||
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
|
||||
newDrawerIsExpanded = rpb.largerThan(TABLET)
|
||||
? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
|
||||
? false
|
||||
: true
|
||||
: false;
|
||||
}
|
||||
|
||||
if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
|
||||
drawerIsExpanded = newDrawerIsExpanded;
|
||||
drawerIsCollapsed = newDrawerIsCollapsed;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
FilterQuality get imageQuality {
|
||||
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
|
||||
}
|
||||
|
@ -58,6 +58,11 @@ class NavigationProvider extends ChangeNotifier {
|
||||
screen: 'realm',
|
||||
label: 'screenRealm',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
|
||||
screen: 'news',
|
||||
label: 'screenNews',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
||||
screen: 'album',
|
||||
@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier {
|
||||
|
||||
List<AppNavDestination> destinations = [];
|
||||
|
||||
int get pinnedDestinationCount =>
|
||||
destinations.where((ele) => ele.isPinned).length;
|
||||
int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
|
||||
|
||||
NavigationProvider() {
|
||||
buildDestinations(kDefaultPinnedDestination);
|
||||
@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
bool isIndexInRange(int min, int max) {
|
||||
return _currentIndex != null &&
|
||||
_currentIndex! >= min &&
|
||||
_currentIndex! < max;
|
||||
return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
|
||||
}
|
||||
|
||||
void autoDetectIndex(GoRouter? state) {
|
||||
if (state == null) return;
|
||||
final idx = destinations.indexWhere(
|
||||
(ele) =>
|
||||
ele.screen ==
|
||||
state.routerDelegate.currentConfiguration.last.route.name,
|
||||
(ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
|
||||
);
|
||||
_currentIndex = idx == -1 ? null : idx;
|
||||
notifyListeners();
|
||||
|
@ -4,18 +4,26 @@ import 'dart:io';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/notification.dart';
|
||||
|
||||
class NotificationProvider extends ChangeNotifier {
|
||||
late final SnNetworkProvider _sn;
|
||||
late final UserProvider _ua;
|
||||
late final WebSocketProvider _ws;
|
||||
late final ConfigProvider _cfg;
|
||||
|
||||
NotificationProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
_ua = context.read<UserProvider>();
|
||||
_ws = context.read<WebSocketProvider>();
|
||||
_cfg = context.read<ConfigProvider>();
|
||||
}
|
||||
|
||||
Future<void> registerPushNotifications() async {
|
||||
@ -62,4 +70,23 @@ class NotificationProvider extends ChangeNotifier {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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!);
|
||||
notifications.add(notification);
|
||||
notifyListeners();
|
||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||
if (doHaptic) HapticFeedback.lightImpact();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void clear() {
|
||||
notifications.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
|
||||
Future<void> connect({noRetry = false}) async {
|
||||
if (!_ua.isAuthorized) return;
|
||||
if (isConnected) {
|
||||
if (isConnected || conn != null) {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
onError: (err) {
|
||||
isConnected = false;
|
||||
notifyListeners();
|
||||
Future.delayed(const Duration(seconds: 11), () => connect());
|
||||
Future.delayed(const Duration(seconds: 1), () => connect());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
424
lib/router.dart
424
lib/router.dart
@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:surface/screens/abuse_report.dart';
|
||||
import 'package:surface/screens/account.dart';
|
||||
import 'package:surface/screens/account/pfp.dart';
|
||||
import 'package:surface/screens/account/account_settings.dart';
|
||||
import 'package:surface/screens/account/factor_settings.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
import 'package:surface/screens/account/profile_edit.dart';
|
||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||
import 'package:surface/screens/account/publishers/publisher_new.dart';
|
||||
@ -19,6 +21,8 @@ import 'package:surface/screens/chat/room.dart';
|
||||
import 'package:surface/screens/explore.dart';
|
||||
import 'package:surface/screens/friend.dart';
|
||||
import 'package:surface/screens/home.dart';
|
||||
import 'package:surface/screens/news/news_detail.dart';
|
||||
import 'package:surface/screens/news/news_list.dart';
|
||||
import 'package:surface/screens/notification.dart';
|
||||
import 'package:surface/screens/post/post_detail.dart';
|
||||
import 'package:surface/screens/post/post_editor.dart';
|
||||
@ -29,290 +33,226 @@ import 'package:surface/screens/realm/manage.dart';
|
||||
import 'package:surface/screens/realm/realm_detail.dart';
|
||||
import 'package:surface/screens/settings.dart';
|
||||
import 'package:surface/screens/sharing.dart';
|
||||
import 'package:surface/screens/wallet.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/about.dart';
|
||||
import 'package:surface/widgets/navigation/app_background.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
Widget _fadeThroughTransition(
|
||||
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
final _appRoutes = [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(
|
||||
body: child,
|
||||
showAppBar: false,
|
||||
),
|
||||
GoRoute(
|
||||
path: '/',
|
||||
name: 'home',
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/posts',
|
||||
name: 'explore',
|
||||
builder: (context, state) => const ExploreScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
name: 'home',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const HomeScreen(),
|
||||
path: '/write/:mode',
|
||||
name: 'postEditor',
|
||||
builder: (context, state) => 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: '/posts',
|
||||
name: 'explore',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const ExploreScreen(),
|
||||
),
|
||||
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(),
|
||||
path: '/search',
|
||||
name: 'postSearch',
|
||||
builder: (context, state) => PostSearchScreen(
|
||||
initialTags: state.uri.queryParameters['tags']?.split(','),
|
||||
initialCategories: state.uri.queryParameters['categories']?.split(','),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/friend',
|
||||
name: 'friend',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const FriendScreen(),
|
||||
),
|
||||
path: '/publishers/:name',
|
||||
name: 'postPublisher',
|
||||
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notification',
|
||||
name: 'notification',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: const NotificationScreen(),
|
||||
path: '/:slug',
|
||||
name: 'postDetail',
|
||||
builder: (context, state) => PostDetailScreen(
|
||||
slug: state.pathParameters['slug']!,
|
||||
preload: state.extra as SnPost?,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
||||
GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
|
||||
GoRoute(
|
||||
path: '/wallet',
|
||||
name: 'accountWallet',
|
||||
builder: (context, state) => const WalletScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'accountSettings',
|
||||
builder: (context, state) => AccountSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings/factors',
|
||||
name: 'factorSettings',
|
||||
builder: (context, state) => FactorSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => ProfileEditScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers',
|
||||
name: 'accountPublishers',
|
||||
builder: (context, state) => PublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/new',
|
||||
name: 'accountPublisherNew',
|
||||
builder: (context, state) => AccountPublisherNewScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/edit/:name',
|
||||
name: 'accountPublisherEdit',
|
||||
builder: (context, state) => AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
),
|
||||
]),
|
||||
GoRoute(
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
builder: (context, state) => const ChatScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/auth/login',
|
||||
name: 'authLogin',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: LoginScreen(),
|
||||
path: '/:scope/:alias',
|
||||
name: 'chatRoom',
|
||||
builder: (context, state) => ChatRoomScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/register',
|
||||
name: 'authRegister',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: RegisterScreen(),
|
||||
path: '/:scope/:alias/call',
|
||||
name: 'chatCallRoom',
|
||||
builder: (context, state) => CallRoomScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/reports',
|
||||
name: 'abuseReport',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: AbuseReportScreen(),
|
||||
path: '/:scope/:alias/detail',
|
||||
name: 'channelDetail',
|
||||
builder: (context, state) => ChannelDetailScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: ProfileEditScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers',
|
||||
name: 'accountPublishers',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: PublisherScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers/new',
|
||||
name: 'accountPublisherNew',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: AccountPublisherNewScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers/edit/:name',
|
||||
name: 'accountPublisherEdit',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
path: '/manage',
|
||||
name: 'chatManage',
|
||||
builder: (context, state) => ChatManageScreen(
|
||||
editingChannelAlias: state.uri.queryParameters['editing'],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
path: '/realm',
|
||||
name: 'realm',
|
||||
pageBuilder: (context, state) => CustomTransitionPage(
|
||||
transitionsBuilder: _fadeThroughTransition,
|
||||
child: const RealmScreen(),
|
||||
),
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: SettingsScreen(),
|
||||
path: '/:alias',
|
||||
name: 'realmDetail',
|
||||
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manage',
|
||||
name: 'realmManage',
|
||||
builder: (context, state) => RealmManageScreen(
|
||||
editingRealmAlias: state.uri.queryParameters['editing'],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: AboutScreen(),
|
||||
),
|
||||
GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
|
||||
GoRoute(
|
||||
path: '/:hash',
|
||||
name: 'newsDetail',
|
||||
builder: (context, state) => NewsDetailScreen(
|
||||
hash: state.pathParameters['hash']!,
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
GoRoute(
|
||||
path: '/album',
|
||||
name: 'album',
|
||||
builder: (context, state) => const AlbumScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/friend',
|
||||
name: 'friend',
|
||||
builder: (context, state) => const FriendScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notification',
|
||||
name: 'notification',
|
||||
builder: (context, state) => const NotificationScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/login',
|
||||
name: 'authLogin',
|
||||
builder: (context, state) => LoginScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/register',
|
||||
name: 'authRegister',
|
||||
builder: (context, state) => RegisterScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/reports',
|
||||
name: 'abuseReport',
|
||||
builder: (context, state) => AbuseReportScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
builder: (context, state) => SettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
builder: (context, state) => AboutScreen(),
|
||||
),
|
||||
];
|
||||
|
||||
|
@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
import '../types/account.dart';
|
||||
|
||||
@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAbuseReport').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
itemCount: _reports.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return ListTile(
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
@ -12,6 +14,8 @@ import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class AccountScreen extends StatelessWidget {
|
||||
const AccountScreen({super.key});
|
||||
@ -19,11 +23,39 @@ class AccountScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text("screenAccount").tr(),
|
||||
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
||||
? Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 56 + MediaQuery.of(context).padding.top,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 10,
|
||||
sigmaY: 10,
|
||||
),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(
|
||||
clampDouble(10 * 0.1, 0, 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.settings, fill: 1),
|
||||
@ -82,16 +114,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
);
|
||||
}).padding(all: 20),
|
||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||
ListTile(
|
||||
title: Text('accountProfileEdit').tr(),
|
||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.contact_page),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountProfileEdit');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountPublishers').tr(),
|
||||
subtitle: Text('accountPublishersSubtitle').tr(),
|
||||
@ -112,6 +134,36 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
GoRouter.of(context).pushNamed('abuseReport');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('factorSettings').tr(),
|
||||
subtitle: Text('factorSettingsSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.lock),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('factorSettings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountWallet').tr(),
|
||||
subtitle: Text('accountWalletSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.wallet),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountWallet');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountSettings').tr(),
|
||||
subtitle: Text('accountSettingsSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.manage_accounts),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountSettings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountLogout').tr(),
|
||||
subtitle: Text('accountLogoutSubtitle').tr(),
|
||||
@ -133,33 +185,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
await Hive.initFlutter();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountDeletion'.tr()),
|
||||
subtitle: Text('accountDeletionActionDescription'.tr()),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.person_cancel),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'accountDeletion'.tr(),
|
||||
'accountDeletionDescription'.tr(),
|
||||
)
|
||||
.then((value) {
|
||||
if (!value || !context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
sn.client.post('/cgi/id/users/me/deletion').then((value) {
|
||||
if (context.mounted) {
|
||||
context.showSnackbar('accountDeletionSubmitted'.tr());
|
||||
}
|
||||
}).catchError((err) {
|
||||
if (context.mounted) {
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
66
lib/screens/account/account_settings.dart
Normal file
66
lib/screens/account/account_settings.dart
Normal file
@ -0,0 +1,66 @@
|
||||
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:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class AccountSettingsScreen extends StatelessWidget {
|
||||
const AccountSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountSettings').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text('accountProfileEdit').tr(),
|
||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.contact_page),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountProfileEdit');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountDeletion'.tr()),
|
||||
subtitle: Text('accountDeletionActionDescription'.tr()),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.person_cancel),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'accountDeletion'.tr(),
|
||||
'accountDeletionDescription'.tr(),
|
||||
)
|
||||
.then((value) {
|
||||
if (!value || !context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
sn.client.post('/cgi/id/users/me/deletion').then((value) {
|
||||
if (context.mounted) {
|
||||
context.showSnackbar('accountDeletionSubmitted'.tr());
|
||||
}
|
||||
}).catchError((err) {
|
||||
if (context.mounted) {
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
294
lib/screens/account/factor_settings.dart
Normal file
294
lib/screens/account/factor_settings.dart
Normal file
@ -0,0 +1,294 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/auth.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
final Map<int, (String, String, IconData)> kFactorTypes = {
|
||||
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
|
||||
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
|
||||
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
|
||||
3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active),
|
||||
};
|
||||
|
||||
class FactorSettingsScreen extends StatefulWidget {
|
||||
const FactorSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FactorSettingsScreen> createState() => _FactorSettingsScreenState();
|
||||
}
|
||||
|
||||
class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
||||
bool _isBusy = false;
|
||||
List<SnAuthFactor>? _factors;
|
||||
|
||||
Future<void> _fetchFactors() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/me/factors');
|
||||
_factors = List<SnAuthFactor>.from(
|
||||
resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchFactors();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenFactorSettings').tr(),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(
|
||||
isActive: _isBusy,
|
||||
),
|
||||
ListTile(
|
||||
title: Text('authFactorAdd').tr(),
|
||||
subtitle: Text('authFactorAddSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.add),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _FactorNewDialog(
|
||||
currentlyHave: _factors!,
|
||||
),
|
||||
).then((val) {
|
||||
if (val == true) _fetchFactors();
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchFactors,
|
||||
child: ListView.builder(
|
||||
itemCount: _factors?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final ele = _factors![idx];
|
||||
return ListTile(
|
||||
title: Text(kFactorTypes[ele.type]!.$1).tr(),
|
||||
subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 12),
|
||||
leading: Icon(kFactorTypes[ele.type]!.$3),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: ele.type > 0
|
||||
? () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'authFactorDelete'.tr(),
|
||||
'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]),
|
||||
)
|
||||
.then((val) async {
|
||||
if (!val) return;
|
||||
try {
|
||||
if (!context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/id/users/me/factors/${ele.id}');
|
||||
_fetchFactors();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FactorNewDialog extends StatefulWidget {
|
||||
final List<SnAuthFactor> currentlyHave;
|
||||
|
||||
const _FactorNewDialog({required this.currentlyHave});
|
||||
|
||||
@override
|
||||
State<_FactorNewDialog> createState() => _FactorNewDialogState();
|
||||
}
|
||||
|
||||
class _FactorNewDialogState extends State<_FactorNewDialog> {
|
||||
int? _factorType;
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _submit() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.post('/cgi/id/users/me/factors', data: {
|
||||
'type': _factorType,
|
||||
});
|
||||
final factor = SnAuthFactor.fromJson(resp.data);
|
||||
if (!mounted) return;
|
||||
if (factor.type == 2) {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _FactorTotpFactorDialog(factor: factor),
|
||||
);
|
||||
}
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(true);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('authFactorAdd').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<int>(
|
||||
hint: Text(
|
||||
'Select Item',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
value: _factorType,
|
||||
items: kFactorTypes.entries.map(
|
||||
(ele) {
|
||||
final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key);
|
||||
return DropdownMenuItem<int>(
|
||||
enabled: !contains,
|
||||
value: ele.key,
|
||||
child: Text(
|
||||
ele.value.$1.tr(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
).opacity(contains ? 0.75 : 1),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
onChanged: (val) => setState(() {
|
||||
_factorType = val;
|
||||
}),
|
||||
buttonStyleData: ButtonStyleData(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => Navigator.of(context).pop(),
|
||||
child: Text('dialogCancel').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _submit(),
|
||||
child: Text('dialogConfirm').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FactorTotpFactorDialog extends StatelessWidget {
|
||||
final SnAuthFactor factor;
|
||||
|
||||
const _FactorTotpFactorDialog({required this.factor});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
'totpPostSetup',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr().width(280),
|
||||
),
|
||||
const Gap(4),
|
||||
Center(
|
||||
child: Text(
|
||||
'totpPostSetupDescription',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
).tr().width(280),
|
||||
),
|
||||
const Gap(16),
|
||||
QrImageView(
|
||||
padding: EdgeInsets.zero,
|
||||
data: factor.config!['url'],
|
||||
errorCorrectionLevel: QrErrorCorrectLevel.H,
|
||||
version: QrVersions.auto,
|
||||
size: 160,
|
||||
gapless: true,
|
||||
eyeStyle: QrEyeStyle(
|
||||
eyeShape: QrEyeShape.circle,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
dataModuleStyle: QrDataModuleStyle(
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Center(
|
||||
child: Text(
|
||||
'totpNeverShare',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
).tr().bold().width(280),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class ProfileEditScreen extends StatefulWidget {
|
||||
@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
onDateTimeChanged: (DateTime newDate) {
|
||||
setState(() {
|
||||
_birthday = newDate;
|
||||
_birthdayController.text =
|
||||
DateFormat(_kDateFormat).format(_birthday!);
|
||||
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
if (image == null) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final ImageProvider imageProvider =
|
||||
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios = place == 'banner'
|
||||
? [CropAspectRatio(width: 16, height: 7)]
|
||||
: [CropAspectRatio(width: 1, height: 1)];
|
||||
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios =
|
||||
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
|
||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||
? await showCupertinoImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final rawBytes =
|
||||
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List();
|
||||
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
||||
|
||||
try {
|
||||
final attachment = await attach.directUploadOne(
|
||||
@ -212,136 +207,141 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
const Gap(24),
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Material(
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(_banner!),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountProfileEdit').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
const Gap(24),
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Material(
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? 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: () {
|
||||
_updateImage('avatar');
|
||||
_updateImage('banner');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding),
|
||||
const Gap(8 + 28),
|
||||
Column(
|
||||
children: [
|
||||
TextField(
|
||||
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(),
|
||||
),
|
||||
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: () {
|
||||
_updateImage('avatar');
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _lastNameController,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldLastName'.tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding),
|
||||
const Gap(8 + 28),
|
||||
Column(
|
||||
children: [
|
||||
TextField(
|
||||
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),
|
||||
TextField(
|
||||
controller: _birthdayController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldBirthday'.tr(),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
controller: _birthdayController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldBirthday'.tr(),
|
||||
),
|
||||
onTap: () => _selectBirthday(),
|
||||
),
|
||||
onTap: () => _selectBirthday(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding + 8),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isBusy ? null : _updateUserInfo,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text('apply').tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding),
|
||||
],
|
||||
],
|
||||
).padding(horizontal: padding + 8),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isBusy ? null : _updateUserInfo,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text('apply').tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -241,6 +241,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
@ -594,7 +595,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
subtitle: Text('@${ele.name}'),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
GoRouter.of(context).goNamed(
|
||||
'postPublisher',
|
||||
pathParameters: {'name': ele.name},
|
||||
);
|
@ -18,6 +18,7 @@ import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class AccountPublisherEditScreen extends StatefulWidget {
|
||||
@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
|
@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class AccountPublisherNewScreen extends StatefulWidget {
|
||||
const AccountPublisherNewScreen({super.key});
|
||||
@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountPublisherNew').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
|
@ -10,6 +10,7 @@ import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class PublisherScreen extends StatefulWidget {
|
||||
const PublisherScreen({super.key});
|
||||
@ -32,8 +33,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
|
||||
try {
|
||||
final resp = await sn.client.get('/cgi/co/publishers/me');
|
||||
final List<SnPublisher> out = List<SnPublisher>.from(
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
||||
final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountPublishers').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
@ -62,9 +66,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.add_circle),
|
||||
onTap: () {
|
||||
GoRouter.of(context)
|
||||
.pushNamed('accountPublisherNew')
|
||||
.then((value) {
|
||||
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
|
||||
if (value == true) {
|
||||
_publishers.clear();
|
||||
_fetchPublishers();
|
||||
@ -75,48 +77,52 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
const Divider(height: 1),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_publishers.clear();
|
||||
return _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();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_publishers.clear();
|
||||
return _fetchPublishers();
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: _publishers.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final publisher = _publishers[idx];
|
||||
return ListTile(
|
||||
title: Text(publisher.nick),
|
||||
subtitle: Text('@${publisher.name}'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(content: publisher.avatar),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const Gap(16),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'accountPublisherEdit',
|
||||
pathParameters: {
|
||||
'name': publisher.name,
|
||||
},
|
||||
).then((value) {
|
||||
if (value == true) {
|
||||
_publishers.clear();
|
||||
_fetchPublishers();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -11,6 +11,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AlbumScreen extends StatefulWidget {
|
||||
@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
|
@ -7,17 +7,14 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/account/factor_settings.dart';
|
||||
import 'package:surface/types/auth.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import '../../providers/websocket.dart';
|
||||
|
||||
final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
|
||||
0: ('authFactorPassword'.tr(), Symbols.password, false),
|
||||
1: ('authFactorEmail'.tr(), Symbols.email, true),
|
||||
};
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@ -35,67 +32,73 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||
child: SingleChildScrollView(
|
||||
child: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 380),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: switch (_period % 3) {
|
||||
1 => _LoginPickerScreen(
|
||||
key: const ValueKey(1),
|
||||
ticket: _currentTicket,
|
||||
factors: _factors,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onPickFactor: (p0) => setState(() {
|
||||
_factorPicked = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period++;
|
||||
}),
|
||||
),
|
||||
2 => _LoginCheckScreen(
|
||||
key: const ValueKey(2),
|
||||
ticket: _currentTicket,
|
||||
factor: _factorPicked,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period = 1;
|
||||
}),
|
||||
),
|
||||
_ => _LoginLookupScreen(
|
||||
key: const ValueKey(0),
|
||||
ticket: _currentTicket,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onFactor: (p0) => setState(() {
|
||||
_factors = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period++;
|
||||
}),
|
||||
),
|
||||
},
|
||||
).padding(all: 24),
|
||||
).center(),
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAuthLogin').tr(),
|
||||
),
|
||||
body: Theme(
|
||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||
child: SingleChildScrollView(
|
||||
child: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 380),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: switch (_period % 3) {
|
||||
1 => _LoginPickerScreen(
|
||||
key: const ValueKey(1),
|
||||
ticket: _currentTicket,
|
||||
factors: _factors,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onPickFactor: (p0) => setState(() {
|
||||
_factorPicked = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period++;
|
||||
}),
|
||||
),
|
||||
2 => _LoginCheckScreen(
|
||||
key: const ValueKey(2),
|
||||
ticket: _currentTicket,
|
||||
factor: _factorPicked,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period = 1;
|
||||
}),
|
||||
),
|
||||
_ => _LoginLookupScreen(
|
||||
key: const ValueKey(0),
|
||||
ticket: _currentTicket,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onFactor: (p0) => setState(() {
|
||||
_factors = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period++;
|
||||
}),
|
||||
),
|
||||
},
|
||||
).padding(all: 24),
|
||||
).center(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -205,7 +208,9 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
autofillHints: [
|
||||
(_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode
|
||||
widget.factor!.type == 0
|
||||
? AutofillHints.password
|
||||
: AutofillHints.oneTimeCode
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
@ -260,7 +265,8 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
||||
bool _isBusy = false;
|
||||
int? _factorPicked;
|
||||
|
||||
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
||||
|
||||
void _performGetFactorCode() async {
|
||||
if (_factorPicked == null) return;
|
||||
@ -321,11 +327,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
||||
),
|
||||
),
|
||||
secondary: Icon(
|
||||
_factorLabelMap[x.type]?.$2 ?? Symbols.question_mark,
|
||||
kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
|
||||
),
|
||||
title: Text(
|
||||
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
|
||||
),
|
||||
kFactorTypes[x.type]?.$1 ?? 'unknown',
|
||||
).tr(),
|
||||
enabled: !widget.ticket!.factorTrail.contains(x.id),
|
||||
value: _factorPicked == x.id,
|
||||
onChanged: (value) {
|
||||
@ -401,11 +407,14 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final lookupResp = await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
||||
final lookupResp =
|
||||
await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
||||
await sn.client.post('/cgi/id/users/me/password-reset', data: {
|
||||
'user_id': lookupResp.data['id'],
|
||||
});
|
||||
if (mounted) context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||
if (mounted) {
|
||||
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
@ -430,7 +439,8 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
widget.onTicket(result.ticket);
|
||||
|
||||
// Pull factors
|
||||
final factorResp = await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
||||
final factorResp =
|
||||
await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
||||
'ticketId': result.ticket!.id.toString(),
|
||||
});
|
||||
widget.onFactor(
|
||||
@ -441,7 +451,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
|
||||
widget.onNext();
|
||||
} catch (err) {
|
||||
if(mounted) context.showErrorDialog(err);
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
return;
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
@ -524,7 +534,10 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
'termAcceptNextWithAgree'.tr(),
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha((255 * 0.75).round()),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
|
@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class RegisterScreen extends StatefulWidget {
|
||||
@ -54,175 +55,178 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StyledWidget(Container(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CircleAvatar(
|
||||
radius: 26,
|
||||
child: const Icon(
|
||||
Symbols.person_add,
|
||||
size: 28,
|
||||
),
|
||||
).padding(bottom: 8),
|
||||
),
|
||||
Text(
|
||||
'screenAuthRegister',
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAuthRegister').tr(),
|
||||
),
|
||||
body: StyledWidget(Container(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CircleAvatar(
|
||||
radius: 26,
|
||||
child: const Icon(
|
||||
Symbols.person_add,
|
||||
size: 28,
|
||||
),
|
||||
).padding(bottom: 8),
|
||||
),
|
||||
).tr().padding(left: 4, bottom: 16),
|
||||
Form(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 4 || value.length > 32) {
|
||||
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
||||
}
|
||||
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
||||
return 'fieldUsernameAlphanumOnly'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _usernameController,
|
||||
autofillHints: const [AutofillHints.username],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldUsername'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
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()),
|
||||
),
|
||||
Text(
|
||||
'screenAuthRegister',
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
).tr().padding(left: 4, bottom: 16),
|
||||
Form(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 4 || value.length > 32) {
|
||||
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
||||
}
|
||||
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
||||
return 'fieldUsernameAlphanumOnly'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _usernameController,
|
||||
autofillHints: const [AutofillHints.username],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldUsername'.tr(),
|
||||
),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('termAcceptLink'.tr()),
|
||||
const Gap(4),
|
||||
const Icon(Symbols.launch, size: 14),
|
||||
],
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
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(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('termAcceptLink'.tr()),
|
||||
const Gap(4),
|
||||
const Icon(Symbols.launch, size: 14),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
launchUrlString('https://solsynth.dev/terms');
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
launchUrlString('https://solsynth.dev/terms');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
).padding(horizontal: 16),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () => _performAction(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('next').tr(),
|
||||
const Icon(Symbols.chevron_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
).padding(horizontal: 16),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () => _performAction(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('next').tr(),
|
||||
const Icon(Symbols.chevron_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)).padding(all: 24).center();
|
||||
)).padding(all: 24).center(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import 'package:surface/widgets/account/account_select.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenChat').tr(),
|
||||
@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenChat').tr(),
|
||||
@ -195,22 +196,58 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
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];
|
||||
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,
|
||||
);
|
||||
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(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
||||
title: Text(channel.name),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
@ -218,15 +255,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text(
|
||||
'channelDirectMessageDescription'.tr(args: [
|
||||
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
||||
]),
|
||||
channel.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
||||
content: null,
|
||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
@ -240,39 +276,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
title: Text(channel.name),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text(
|
||||
channel.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: null,
|
||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {
|
||||
'scope': channel.realm?.alias ?? 'global',
|
||||
'alias': channel.alias,
|
||||
},
|
||||
).then((value) {
|
||||
if (value == true) _refreshChannels();
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -9,10 +9,12 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/widgets/chat/call/call_controls.dart';
|
||||
import 'package:surface/widgets/chat/call/call_participant.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class CallRoomScreen extends StatefulWidget {
|
||||
final String scope;
|
||||
final String alias;
|
||||
|
||||
const CallRoomScreen({super.key, required this.scope, required this.alias});
|
||||
|
||||
@override
|
||||
@ -35,8 +37,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
||||
child: call.focusTrack != null
|
||||
? InteractiveParticipantWidget(
|
||||
isFixedAvatar: false,
|
||||
@ -71,8 +72,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
color: Theme.of(context).cardColor,
|
||||
participant: track,
|
||||
onTap: () {
|
||||
if (track.participant.sid !=
|
||||
call.focusTrack?.participant.sid) {
|
||||
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
||||
call.setFocusTrack(track);
|
||||
}
|
||||
},
|
||||
@ -114,14 +114,10 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: InteractiveParticipantWidget(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh
|
||||
.withOpacity(0.75),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
|
||||
participant: track,
|
||||
onTap: () {
|
||||
if (track.participant.sid !=
|
||||
call.focusTrack?.participant.sid) {
|
||||
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
||||
call.setFocusTrack(track);
|
||||
}
|
||||
},
|
||||
@ -152,157 +148,134 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
return ListenableBuilder(
|
||||
listenable: call,
|
||||
builder: (context, _) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: 'call'.tr(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge!
|
||||
.copyWith(color: Colors.white),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: call.lastDuration.toString(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: Colors.white),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Column(
|
||||
children: [
|
||||
body: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Column(
|
||||
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(
|
||||
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();
|
||||
}
|
||||
},
|
||||
),
|
||||
child: ControlsWidget(
|
||||
call.room,
|
||||
call.room.localParticipant!,
|
||||
),
|
||||
),
|
||||
if (call.room.localParticipant != null)
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: ControlsWidget(
|
||||
call.room,
|
||||
call.room.localParticipant!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {},
|
||||
],
|
||||
),
|
||||
onTap: () {},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
@ -14,6 +14,7 @@ import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class ChannelDetailScreen extends StatefulWidget {
|
||||
@ -189,7 +190,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
|
||||
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
|
||||
),
|
||||
|
@ -12,6 +12,7 @@ import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChatManageScreen extends StatefulWidget {
|
||||
@ -121,7 +122,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.editingChannelAlias != null
|
||||
? Text('screenChatManage').tr()
|
||||
|
@ -20,6 +20,7 @@ import 'package:surface/widgets/chat/chat_message_input.dart';
|
||||
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
import '../../providers/user_directory.dart';
|
||||
@ -211,7 +212,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
final call = context.watch<ChatCallProvider>();
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_channel?.type == 1
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||
@ -6,11 +7,14 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/post/post_detail.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
@ -93,7 +97,9 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: ExpandableFab(
|
||||
key: _fabKey,
|
||||
@ -210,6 +216,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverGap(12),
|
||||
SliverInfiniteList(
|
||||
itemCount: _posts.length,
|
||||
isLoading: _isBusy,
|
||||
@ -217,27 +224,39 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
||||
onFetchData: _fetchPosts,
|
||||
itemBuilder: (context, idx) {
|
||||
return GestureDetector(
|
||||
child: PostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
_refreshPosts();
|
||||
},
|
||||
return Center(
|
||||
child: OpenContainer(
|
||||
closedBuilder: (_, __) => Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: PostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
_refreshPosts();
|
||||
},
|
||||
),
|
||||
),
|
||||
openBuilder: (_, close) => PostDetailScreen(
|
||||
slug: _posts[idx].id.toString(),
|
||||
preload: _posts[idx],
|
||||
onBack: close,
|
||||
),
|
||||
openColor: Colors.transparent,
|
||||
openElevation: 0,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -11,6 +11,7 @@ import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
import '../providers/userinfo.dart';
|
||||
import '../widgets/unauthorized_hint.dart';
|
||||
@ -180,7 +181,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenFriend').tr(),
|
||||
@ -191,7 +192,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenFriend').tr(),
|
||||
@ -233,52 +234,56 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
if (_requests.isNotEmpty || _blocks.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.wait([
|
||||
_fetchRelations(),
|
||||
_fetchRequests(),
|
||||
]),
|
||||
child: ListView.builder(
|
||||
itemCount: _relations.length,
|
||||
itemBuilder: (context, index) {
|
||||
final relation = _relations[index];
|
||||
final other = relation.related;
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||
leading: AccountImage(content: other?.avatar),
|
||||
title: Text(other?.nick ?? 'unknown'),
|
||||
subtitle: Text(other?.nick ?? 'unknown'),
|
||||
trailing: SizedBox(
|
||||
height: 48,
|
||||
width: 120,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _changeRelation(relation, 2),
|
||||
child: Text('friendBlock').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
InkWell(
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _deleteRelation(relation),
|
||||
child: Text('friendDeleteAction').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.wait([
|
||||
_fetchRelations(),
|
||||
_fetchRequests(),
|
||||
]),
|
||||
child: ListView.builder(
|
||||
itemCount: _relations.length,
|
||||
itemBuilder: (context, index) {
|
||||
final relation = _relations[index];
|
||||
final other = relation.related;
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||
leading: AccountImage(content: other?.avatar),
|
||||
title: Text(other?.nick ?? 'unknown'),
|
||||
subtitle: Text(other?.nick ?? 'unknown'),
|
||||
trailing: SizedBox(
|
||||
height: 48,
|
||||
width: 120,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _changeRelation(relation, 2),
|
||||
child: Text('friendBlock').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
InkWell(
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _deleteRelation(relation),
|
||||
child: Text('friendDeleteAction').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -9,6 +9,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
@ -22,9 +23,11 @@ import 'package:surface/providers/special_day.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/widget.dart';
|
||||
import 'package:surface/types/check_in.dart';
|
||||
import 'package:surface/types/news.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
|
||||
class HomeScreenDashEntry {
|
||||
@ -48,12 +51,12 @@ class HomeScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
static const List<HomeScreenDashEntry> kCards = [
|
||||
late final List<HomeScreenDashEntry> kCards = [
|
||||
HomeScreenDashEntry(
|
||||
name: 'dashEntryRecommendation',
|
||||
cols: 2,
|
||||
rows: 2,
|
||||
child: _HomeDashRecommendationPostWidget(),
|
||||
rows: 2,
|
||||
cols: 2,
|
||||
),
|
||||
HomeScreenDashEntry(
|
||||
name: 'dashEntryCheckIn',
|
||||
@ -63,11 +66,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
name: 'dashEntryNotification',
|
||||
child: _HomeDashNotificationWidget(),
|
||||
),
|
||||
HomeScreenDashEntry(
|
||||
name: 'dashEntryTodayNews',
|
||||
child: _HomeDashTodayNews(),
|
||||
cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text("screenHome").tr(),
|
||||
@ -229,6 +237,105 @@ 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(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
_article!.title,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18),
|
||||
maxLines: MediaQuery.of(context).size.width >= 640 ? 2 : 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
parse(_article!.description).children.map((e) => e.text.trim()).join(),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75);
|
||||
}),
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'newsDetail',
|
||||
pathParameters: {'hash': _article!.hash},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeDashCheckInWidget extends StatefulWidget {
|
||||
const _HomeDashCheckInWidget();
|
||||
|
||||
@ -387,6 +494,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
Text(
|
||||
'dailyCheckInNone',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
],
|
||||
)
|
||||
@ -404,6 +513,11 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
'+${_todayRecord!.resultExperience} EXP',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (_todayRecord!.resultCoin >= 0)
|
||||
Text(
|
||||
'+${_todayRecord!.resultCoin} ${'walletCurrencyShort'.tr()}',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
241
lib/screens/news/news_detail.dart
Normal file
241
lib/screens/news/news_detail.dart
Normal file
@ -0,0 +1,241 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/news.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class NewsDetailScreen extends StatefulWidget {
|
||||
final String hash;
|
||||
|
||||
const NewsDetailScreen({super.key, required this.hash});
|
||||
|
||||
@override
|
||||
State<NewsDetailScreen> createState() => _NewsDetailScreenState();
|
||||
}
|
||||
|
||||
class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
SnNewsArticle? _article;
|
||||
dom.Document? _articleFragment;
|
||||
|
||||
Future<void> _fetchArticle() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
|
||||
_article = SnNewsArticle.fromJson(resp.data);
|
||||
_articleFragment = parse(_article!.content);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err).then((_) {
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context);
|
||||
});
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _parseHtmlToWidgets(Iterable<dom.Element>? elements) {
|
||||
if (elements == null) return [];
|
||||
|
||||
final List<Widget> widgets = [];
|
||||
|
||||
for (final node in elements) {
|
||||
switch (node.localName) {
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
widgets.add(Text(node.text.trim(), style: Theme.of(context).textTheme.titleMedium));
|
||||
break;
|
||||
case 'p':
|
||||
if (node.text.trim().isEmpty) continue;
|
||||
widgets.add(
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: node.text.trim(),
|
||||
children: [
|
||||
for (final child in node.children)
|
||||
switch (child.localName) {
|
||||
'a' => TextSpan(
|
||||
text: child.text.trim(),
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlString(child.attributes['href']!);
|
||||
},
|
||||
),
|
||||
_ => TextSpan(text: child.text.trim()),
|
||||
},
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'a':
|
||||
// drop single link
|
||||
break;
|
||||
case 'div':
|
||||
// ignore div text, normally it is not meaningful
|
||||
widgets.addAll(_parseHtmlToWidgets(node.children));
|
||||
break;
|
||||
case 'hr':
|
||||
widgets.add(const Divider());
|
||||
break;
|
||||
case 'img':
|
||||
var src = node.attributes['src'];
|
||||
if (src == null) break;
|
||||
final width = double.tryParse(node.attributes['width'] ?? 'null');
|
||||
final height = double.tryParse(node.attributes['height'] ?? 'null');
|
||||
final ratio = width != null && height != null ? width / height : 1.0;
|
||||
if (src.startsWith('//')) {
|
||||
src = 'https:$src';
|
||||
} else if (!src.startsWith('http')) {
|
||||
final baseUri = Uri.parse(_article!.url);
|
||||
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||
src = '$baseUrl/$src';
|
||||
}
|
||||
widgets.add(
|
||||
AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
height: height ?? double.infinity,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AutoResizeUniversalImage(
|
||||
src,
|
||||
fit: width != null && height != null ? BoxFit.cover : BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
widgets.addAll(_parseHtmlToWidgets(node.children));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchArticle();
|
||||
}
|
||||
|
||||
bool _isReadingFromReader = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text(_article?.title ?? 'loading'.tr()),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
MaterialBanner(
|
||||
dividerColor: Colors.transparent,
|
||||
leading: const Icon(Icons.info),
|
||||
content: Text(_isReadingFromReader ? 'newsReadingFromReader'.tr() : 'newsReadingFromOriginal'.tr()),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('newsReadingProviderSwap').tr(),
|
||||
onPressed: () {
|
||||
setState(() => _isReadingFromReader = !_isReadingFromReader);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_articleFragment != null && _isReadingFromReader)
|
||||
Expanded(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
|
||||
Builder(builder: (context) {
|
||||
final htmlDescription = parse(_article!.description);
|
||||
return Text(
|
||||
htmlDescription.children.map((ele) => ele.text.trim()).join(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
);
|
||||
}),
|
||||
Builder(builder: (context) {
|
||||
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||
return Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75);
|
||||
}),
|
||||
Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75),
|
||||
const Divider(),
|
||||
..._parseHtmlToWidgets(_articleFragment!.children),
|
||||
const Divider(),
|
||||
InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Reference from original website',
|
||||
style: TextStyle(decoration: TextDecoration.underline),
|
||||
),
|
||||
const Gap(4),
|
||||
Icon(Icons.launch, size: 16),
|
||||
],
|
||||
).opacity(0.85),
|
||||
onTap: () {
|
||||
launchUrlString(_article!.url);
|
||||
},
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
).padding(horizontal: 12, vertical: 16),
|
||||
),
|
||||
).center(),
|
||||
)
|
||||
else if (_article != null)
|
||||
Expanded(
|
||||
child: InAppWebView(
|
||||
key: GlobalKey(),
|
||||
initialUrlRequest: URLRequest(url: WebUri(_article!.url)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
239
lib/screens/news/news_list.dart
Normal file
239
lib/screens/news/news_list.dart
Normal file
@ -0,0 +1,239 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/news.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class NewsScreen extends StatefulWidget {
|
||||
const NewsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<NewsScreen> createState() => _NewsScreenState();
|
||||
}
|
||||
|
||||
class _NewsScreenState extends State<NewsScreen> {
|
||||
List<SnNewsSource>? _sources;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_fetchSources();
|
||||
}
|
||||
|
||||
Future<void> _fetchSources() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/re/well-known/sources');
|
||||
_sources = List<SnNewsSource>.from(
|
||||
resp.data?.map((e) => SnNewsSource.fromJson(e)) ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_sources == null) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNews').tr(),
|
||||
),
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return DefaultTabController(
|
||||
length: _sources!.length + 1,
|
||||
child: AppScaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverOverlapAbsorber(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: SliverAppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNews').tr(),
|
||||
floating: true,
|
||||
snap: true,
|
||||
bottom: TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(child: Text('newsAllSources'.tr()).textColor(Theme.of(context).appBarTheme.foregroundColor)),
|
||||
for (final source in _sources!)
|
||||
Tab(
|
||||
child: Text(source.label).textColor(Theme.of(context).appBarTheme.foregroundColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
body: TabBarView(
|
||||
children: [
|
||||
_NewsArticleListWidget(allSources: _sources!),
|
||||
for (final source in _sources!)
|
||||
_NewsArticleListWidget(
|
||||
source: source.id,
|
||||
allSources: _sources!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NewsArticleListWidget extends StatefulWidget {
|
||||
final String? source;
|
||||
final List<SnNewsSource> allSources;
|
||||
|
||||
const _NewsArticleListWidget({this.source, required this.allSources});
|
||||
|
||||
@override
|
||||
State<_NewsArticleListWidget> createState() => _NewsArticleListWidgetState();
|
||||
}
|
||||
|
||||
class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
|
||||
bool _isBusy = false;
|
||||
|
||||
int? _totalCount;
|
||||
final List<SnNewsArticle> _articles = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchArticles() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/re/news', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _articles.length,
|
||||
if (widget.source != null) 'source': widget.source,
|
||||
});
|
||||
_totalCount = resp.data['count'];
|
||||
_articles.addAll(List<SnNewsArticle>.from(
|
||||
resp.data['data']?.map((e) => SnNewsArticle.fromJson(e)) ?? [],
|
||||
));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchArticles();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchArticles,
|
||||
child: InfiniteList(
|
||||
isLoading: _isBusy,
|
||||
itemCount: _articles.length,
|
||||
hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
|
||||
onFetchData: () {
|
||||
_fetchArticles();
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final article = _articles[index];
|
||||
|
||||
final baseUri = Uri.parse(article.url);
|
||||
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||
|
||||
final htmlDescription = parse(article.description);
|
||||
final date = article.publishedAt ?? article.createdAt;
|
||||
|
||||
return Card(
|
||||
child: InkWell(
|
||||
radius: 8,
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'newsDetail',
|
||||
pathParameters: {'hash': article.hash},
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(8),
|
||||
topLeft: Radius.circular(8),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AutoResizeUniversalImage(
|
||||
article.thumbnail.startsWith('http')
|
||||
? article.thumbnail
|
||||
: '$baseUrl/${article.thumbnail}',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
|
||||
.textStyle(Theme.of(context).textTheme.bodyMedium!)
|
||||
.padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(widget.allSources.where((x) => x.id == article.source).first.label)
|
||||
.textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75).padding(horizontal: 16),
|
||||
Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75).padding(horizontal: 16),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
@ -37,9 +38,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
|
||||
static const Map<String, IconData> kNotificationTopicIcons = {
|
||||
'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,
|
||||
};
|
||||
|
||||
Future<void> _fetchNotifications() async {
|
||||
@ -137,7 +140,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNotification').tr(),
|
||||
@ -148,7 +151,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNotification').tr(),
|
||||
@ -206,10 +209,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
if (nty.subtitle != null) const Gap(4),
|
||||
MarkdownTextContent(
|
||||
content: nty.body,
|
||||
isAutoWarp: true,
|
||||
isSelectable: true,
|
||||
SelectionArea(
|
||||
child: MarkdownTextContent(
|
||||
content: nty.body,
|
||||
isAutoWarp: true,
|
||||
),
|
||||
),
|
||||
if ([
|
||||
'interactive.feedback',
|
||||
|
@ -13,6 +13,8 @@ import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_background.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
@ -20,12 +22,9 @@ import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
class PostDetailScreen extends StatefulWidget {
|
||||
final String slug;
|
||||
final SnPost? preload;
|
||||
final Function? onBack;
|
||||
|
||||
const PostDetailScreen({
|
||||
super.key,
|
||||
required this.slug,
|
||||
this.preload,
|
||||
});
|
||||
const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
|
||||
|
||||
@override
|
||||
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
||||
@ -67,121 +66,129 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
final ua = context.watch<UserProvider>();
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (GoRouter.of(context).canPop()) {
|
||||
GoRouter.of(context).pop(context);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).replaceNamed('explore');
|
||||
},
|
||||
),
|
||||
title: _data?.body['title'] != null
|
||||
? RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: 'postDetail'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
]),
|
||||
)
|
||||
: Text('postDetail').tr(),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: LoadingIndicator(isActive: _isBusy),
|
||||
return AppBackground(
|
||||
isRoot: widget.onBack != null,
|
||||
child: AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (widget.onBack != null) {
|
||||
widget.onBack!.call();
|
||||
}
|
||||
if (GoRouter.of(context).canPop()) {
|
||||
GoRouter.of(context).pop(context);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).replaceNamed('explore');
|
||||
},
|
||||
),
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
data: _data!,
|
||||
maxWidth: 640,
|
||||
showComments: false,
|
||||
showFullPost: true,
|
||||
onChanged: (data) {
|
||||
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,
|
||||
title: _data?.body['title'] != null
|
||||
? RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PostMiniEditor(
|
||||
postReplyId: _data!.id,
|
||||
onPost: () {
|
||||
setState(() {
|
||||
_data = _data!.copyWith(
|
||||
metric: _data!.metric.copyWith(
|
||||
replyCount: _data!.metric.replyCount + 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
_childListKey.currentState!.refresh();
|
||||
),
|
||||
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)
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
data: _data!,
|
||||
maxWidth: 640,
|
||||
showComments: false,
|
||||
showFullPost: true,
|
||||
onChanged: (data) {
|
||||
setState(() => _data = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
).center(),
|
||||
),
|
||||
if (_data != null)
|
||||
PostCommentSliverList(
|
||||
key: _childListKey,
|
||||
parentPostId: _data!.id,
|
||||
maxWidth: 640,
|
||||
),
|
||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
||||
import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||
@ -54,7 +55,9 @@ class PostEditorScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
final PostWriteController _writeController = PostWriteController();
|
||||
late final PostWriteController _writeController = PostWriteController(
|
||||
doLoadFromTemporary: widget.postEditId == null,
|
||||
);
|
||||
|
||||
bool _isFetching = false;
|
||||
|
||||
@ -126,7 +129,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
return ListenableBuilder(
|
||||
listenable: _writeController,
|
||||
builder: (context, _) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
@ -301,19 +304,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
],
|
||||
),
|
||||
// Content Input Area
|
||||
TextField(
|
||||
controller: _writeController.contentController,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'fieldPostContent'.tr(),
|
||||
hintStyle: TextStyle(fontSize: 14),
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: TextField(
|
||||
controller: _writeController.contentController,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'fieldPostContent'.tr(),
|
||||
hintStyle: TextStyle(fontSize: 14),
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
]
|
||||
.expandIndexed(
|
||||
@ -364,6 +370,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(
|
||||
child: _writeController.temporaryRestored
|
||||
? Container(
|
||||
@ -394,15 +409,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
)
|
||||
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
|
||||
.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(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_tags_field.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
@ -119,7 +120,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('screenPostSearch').tr(),
|
||||
actions: [
|
||||
|
@ -17,6 +17,7 @@ import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
@ -274,7 +275,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
body: NestedScrollView(
|
||||
controller: _scrollController,
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
|
@ -12,6 +12,7 @@ import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
@ -83,7 +84,7 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenRealm').tr(),
|
||||
@ -94,7 +95,7 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenRealm').tr(),
|
||||
@ -118,113 +119,61 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchRealms,
|
||||
child: ListView.builder(
|
||||
itemCount: _realms?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final realm = _realms![idx];
|
||||
if (_isCompactView) {
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: realm.avatar,
|
||||
fallbackWidget: const Icon(Symbols.group, size: 20),
|
||||
),
|
||||
title: Text(realm.name),
|
||||
subtitle: Text(
|
||||
realm.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const Gap(16),
|
||||
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,
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchRealms,
|
||||
child: ListView.builder(
|
||||
itemCount: _realms?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final realm = _realms![idx];
|
||||
if (_isCompactView) {
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: realm.avatar,
|
||||
fallbackWidget: const Icon(Symbols.group, size: 20),
|
||||
),
|
||||
title: Text(realm.name),
|
||||
subtitle: Text(
|
||||
realm.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
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 Icon(Symbols.edit),
|
||||
const Gap(16),
|
||||
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);
|
||||
},
|
||||
),
|
||||
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: () {
|
||||
@ -233,10 +182,69 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
pathParameters: {'alias': realm.alias},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(12),
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: (realm.banner?.isEmpty ?? true)
|
||||
? const SizedBox.shrink()
|
||||
: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(realm.banner!),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -30,
|
||||
left: 18,
|
||||
child: AccountImage(
|
||||
content: realm.avatar,
|
||||
radius: 24,
|
||||
fallbackWidget: const Icon(Symbols.group, size: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(20 + 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
|
||||
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).padding(horizontal: 24, bottom: 14),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmDetail',
|
||||
pathParameters: {'alias': realm.alias},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
).center();
|
||||
},
|
||||
).center();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -18,6 +18,7 @@ import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@ -179,7 +180,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.editingRealmAlias != null
|
||||
? Text('screenRealmManage').tr()
|
||||
|
@ -8,13 +8,13 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
import '../../types/post.dart';
|
||||
|
||||
class RealmDetailScreen extends StatefulWidget {
|
||||
final String alias;
|
||||
|
||||
@ -70,19 +70,11 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
child: AppScaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
// These are the slivers that show up in the "outer" scroll view.
|
||||
return <Widget>[
|
||||
SliverOverlapAbsorber(
|
||||
// This widget takes the overlapping behavior of the SliverAppBar,
|
||||
// and redirects it to the SliverOverlapInjector below. If it is
|
||||
// missing, then it is possible for the nested "inner" scroll view
|
||||
// below to end up under the SliverAppBar even when the inner
|
||||
// scroll view thinks it has not been scrolled.
|
||||
// This is not necessary if the "headerSliverBuilder" only builds
|
||||
// widgets that do not overlap the next sliver.
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: SliverAppBar(
|
||||
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||
@ -428,7 +420,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const Gap(16),
|
||||
const Gap(8),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.edit),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
|
@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/theme.dart';
|
||||
import 'package:surface/theme.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
const Map<String, Color> kColorSchemes = {
|
||||
'colorSchemeIndigo': Colors.indigo,
|
||||
@ -67,7 +68,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenSettings').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
@ -77,6 +82,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('settingsAppearance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
ListTile(
|
||||
title: Text('settingsDisplayLanguage').tr(),
|
||||
subtitle: Text('settingsDisplayLanguageDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.translate),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<Locale?>(
|
||||
isExpanded: true,
|
||||
items: [
|
||||
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
|
||||
return DropdownMenuItem<Locale?>(
|
||||
value: ele,
|
||||
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
|
||||
);
|
||||
}),
|
||||
DropdownMenuItem<Locale?>(
|
||||
value: null,
|
||||
child: Text('settingsDisplayLanguageSystem').tr().fontSize(14),
|
||||
),
|
||||
],
|
||||
value: EasyLocalization.of(context)!.currentLocale,
|
||||
onChanged: (Locale? value) {
|
||||
if (value != null) {
|
||||
EasyLocalization.of(context)!.setLocale(value);
|
||||
} else {
|
||||
EasyLocalization.of(context)!.resetLocale();
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 5,
|
||||
),
|
||||
height: 40,
|
||||
width: 160,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb)
|
||||
ListTile(
|
||||
title: Text('settingsBackgroundImage').tr(),
|
||||
@ -120,7 +167,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
subtitle: Text('settingsThemeMaterial3Description').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
secondary: const Icon(Symbols.new_releases),
|
||||
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false,
|
||||
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_prefs.setBool(
|
||||
@ -142,30 +189,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value);
|
||||
final color = await showDialog<Color?>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
pickerColor: pickerColor,
|
||||
onColorChanged: (color) => pickerColor = color,
|
||||
enableAlpha: false,
|
||||
hexInputBar: true,
|
||||
builder: (context) =>
|
||||
AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
pickerColor: pickerColor,
|
||||
onColorChanged: (color) => pickerColor = color,
|
||||
enableAlpha: false,
|
||||
hexInputBar: true,
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('dialogDismiss').tr(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('dialogConfirm').tr(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(pickerColor);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('dialogDismiss').tr(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('dialogConfirm').tr(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(pickerColor);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (color == null || !context.mounted) return;
|
||||
@ -201,11 +249,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
value: _prefs.getInt(kAppColorSchemeStoreKey) == null
|
||||
? 1
|
||||
: kColorSchemes.values
|
||||
.toList()
|
||||
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
|
||||
.toList()
|
||||
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
|
||||
onChanged: (int? value) {
|
||||
if (value != null && value != -1) {
|
||||
_prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values.elementAt(value).value);
|
||||
_prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values
|
||||
.elementAt(value)
|
||||
.value);
|
||||
final th = context.read<ThemeProvider>();
|
||||
th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value));
|
||||
setState(() {});
|
||||
@ -240,6 +290,61 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.left_panel_close),
|
||||
title: Text('settingsDrawerPreferCollapse').tr(),
|
||||
subtitle: Text('settingsDrawerPreferCollapseDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
|
||||
onChanged: (value) {
|
||||
_prefs.setBool(kAppDrawerPreferCollapse, value ?? false);
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
cfg.calcDrawerSize(context);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('settingsFeatures').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.vibration),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
title: Text('settingsNotifyWithHaptic').tr(),
|
||||
subtitle: Text('settingsNotifyWithHapticDescription').tr(),
|
||||
value: _prefs.getBool(kAppNotifyWithHaptic) ?? true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_prefs.setBool(kAppNotifyWithHaptic, value ?? false);
|
||||
});
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.link),
|
||||
title: Text('settingsExpandPostLink').tr(),
|
||||
subtitle: Text('settingsExpandPostLinkDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
value: _prefs.getBool(kAppExpandPostLink) ?? true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_prefs.setBool(kAppExpandPostLink, value ?? false);
|
||||
});
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.chat),
|
||||
title: Text('settingsExpandChatLink').tr(),
|
||||
subtitle: Text('settingsExpandChatLinkDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
value: _prefs.getBool(kAppExpandChatLink) ?? true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_prefs.setBool(kAppExpandChatLink, value ?? false);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
@ -282,7 +387,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
('Custom', _serverUrlController.text),
|
||||
]
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
(item) =>
|
||||
DropdownMenuItem<String>(
|
||||
value: item.$2,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
@ -294,7 +400,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
.toList(),
|
||||
value: _serverUrlController.text,
|
||||
onChanged: (String? value) {
|
||||
@ -349,11 +455,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
isExpanded: true,
|
||||
items: kImageQualityLevel.entries
|
||||
.map(
|
||||
(item) => DropdownMenuItem<FilterQuality>(
|
||||
(item) =>
|
||||
DropdownMenuItem<FilterQuality>(
|
||||
value: item.value,
|
||||
child: Text(item.key).tr().fontSize(14),
|
||||
),
|
||||
)
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (FilterQuality? value) {
|
||||
if (value == null) return;
|
||||
|
279
lib/screens/wallet.dart
Normal file
279
lib/screens/wallet.dart
Normal file
@ -0,0 +1,279 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/wallet.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class WalletScreen extends StatefulWidget {
|
||||
const WalletScreen({super.key});
|
||||
|
||||
@override
|
||||
State<WalletScreen> createState() => _WalletScreenState();
|
||||
}
|
||||
|
||||
class _WalletScreenState extends State<WalletScreen> {
|
||||
bool _isBusy = false;
|
||||
SnWallet? _wallet;
|
||||
|
||||
Future<void> _fetchWallet() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/wa/wallets/me');
|
||||
_wallet = SnWallet.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchWallet();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountWallet').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (_wallet == null)
|
||||
Expanded(
|
||||
child: _CreateWalletWidget(
|
||||
onCreate: () {
|
||||
_fetchWallet();
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
child: Icon(Symbols.wallet, size: 28),
|
||||
),
|
||||
const Gap(12),
|
||||
SizedBox(width: double.infinity),
|
||||
Text(
|
||||
NumberFormat.compactCurrency(
|
||||
locale: EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
symbol: '${'walletCurrencyShort'.tr()} ',
|
||||
decimalDigits: 2,
|
||||
).format(double.parse(_wallet!.balance)),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text('walletCurrency'.plural(double.parse(_wallet!.balance))),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 24),
|
||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||
if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletTransactionList extends StatefulWidget {
|
||||
final SnWallet myself;
|
||||
|
||||
const _WalletTransactionList({required this.myself});
|
||||
|
||||
@override
|
||||
State<_WalletTransactionList> createState() => _WalletTransactionListState();
|
||||
}
|
||||
|
||||
class _WalletTransactionListState extends State<_WalletTransactionList> {
|
||||
bool _isBusy = false;
|
||||
int? _totalCount;
|
||||
final List<SnTransaction> _transactions = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchTransactions() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/wa/transactions/me', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _transactions.length,
|
||||
});
|
||||
_totalCount = resp.data['count'];
|
||||
_transactions.addAll(
|
||||
resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchTransactions();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchTransactions,
|
||||
child: InfiniteList(
|
||||
itemCount: _transactions.length,
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!,
|
||||
onFetchData: () {
|
||||
_fetchTransactions();
|
||||
},
|
||||
itemBuilder: (context, idx) {
|
||||
final ele = _transactions[idx];
|
||||
final isIncoming = ele.payeeId == widget.myself.id;
|
||||
return ListTile(
|
||||
leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made),
|
||||
title: Text(
|
||||
'${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
|
||||
style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(ele.remark),
|
||||
const Gap(2),
|
||||
Text(
|
||||
DateFormat(
|
||||
null,
|
||||
EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
).format(ele.createdAt),
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateWalletWidget extends StatefulWidget {
|
||||
final Function()? onCreate;
|
||||
|
||||
const _CreateWalletWidget({required this.onCreate});
|
||||
|
||||
@override
|
||||
State<_CreateWalletWidget> createState() => _CreateWalletWidgetState();
|
||||
}
|
||||
|
||||
class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _createWallet() async {
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
final password = await showDialog<String?>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('walletCreate').tr(),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('walletCreatePassword').tr(),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
controller: passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPassword'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text('cancel').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop(passwordController.text);
|
||||
},
|
||||
child: Text('next').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
passwordController.dispose();
|
||||
});
|
||||
if (password == null || password.isEmpty) return;
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/wa/wallets/me', data: {
|
||||
'password': password,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
child: Icon(Symbols.add, size: 28),
|
||||
),
|
||||
const Gap(12),
|
||||
Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(),
|
||||
const Gap(8),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: _isBusy ? null : () => _createWallet(),
|
||||
child: Text('next').tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3
|
||||
|
||||
Future<ThemeData> createAppTheme(
|
||||
Brightness brightness, {
|
||||
Color? seedColorOverride,
|
||||
Color? seedColorOverride,
|
||||
bool? useMaterial3,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@ -34,9 +34,10 @@ Future<ThemeData> createAppTheme(
|
||||
);
|
||||
|
||||
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false),
|
||||
useMaterial3: useM3,
|
||||
colorScheme: colorScheme,
|
||||
brightness: brightness,
|
||||
iconTheme: IconThemeData(
|
||||
@ -45,12 +46,24 @@ Future<ThemeData> createAppTheme(
|
||||
opticalSize: 20,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: hasAppBarBlurry ? 0 : null,
|
||||
backgroundColor: hasAppBarBlurry ? colorScheme.surfaceContainer.withAlpha(200) : colorScheme.primary,
|
||||
backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
|
||||
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.transparent,
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
|
||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ class SnAccount with _$SnAccount {
|
||||
required DateTime? deletedAt,
|
||||
required DateTime? confirmedAt,
|
||||
required List<SnAccountContact>? contacts,
|
||||
required String avatar,
|
||||
required String banner,
|
||||
@Default("") String avatar,
|
||||
@Default("") String banner,
|
||||
required String description,
|
||||
required String name,
|
||||
required String nick,
|
||||
|
@ -367,8 +367,8 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
required this.deletedAt,
|
||||
required this.confirmedAt,
|
||||
required final List<SnAccountContact>? contacts,
|
||||
required this.avatar,
|
||||
required this.banner,
|
||||
this.avatar = "",
|
||||
this.banner = "",
|
||||
required this.description,
|
||||
required this.name,
|
||||
required this.nick,
|
||||
@ -410,8 +410,10 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final String avatar;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String banner;
|
||||
@override
|
||||
final String description;
|
||||
@ -540,8 +542,8 @@ abstract class _SnAccount extends SnAccount {
|
||||
required final DateTime? deletedAt,
|
||||
required final DateTime? confirmedAt,
|
||||
required final List<SnAccountContact>? contacts,
|
||||
required final String avatar,
|
||||
required final String banner,
|
||||
final String avatar,
|
||||
final String banner,
|
||||
required final String description,
|
||||
required final String name,
|
||||
required final String nick,
|
||||
|
@ -20,8 +20,8 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
|
||||
contacts: (json['contacts'] as List<dynamic>?)
|
||||
?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
avatar: json['avatar'] as String,
|
||||
banner: json['banner'] as String,
|
||||
avatar: json['avatar'] as String? ?? "",
|
||||
banner: json['banner'] as String? ?? "",
|
||||
description: json['description'] as String,
|
||||
name: json['name'] as String,
|
||||
nick: json['nick'] as String,
|
||||
|
@ -16,6 +16,7 @@ class SnCheckInRecord with _$SnCheckInRecord {
|
||||
required DateTime? deletedAt,
|
||||
required int resultTier,
|
||||
required int resultExperience,
|
||||
required double resultCoin,
|
||||
required List<int> resultModifiers,
|
||||
required int accountId,
|
||||
}) = _SnCheckInRecord;
|
||||
|
@ -26,6 +26,7 @@ mixin _$SnCheckInRecord {
|
||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||
int get resultTier => throw _privateConstructorUsedError;
|
||||
int get resultExperience => throw _privateConstructorUsedError;
|
||||
double get resultCoin => throw _privateConstructorUsedError;
|
||||
List<int> get resultModifiers => throw _privateConstructorUsedError;
|
||||
int get accountId => throw _privateConstructorUsedError;
|
||||
|
||||
@ -52,6 +53,7 @@ abstract class $SnCheckInRecordCopyWith<$Res> {
|
||||
DateTime? deletedAt,
|
||||
int resultTier,
|
||||
int resultExperience,
|
||||
double resultCoin,
|
||||
List<int> resultModifiers,
|
||||
int accountId});
|
||||
}
|
||||
@ -77,6 +79,7 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
||||
Object? deletedAt = freezed,
|
||||
Object? resultTier = null,
|
||||
Object? resultExperience = null,
|
||||
Object? resultCoin = null,
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
@ -105,6 +108,10 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
||||
? _value.resultExperience
|
||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultCoin: null == resultCoin
|
||||
? _value.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _value.resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
@ -132,6 +139,7 @@ abstract class _$$SnCheckInRecordImplCopyWith<$Res>
|
||||
DateTime? deletedAt,
|
||||
int resultTier,
|
||||
int resultExperience,
|
||||
double resultCoin,
|
||||
List<int> resultModifiers,
|
||||
int accountId});
|
||||
}
|
||||
@ -155,6 +163,7 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
||||
Object? deletedAt = freezed,
|
||||
Object? resultTier = null,
|
||||
Object? resultExperience = null,
|
||||
Object? resultCoin = null,
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
@ -183,6 +192,10 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
||||
? _value.resultExperience
|
||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultCoin: null == resultCoin
|
||||
? _value.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _value._resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
@ -205,6 +218,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
required this.deletedAt,
|
||||
required this.resultTier,
|
||||
required this.resultExperience,
|
||||
required this.resultCoin,
|
||||
required final List<int> resultModifiers,
|
||||
required this.accountId})
|
||||
: _resultModifiers = resultModifiers,
|
||||
@ -225,6 +239,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
final int resultTier;
|
||||
@override
|
||||
final int resultExperience;
|
||||
@override
|
||||
final double resultCoin;
|
||||
final List<int> _resultModifiers;
|
||||
@override
|
||||
List<int> get resultModifiers {
|
||||
@ -238,7 +254,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -257,6 +273,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
other.resultTier == resultTier) &&
|
||||
(identical(other.resultExperience, resultExperience) ||
|
||||
other.resultExperience == resultExperience) &&
|
||||
(identical(other.resultCoin, resultCoin) ||
|
||||
other.resultCoin == resultCoin) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._resultModifiers, _resultModifiers) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
@ -273,6 +291,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
deletedAt,
|
||||
resultTier,
|
||||
resultExperience,
|
||||
resultCoin,
|
||||
const DeepCollectionEquality().hash(_resultModifiers),
|
||||
accountId);
|
||||
|
||||
@ -301,6 +320,7 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
|
||||
required final DateTime? deletedAt,
|
||||
required final int resultTier,
|
||||
required final int resultExperience,
|
||||
required final double resultCoin,
|
||||
required final List<int> resultModifiers,
|
||||
required final int accountId}) = _$SnCheckInRecordImpl;
|
||||
const _SnCheckInRecord._() : super._();
|
||||
@ -321,6 +341,8 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
|
||||
@override
|
||||
int get resultExperience;
|
||||
@override
|
||||
double get resultCoin;
|
||||
@override
|
||||
List<int> get resultModifiers;
|
||||
@override
|
||||
int get accountId;
|
||||
|
@ -17,6 +17,7 @@ _$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson(
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
resultTier: (json['result_tier'] as num).toInt(),
|
||||
resultExperience: (json['result_experience'] as num).toInt(),
|
||||
resultCoin: (json['result_coin'] as num).toDouble(),
|
||||
resultModifiers: (json['result_modifiers'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
@ -32,6 +33,7 @@ Map<String, dynamic> _$$SnCheckInRecordImplToJson(
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'result_tier': instance.resultTier,
|
||||
'result_experience': instance.resultExperience,
|
||||
'result_coin': instance.resultCoin,
|
||||
'result_modifiers': instance.resultModifiers,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
38
lib/types/news.dart
Normal file
38
lib/types/news.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'news.freezed.dart';
|
||||
part 'news.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnNewsSource with _$SnNewsSource {
|
||||
const factory SnNewsSource({
|
||||
required String id,
|
||||
required String label,
|
||||
required String type,
|
||||
required String source,
|
||||
required int depth,
|
||||
required bool enabled,
|
||||
}) = _SnNewsSource;
|
||||
|
||||
factory SnNewsSource.fromJson(Map<String, dynamic> json) => _$SnNewsSourceFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnNewsArticle with _$SnNewsArticle {
|
||||
const factory SnNewsArticle({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required dynamic deletedAt,
|
||||
required String thumbnail,
|
||||
required String title,
|
||||
required String description,
|
||||
required String content,
|
||||
required String url,
|
||||
required String hash,
|
||||
required String source,
|
||||
required DateTime? publishedAt,
|
||||
}) = _SnNewsArticle;
|
||||
|
||||
factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json);
|
||||
}
|
660
lib/types/news.freezed.dart
Normal file
660
lib/types/news.freezed.dart
Normal file
@ -0,0 +1,660 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'news.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) {
|
||||
return _SnNewsSource.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnNewsSource {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
String get label => throw _privateConstructorUsedError;
|
||||
String get type => throw _privateConstructorUsedError;
|
||||
String get source => throw _privateConstructorUsedError;
|
||||
int get depth => throw _privateConstructorUsedError;
|
||||
bool get enabled => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnNewsSource to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnNewsSourceCopyWith<SnNewsSource> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnNewsSourceCopyWith<$Res> {
|
||||
factory $SnNewsSourceCopyWith(
|
||||
SnNewsSource value, $Res Function(SnNewsSource) then) =
|
||||
_$SnNewsSourceCopyWithImpl<$Res, SnNewsSource>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String id,
|
||||
String label,
|
||||
String type,
|
||||
String source,
|
||||
int depth,
|
||||
bool enabled});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnNewsSourceCopyWithImpl<$Res, $Val extends SnNewsSource>
|
||||
implements $SnNewsSourceCopyWith<$Res> {
|
||||
_$SnNewsSourceCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? label = null,
|
||||
Object? type = null,
|
||||
Object? source = null,
|
||||
Object? depth = null,
|
||||
Object? enabled = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
label: null == label
|
||||
? _value.label
|
||||
: label // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
type: null == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
source: null == source
|
||||
? _value.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
depth: null == depth
|
||||
? _value.depth
|
||||
: depth // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
enabled: null == enabled
|
||||
? _value.enabled
|
||||
: enabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnNewsSourceImplCopyWith<$Res>
|
||||
implements $SnNewsSourceCopyWith<$Res> {
|
||||
factory _$$SnNewsSourceImplCopyWith(
|
||||
_$SnNewsSourceImpl value, $Res Function(_$SnNewsSourceImpl) then) =
|
||||
__$$SnNewsSourceImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String id,
|
||||
String label,
|
||||
String type,
|
||||
String source,
|
||||
int depth,
|
||||
bool enabled});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnNewsSourceImplCopyWithImpl<$Res>
|
||||
extends _$SnNewsSourceCopyWithImpl<$Res, _$SnNewsSourceImpl>
|
||||
implements _$$SnNewsSourceImplCopyWith<$Res> {
|
||||
__$$SnNewsSourceImplCopyWithImpl(
|
||||
_$SnNewsSourceImpl _value, $Res Function(_$SnNewsSourceImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? label = null,
|
||||
Object? type = null,
|
||||
Object? source = null,
|
||||
Object? depth = null,
|
||||
Object? enabled = null,
|
||||
}) {
|
||||
return _then(_$SnNewsSourceImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
label: null == label
|
||||
? _value.label
|
||||
: label // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
type: null == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
source: null == source
|
||||
? _value.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
depth: null == depth
|
||||
? _value.depth
|
||||
: depth // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
enabled: null == enabled
|
||||
? _value.enabled
|
||||
: enabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnNewsSourceImpl implements _SnNewsSource {
|
||||
const _$SnNewsSourceImpl(
|
||||
{required this.id,
|
||||
required this.label,
|
||||
required this.type,
|
||||
required this.source,
|
||||
required this.depth,
|
||||
required this.enabled});
|
||||
|
||||
factory _$SnNewsSourceImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnNewsSourceImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final String label;
|
||||
@override
|
||||
final String type;
|
||||
@override
|
||||
final String source;
|
||||
@override
|
||||
final int depth;
|
||||
@override
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnNewsSourceImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.label, label) || other.label == label) &&
|
||||
(identical(other.type, type) || other.type == type) &&
|
||||
(identical(other.source, source) || other.source == source) &&
|
||||
(identical(other.depth, depth) || other.depth == depth) &&
|
||||
(identical(other.enabled, enabled) || other.enabled == enabled));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, id, label, type, source, depth, enabled);
|
||||
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith =>
|
||||
__$$SnNewsSourceImplCopyWithImpl<_$SnNewsSourceImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnNewsSourceImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnNewsSource implements SnNewsSource {
|
||||
const factory _SnNewsSource(
|
||||
{required final String id,
|
||||
required final String label,
|
||||
required final String type,
|
||||
required final String source,
|
||||
required final int depth,
|
||||
required final bool enabled}) = _$SnNewsSourceImpl;
|
||||
|
||||
factory _SnNewsSource.fromJson(Map<String, dynamic> json) =
|
||||
_$SnNewsSourceImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get id;
|
||||
@override
|
||||
String get label;
|
||||
@override
|
||||
String get type;
|
||||
@override
|
||||
String get source;
|
||||
@override
|
||||
int get depth;
|
||||
@override
|
||||
bool get enabled;
|
||||
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) {
|
||||
return _SnNewsArticle.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnNewsArticle {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
dynamic get deletedAt => throw _privateConstructorUsedError;
|
||||
String get thumbnail => throw _privateConstructorUsedError;
|
||||
String get title => throw _privateConstructorUsedError;
|
||||
String get description => throw _privateConstructorUsedError;
|
||||
String get content => throw _privateConstructorUsedError;
|
||||
String get url => throw _privateConstructorUsedError;
|
||||
String get hash => throw _privateConstructorUsedError;
|
||||
String get source => throw _privateConstructorUsedError;
|
||||
DateTime? get publishedAt => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnNewsArticle to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnNewsArticleCopyWith<SnNewsArticle> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnNewsArticleCopyWith<$Res> {
|
||||
factory $SnNewsArticleCopyWith(
|
||||
SnNewsArticle value, $Res Function(SnNewsArticle) then) =
|
||||
_$SnNewsArticleCopyWithImpl<$Res, SnNewsArticle>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
String thumbnail,
|
||||
String title,
|
||||
String description,
|
||||
String content,
|
||||
String url,
|
||||
String hash,
|
||||
String source,
|
||||
DateTime? publishedAt});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnNewsArticleCopyWithImpl<$Res, $Val extends SnNewsArticle>
|
||||
implements $SnNewsArticleCopyWith<$Res> {
|
||||
_$SnNewsArticleCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? thumbnail = null,
|
||||
Object? title = null,
|
||||
Object? description = null,
|
||||
Object? content = null,
|
||||
Object? url = null,
|
||||
Object? hash = null,
|
||||
Object? source = null,
|
||||
Object? publishedAt = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
thumbnail: null == thumbnail
|
||||
? _value.thumbnail
|
||||
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: null == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
content: null == content
|
||||
? _value.content
|
||||
: content // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
url: null == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
hash: null == hash
|
||||
? _value.hash
|
||||
: hash // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
source: null == source
|
||||
? _value.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
publishedAt: freezed == publishedAt
|
||||
? _value.publishedAt
|
||||
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnNewsArticleImplCopyWith<$Res>
|
||||
implements $SnNewsArticleCopyWith<$Res> {
|
||||
factory _$$SnNewsArticleImplCopyWith(
|
||||
_$SnNewsArticleImpl value, $Res Function(_$SnNewsArticleImpl) then) =
|
||||
__$$SnNewsArticleImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
String thumbnail,
|
||||
String title,
|
||||
String description,
|
||||
String content,
|
||||
String url,
|
||||
String hash,
|
||||
String source,
|
||||
DateTime? publishedAt});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnNewsArticleImplCopyWithImpl<$Res>
|
||||
extends _$SnNewsArticleCopyWithImpl<$Res, _$SnNewsArticleImpl>
|
||||
implements _$$SnNewsArticleImplCopyWith<$Res> {
|
||||
__$$SnNewsArticleImplCopyWithImpl(
|
||||
_$SnNewsArticleImpl _value, $Res Function(_$SnNewsArticleImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? thumbnail = null,
|
||||
Object? title = null,
|
||||
Object? description = null,
|
||||
Object? content = null,
|
||||
Object? url = null,
|
||||
Object? hash = null,
|
||||
Object? source = null,
|
||||
Object? publishedAt = freezed,
|
||||
}) {
|
||||
return _then(_$SnNewsArticleImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
thumbnail: null == thumbnail
|
||||
? _value.thumbnail
|
||||
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
title: null == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
content: null == content
|
||||
? _value.content
|
||||
: content // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
url: null == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
hash: null == hash
|
||||
? _value.hash
|
||||
: hash // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
source: null == source
|
||||
? _value.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
publishedAt: freezed == publishedAt
|
||||
? _value.publishedAt
|
||||
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnNewsArticleImpl implements _SnNewsArticle {
|
||||
const _$SnNewsArticleImpl(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.thumbnail,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.content,
|
||||
required this.url,
|
||||
required this.hash,
|
||||
required this.source,
|
||||
required this.publishedAt});
|
||||
|
||||
factory _$SnNewsArticleImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnNewsArticleImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final dynamic deletedAt;
|
||||
@override
|
||||
final String thumbnail;
|
||||
@override
|
||||
final String title;
|
||||
@override
|
||||
final String description;
|
||||
@override
|
||||
final String content;
|
||||
@override
|
||||
final String url;
|
||||
@override
|
||||
final String hash;
|
||||
@override
|
||||
final String source;
|
||||
@override
|
||||
final DateTime? publishedAt;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnNewsArticle(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, source: $source, publishedAt: $publishedAt)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnNewsArticleImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
||||
(identical(other.thumbnail, thumbnail) ||
|
||||
other.thumbnail == thumbnail) &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
(identical(other.description, description) ||
|
||||
other.description == description) &&
|
||||
(identical(other.content, content) || other.content == content) &&
|
||||
(identical(other.url, url) || other.url == url) &&
|
||||
(identical(other.hash, hash) || other.hash == hash) &&
|
||||
(identical(other.source, source) || other.source == source) &&
|
||||
(identical(other.publishedAt, publishedAt) ||
|
||||
other.publishedAt == publishedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
const DeepCollectionEquality().hash(deletedAt),
|
||||
thumbnail,
|
||||
title,
|
||||
description,
|
||||
content,
|
||||
url,
|
||||
hash,
|
||||
source,
|
||||
publishedAt);
|
||||
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith =>
|
||||
__$$SnNewsArticleImplCopyWithImpl<_$SnNewsArticleImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnNewsArticleImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnNewsArticle implements SnNewsArticle {
|
||||
const factory _SnNewsArticle(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final dynamic deletedAt,
|
||||
required final String thumbnail,
|
||||
required final String title,
|
||||
required final String description,
|
||||
required final String content,
|
||||
required final String url,
|
||||
required final String hash,
|
||||
required final String source,
|
||||
required final DateTime? publishedAt}) = _$SnNewsArticleImpl;
|
||||
|
||||
factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =
|
||||
_$SnNewsArticleImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
dynamic get deletedAt;
|
||||
@override
|
||||
String get thumbnail;
|
||||
@override
|
||||
String get title;
|
||||
@override
|
||||
String get description;
|
||||
@override
|
||||
String get content;
|
||||
@override
|
||||
String get url;
|
||||
@override
|
||||
String get hash;
|
||||
@override
|
||||
String get source;
|
||||
@override
|
||||
DateTime? get publishedAt;
|
||||
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
61
lib/types/news.g.dart
Normal file
61
lib/types/news.g.dart
Normal file
@ -0,0 +1,61 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'news.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnNewsSourceImpl _$$SnNewsSourceImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnNewsSourceImpl(
|
||||
id: json['id'] as String,
|
||||
label: json['label'] as String,
|
||||
type: json['type'] as String,
|
||||
source: json['source'] as String,
|
||||
depth: (json['depth'] as num).toInt(),
|
||||
enabled: json['enabled'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnNewsSourceImplToJson(_$SnNewsSourceImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'label': instance.label,
|
||||
'type': instance.type,
|
||||
'source': instance.source,
|
||||
'depth': instance.depth,
|
||||
'enabled': instance.enabled,
|
||||
};
|
||||
|
||||
_$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnNewsArticleImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'],
|
||||
thumbnail: json['thumbnail'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
content: json['content'] as String,
|
||||
url: json['url'] as String,
|
||||
hash: json['hash'] as String,
|
||||
source: json['source'] as String,
|
||||
publishedAt: json['published_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['published_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt,
|
||||
'thumbnail': instance.thumbnail,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'content': instance.content,
|
||||
'url': instance.url,
|
||||
'hash': instance.hash,
|
||||
'source': instance.source,
|
||||
'published_at': instance.publishedAt?.toIso8601String(),
|
||||
};
|
37
lib/types/wallet.dart
Normal file
37
lib/types/wallet.dart
Normal file
@ -0,0 +1,37 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'wallet.freezed.dart';
|
||||
part 'wallet.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnWallet with _$SnWallet {
|
||||
const factory SnWallet({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required String balance,
|
||||
required String password,
|
||||
required int accountId,
|
||||
}) = _SnWallet;
|
||||
|
||||
factory SnWallet.fromJson(Map<String, dynamic> json) => _$SnWalletFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnTransaction with _$SnTransaction {
|
||||
const factory SnTransaction({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required String remark,
|
||||
required String amount,
|
||||
required SnWallet? payer,
|
||||
required SnWallet? payee,
|
||||
required int? payerId,
|
||||
required int? payeeId,
|
||||
}) = _SnTransaction;
|
||||
|
||||
factory SnTransaction.fromJson(Map<String, dynamic> json) => _$SnTransactionFromJson(json);
|
||||
}
|
666
lib/types/wallet.freezed.dart
Normal file
666
lib/types/wallet.freezed.dart
Normal file
@ -0,0 +1,666 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'wallet.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
SnWallet _$SnWalletFromJson(Map<String, dynamic> json) {
|
||||
return _SnWallet.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnWallet {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||
String get balance => throw _privateConstructorUsedError;
|
||||
String get password => throw _privateConstructorUsedError;
|
||||
int get accountId => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnWallet to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnWallet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnWalletCopyWith<SnWallet> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnWalletCopyWith<$Res> {
|
||||
factory $SnWalletCopyWith(SnWallet value, $Res Function(SnWallet) then) =
|
||||
_$SnWalletCopyWithImpl<$Res, SnWallet>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String balance,
|
||||
String password,
|
||||
int accountId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnWalletCopyWithImpl<$Res, $Val extends SnWallet>
|
||||
implements $SnWalletCopyWith<$Res> {
|
||||
_$SnWalletCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnWallet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? balance = null,
|
||||
Object? password = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
balance: null == balance
|
||||
? _value.balance
|
||||
: balance // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
password: null == password
|
||||
? _value.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnWalletImplCopyWith<$Res>
|
||||
implements $SnWalletCopyWith<$Res> {
|
||||
factory _$$SnWalletImplCopyWith(
|
||||
_$SnWalletImpl value, $Res Function(_$SnWalletImpl) then) =
|
||||
__$$SnWalletImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String balance,
|
||||
String password,
|
||||
int accountId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnWalletImplCopyWithImpl<$Res>
|
||||
extends _$SnWalletCopyWithImpl<$Res, _$SnWalletImpl>
|
||||
implements _$$SnWalletImplCopyWith<$Res> {
|
||||
__$$SnWalletImplCopyWithImpl(
|
||||
_$SnWalletImpl _value, $Res Function(_$SnWalletImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnWallet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? balance = null,
|
||||
Object? password = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
return _then(_$SnWalletImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
balance: null == balance
|
||||
? _value.balance
|
||||
: balance // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
password: null == password
|
||||
? _value.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnWalletImpl implements _SnWallet {
|
||||
const _$SnWalletImpl(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.balance,
|
||||
required this.password,
|
||||
required this.accountId});
|
||||
|
||||
factory _$SnWalletImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnWalletImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final String balance;
|
||||
@override
|
||||
final String password;
|
||||
@override
|
||||
final int accountId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, password: $password, accountId: $accountId)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnWalletImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.balance, balance) || other.balance == balance) &&
|
||||
(identical(other.password, password) ||
|
||||
other.password == password) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||
deletedAt, balance, password, accountId);
|
||||
|
||||
/// Create a copy of SnWallet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnWalletImplCopyWith<_$SnWalletImpl> get copyWith =>
|
||||
__$$SnWalletImplCopyWithImpl<_$SnWalletImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnWalletImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnWallet implements SnWallet {
|
||||
const factory _SnWallet(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final DateTime? deletedAt,
|
||||
required final String balance,
|
||||
required final String password,
|
||||
required final int accountId}) = _$SnWalletImpl;
|
||||
|
||||
factory _SnWallet.fromJson(Map<String, dynamic> json) =
|
||||
_$SnWalletImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
DateTime? get deletedAt;
|
||||
@override
|
||||
String get balance;
|
||||
@override
|
||||
String get password;
|
||||
@override
|
||||
int get accountId;
|
||||
|
||||
/// Create a copy of SnWallet
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnWalletImplCopyWith<_$SnWalletImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnTransaction _$SnTransactionFromJson(Map<String, dynamic> json) {
|
||||
return _SnTransaction.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnTransaction {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||
String get remark => throw _privateConstructorUsedError;
|
||||
String get amount => throw _privateConstructorUsedError;
|
||||
SnWallet? get payer => throw _privateConstructorUsedError;
|
||||
SnWallet? get payee => throw _privateConstructorUsedError;
|
||||
int? get payerId => throw _privateConstructorUsedError;
|
||||
int? get payeeId => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnTransaction to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnTransactionCopyWith<SnTransaction> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnTransactionCopyWith<$Res> {
|
||||
factory $SnTransactionCopyWith(
|
||||
SnTransaction value, $Res Function(SnTransaction) then) =
|
||||
_$SnTransactionCopyWithImpl<$Res, SnTransaction>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String remark,
|
||||
String amount,
|
||||
SnWallet? payer,
|
||||
SnWallet? payee,
|
||||
int? payerId,
|
||||
int? payeeId});
|
||||
|
||||
$SnWalletCopyWith<$Res>? get payer;
|
||||
$SnWalletCopyWith<$Res>? get payee;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnTransactionCopyWithImpl<$Res, $Val extends SnTransaction>
|
||||
implements $SnTransactionCopyWith<$Res> {
|
||||
_$SnTransactionCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? remark = null,
|
||||
Object? amount = null,
|
||||
Object? payer = freezed,
|
||||
Object? payee = freezed,
|
||||
Object? payerId = freezed,
|
||||
Object? payeeId = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
remark: null == remark
|
||||
? _value.remark
|
||||
: remark // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
amount: null == amount
|
||||
? _value.amount
|
||||
: amount // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
payer: freezed == payer
|
||||
? _value.payer
|
||||
: payer // ignore: cast_nullable_to_non_nullable
|
||||
as SnWallet?,
|
||||
payee: freezed == payee
|
||||
? _value.payee
|
||||
: payee // ignore: cast_nullable_to_non_nullable
|
||||
as SnWallet?,
|
||||
payerId: freezed == payerId
|
||||
? _value.payerId
|
||||
: payerId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
payeeId: freezed == payeeId
|
||||
? _value.payeeId
|
||||
: payeeId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnWalletCopyWith<$Res>? get payer {
|
||||
if (_value.payer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnWalletCopyWith<$Res>(_value.payer!, (value) {
|
||||
return _then(_value.copyWith(payer: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnWalletCopyWith<$Res>? get payee {
|
||||
if (_value.payee == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnWalletCopyWith<$Res>(_value.payee!, (value) {
|
||||
return _then(_value.copyWith(payee: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnTransactionImplCopyWith<$Res>
|
||||
implements $SnTransactionCopyWith<$Res> {
|
||||
factory _$$SnTransactionImplCopyWith(
|
||||
_$SnTransactionImpl value, $Res Function(_$SnTransactionImpl) then) =
|
||||
__$$SnTransactionImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String remark,
|
||||
String amount,
|
||||
SnWallet? payer,
|
||||
SnWallet? payee,
|
||||
int? payerId,
|
||||
int? payeeId});
|
||||
|
||||
@override
|
||||
$SnWalletCopyWith<$Res>? get payer;
|
||||
@override
|
||||
$SnWalletCopyWith<$Res>? get payee;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnTransactionImplCopyWithImpl<$Res>
|
||||
extends _$SnTransactionCopyWithImpl<$Res, _$SnTransactionImpl>
|
||||
implements _$$SnTransactionImplCopyWith<$Res> {
|
||||
__$$SnTransactionImplCopyWithImpl(
|
||||
_$SnTransactionImpl _value, $Res Function(_$SnTransactionImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? remark = null,
|
||||
Object? amount = null,
|
||||
Object? payer = freezed,
|
||||
Object? payee = freezed,
|
||||
Object? payerId = freezed,
|
||||
Object? payeeId = freezed,
|
||||
}) {
|
||||
return _then(_$SnTransactionImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
remark: null == remark
|
||||
? _value.remark
|
||||
: remark // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
amount: null == amount
|
||||
? _value.amount
|
||||
: amount // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
payer: freezed == payer
|
||||
? _value.payer
|
||||
: payer // ignore: cast_nullable_to_non_nullable
|
||||
as SnWallet?,
|
||||
payee: freezed == payee
|
||||
? _value.payee
|
||||
: payee // ignore: cast_nullable_to_non_nullable
|
||||
as SnWallet?,
|
||||
payerId: freezed == payerId
|
||||
? _value.payerId
|
||||
: payerId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
payeeId: freezed == payeeId
|
||||
? _value.payeeId
|
||||
: payeeId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnTransactionImpl implements _SnTransaction {
|
||||
const _$SnTransactionImpl(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.remark,
|
||||
required this.amount,
|
||||
required this.payer,
|
||||
required this.payee,
|
||||
required this.payerId,
|
||||
required this.payeeId});
|
||||
|
||||
factory _$SnTransactionImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnTransactionImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final String remark;
|
||||
@override
|
||||
final String amount;
|
||||
@override
|
||||
final SnWallet? payer;
|
||||
@override
|
||||
final SnWallet? payee;
|
||||
@override
|
||||
final int? payerId;
|
||||
@override
|
||||
final int? payeeId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnTransactionImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.remark, remark) || other.remark == remark) &&
|
||||
(identical(other.amount, amount) || other.amount == amount) &&
|
||||
(identical(other.payer, payer) || other.payer == payer) &&
|
||||
(identical(other.payee, payee) || other.payee == payee) &&
|
||||
(identical(other.payerId, payerId) || other.payerId == payerId) &&
|
||||
(identical(other.payeeId, payeeId) || other.payeeId == payeeId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||
deletedAt, remark, amount, payer, payee, payerId, payeeId);
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnTransactionImplCopyWith<_$SnTransactionImpl> get copyWith =>
|
||||
__$$SnTransactionImplCopyWithImpl<_$SnTransactionImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnTransactionImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnTransaction implements SnTransaction {
|
||||
const factory _SnTransaction(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final DateTime? deletedAt,
|
||||
required final String remark,
|
||||
required final String amount,
|
||||
required final SnWallet? payer,
|
||||
required final SnWallet? payee,
|
||||
required final int? payerId,
|
||||
required final int? payeeId}) = _$SnTransactionImpl;
|
||||
|
||||
factory _SnTransaction.fromJson(Map<String, dynamic> json) =
|
||||
_$SnTransactionImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
DateTime? get deletedAt;
|
||||
@override
|
||||
String get remark;
|
||||
@override
|
||||
String get amount;
|
||||
@override
|
||||
SnWallet? get payer;
|
||||
@override
|
||||
SnWallet? get payee;
|
||||
@override
|
||||
int? get payerId;
|
||||
@override
|
||||
int? get payeeId;
|
||||
|
||||
/// Create a copy of SnTransaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnTransactionImplCopyWith<_$SnTransactionImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
65
lib/types/wallet.g.dart
Normal file
65
lib/types/wallet.g.dart
Normal file
@ -0,0 +1,65 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'wallet.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnWalletImpl _$$SnWalletImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnWalletImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
balance: json['balance'] as String,
|
||||
password: json['password'] as String,
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnWalletImplToJson(_$SnWalletImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'balance': instance.balance,
|
||||
'password': instance.password,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
_$SnTransactionImpl _$$SnTransactionImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnTransactionImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
remark: json['remark'] as String,
|
||||
amount: json['amount'] as String,
|
||||
payer: json['payer'] == null
|
||||
? null
|
||||
: SnWallet.fromJson(json['payer'] as Map<String, dynamic>),
|
||||
payee: json['payee'] == null
|
||||
? null
|
||||
: SnWallet.fromJson(json['payee'] as Map<String, dynamic>),
|
||||
payerId: (json['payer_id'] as num?)?.toInt(),
|
||||
payeeId: (json['payee_id'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnTransactionImplToJson(_$SnTransactionImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'remark': instance.remark,
|
||||
'amount': instance.amount,
|
||||
'payer': instance.payer?.toJson(),
|
||||
'payee': instance.payee?.toJson(),
|
||||
'payer_id': instance.payerId,
|
||||
'payee_id': instance.payeeId,
|
||||
};
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class AboutScreen extends StatelessWidget {
|
||||
@ -12,97 +13,103 @@ class AboutScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Solian',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
|
||||
),
|
||||
const Text(
|
||||
'The Solar Network',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const Gap(8),
|
||||
FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAbout').tr(),
|
||||
),
|
||||
body: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Solian',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
|
||||
),
|
||||
const Text(
|
||||
'The Solar Network',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const Gap(8),
|
||||
FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Text(
|
||||
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
||||
const Gap(16),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('appDetails').tr(),
|
||||
onPressed: () async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
return Text(
|
||||
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
||||
const Gap(16),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('appDetails').tr(),
|
||||
onPressed: () async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
|
||||
if (!context.mounted) return;
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: 'Solian',
|
||||
applicationVersion: '${info.version}+${info.buildNumber}',
|
||||
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.',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset(
|
||||
'assets/icon/icon-light-radius.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
if (!context.mounted) return;
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: 'Solian',
|
||||
applicationVersion: '${info.version}+${info.buildNumber}',
|
||||
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.',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset(
|
||||
'assets/icon/icon-light-radius.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('termRelated').tr(),
|
||||
onPressed: () {
|
||||
launchUrlString('https://solsynth.dev/terms');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('serviceStatus').tr(),
|
||||
onPressed: () {
|
||||
launchUrlString('https://status.solsynth.dev');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('termRelated').tr(),
|
||||
onPressed: () {
|
||||
launchUrlString('https://solsynth.dev/terms');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('serviceStatus').tr(),
|
||||
onPressed: () {
|
||||
launchUrlString('https://status.solsynth.dev');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
).center(),
|
||||
const Gap(16),
|
||||
const Text(
|
||||
'Open-sourced under AGPLv3',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
).center(),
|
||||
const Gap(16),
|
||||
const Text(
|
||||
'Open-sourced under AGPLv3',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
164
lib/widgets/account/account_popover.dart
Normal file
164
lib/widgets/account/account_popover.dart
Normal file
@ -0,0 +1,164 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/experience.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class AccountPopoverCard extends StatelessWidget {
|
||||
final SnAccount data;
|
||||
|
||||
const AccountPopoverCard({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (data.banner.isNotEmpty)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(data.banner),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Top padding
|
||||
Gap(16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountImage(
|
||||
content: data.avatar,
|
||||
radius: 20,
|
||||
),
|
||||
Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(data.nick).bold(),
|
||||
Text('@${data.name}').fontSize(13).opacity(0.75),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
GoRouter.of(context).pushNamed(
|
||||
'accountProfilePage',
|
||||
pathParameters: {'name': data.name},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.chevron_right),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
const Gap(8)
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
const Gap(16),
|
||||
Wrap(
|
||||
children: data.badges
|
||||
.map(
|
||||
(ele) => Tooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
|
||||
if (ele.metadata['title'] != null)
|
||||
TextSpan(
|
||||
text: '\n${ele.metadata['title']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: DateFormat.yMEd().format(ele.createdAt),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
|
||||
color: kBadgesMeta[ele.type]?.$3,
|
||||
fill: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
).padding(horizontal: 24),
|
||||
const Gap(8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.star),
|
||||
const Gap(8),
|
||||
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
|
||||
const Gap(8),
|
||||
Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
|
||||
const Gap(8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: LinearProgressIndicator(
|
||||
value: calcLevelUpProgress(data.profile?.experience ?? 0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
).alignment(Alignment.centerLeft),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24),
|
||||
FutureBuilder(
|
||||
future: sn.client.get('/cgi/id/users/${data.name}/status'),
|
||||
builder: (context, snapshot) {
|
||||
final SnAccountStatusInfo? status =
|
||||
snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null;
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.circle,
|
||||
fill: 1,
|
||||
size: 16,
|
||||
color: (status?.isOnline ?? false) ? Colors.green : Colors.grey,
|
||||
).padding(all: 4),
|
||||
const Gap(8),
|
||||
Text(
|
||||
status != null
|
||||
? status.isOnline
|
||||
? 'accountStatusOnline'.tr()
|
||||
: 'accountStatusOffline'.tr()
|
||||
: 'loading'.tr(),
|
||||
),
|
||||
if (status != null && !status.isOnline && status.lastSeenAt != null)
|
||||
Text(
|
||||
'accountStatusLastSeen'.tr(args: [
|
||||
status.lastSeenAt != null
|
||||
? RelativeTime(context).format(
|
||||
status.lastSeenAt!.toLocal(),
|
||||
)
|
||||
: 'unknown',
|
||||
]),
|
||||
).padding(left: 6).opacity(0.75),
|
||||
],
|
||||
).padding(horizontal: 24);
|
||||
},
|
||||
),
|
||||
// Bottom padding
|
||||
const Gap(16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ class AttachmentList extends StatefulWidget {
|
||||
final List<SnAttachment?> data;
|
||||
final bool bordered;
|
||||
final bool gridded;
|
||||
final bool columned;
|
||||
final BoxFit fit;
|
||||
final double? maxHeight;
|
||||
final double? minWidth;
|
||||
@ -26,6 +27,7 @@ class AttachmentList extends StatefulWidget {
|
||||
required this.data,
|
||||
this.bordered = false,
|
||||
this.gridded = false,
|
||||
this.columned = false,
|
||||
this.fit = BoxFit.cover,
|
||||
this.maxHeight,
|
||||
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(
|
||||
margin: widget.padding ?? EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
@ -153,68 +158,109 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||
child: ScrollConfiguration(
|
||||
behavior: _AttachmentListScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
padding: widget.padding,
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.data.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
|
||||
child: AspectRatio(
|
||||
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
if ((!fullOfImage && widget.gridded) || widget.columned) {
|
||||
return Container(
|
||||
margin: widget.padding ?? EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: Column(
|
||||
children: widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => GestureDetector(
|
||||
child: AspectRatio(
|
||||
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
child: AttachmentItem(
|
||||
data: ele,
|
||||
heroTag: heroTags[idx],
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
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(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(
|
||||
label: Text('${idx + 1}/${widget.data.length}'),
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(
|
||||
label: Text('${idx + 1}/${widget.data.length}'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -129,6 +129,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
|
||||
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
bool _showDetail = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
@ -144,218 +146,350 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
onDismissed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
direction: DismissiblePageDismissDirection.down,
|
||||
direction: DismissiblePageDismissDirection.none,
|
||||
backgroundColor: Colors.transparent,
|
||||
isFullScreen: true,
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Builder(builder: (context) {
|
||||
if (widget.data.length == 1) {
|
||||
final heroTag = widget.heroTags?.first ?? uuid.v4();
|
||||
return Hero(
|
||||
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
||||
child: PhotoView(
|
||||
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',
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Builder(builder: (context) {
|
||||
if (widget.data.length == 1) {
|
||||
final heroTag = widget.heroTags?.first ?? uuid.v4();
|
||||
return Hero(
|
||||
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
||||
child: PhotoView(
|
||||
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
|
||||
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
||||
imageProvider: UniversalImage.provider(
|
||||
sn.getAttachmentUrl(widget.data.first.rid),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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),
|
||||
}
|
||||
|
||||
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(
|
||||
child: SizedBox(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
||||
);
|
||||
}),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Colors.transparent,
|
||||
],
|
||||
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
||||
);
|
||||
}),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Builder(builder: (context) {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final item = widget.data.elementAt(
|
||||
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
|
||||
);
|
||||
final account = ud.getAccountFromCache(item.accountId);
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Builder(builder: (context) {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final item = widget.data.elementAt(
|
||||
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
|
||||
);
|
||||
final account = ud.getAccountFromCache(item.accountId);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (item.accountId > 0)
|
||||
Row(
|
||||
children: [
|
||||
IgnorePointer(
|
||||
child: AccountImage(
|
||||
content: account?.avatar,
|
||||
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,
|
||||
),
|
||||
],
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (item.accountId > 0)
|
||||
Row(
|
||||
children: [
|
||||
IgnorePointer(
|
||||
child: AccountImage(
|
||||
content: account?.avatar,
|
||||
radius: 19,
|
||||
),
|
||||
),
|
||||
),
|
||||
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(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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
IgnorePointer(
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (item.metadata['exif'] == null)
|
||||
const Gap(2),
|
||||
IgnorePointer(
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
children: [
|
||||
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(
|
||||
'#${item.rid}',
|
||||
item.mimetype,
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,14 +1,19 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:popover/popover.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/account_popover.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/context_menu.dart';
|
||||
import 'package:surface/widgets/link_preview.dart';
|
||||
@ -49,6 +54,8 @@ class ChatMessage extends StatelessWidget {
|
||||
|
||||
final dateFormatter = DateFormat('MM/dd HH:mm');
|
||||
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return SwipeTo(
|
||||
key: Key('chat-message-${data.id}'),
|
||||
iconOnLeftSwipe: Symbols.reply,
|
||||
@ -95,8 +102,28 @@ class ChatMessage extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isMerged && !isCompact)
|
||||
AccountImage(
|
||||
content: user?.avatar,
|
||||
GestureDetector(
|
||||
child: AccountImage(
|
||||
content: user?.avatar,
|
||||
),
|
||||
onTap: () {
|
||||
if (user == null) return;
|
||||
showPopover(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
transition: PopoverTransition.other,
|
||||
bodyBuilder: (context) => SizedBox(
|
||||
width: math.min(400, MediaQuery.of(context).size.width - 10),
|
||||
child: AccountPopoverCard(
|
||||
data: user,
|
||||
),
|
||||
),
|
||||
direction: PopoverDirection.bottom,
|
||||
arrowHeight: 5,
|
||||
arrowWidth: 15,
|
||||
arrowDxOffset: -190,
|
||||
);
|
||||
},
|
||||
)
|
||||
else if (isMerged)
|
||||
const Gap(40),
|
||||
@ -124,10 +151,13 @@ class ChatMessage extends StatelessWidget {
|
||||
dateFormatter.format(data.createdAt.toLocal()),
|
||||
).fontSize(13),
|
||||
],
|
||||
),
|
||||
).height(21),
|
||||
if (isCompact) const Gap(8),
|
||||
if (data.preload?.quoteEvent != null)
|
||||
StyledWidget(Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 480,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
@ -150,7 +180,12 @@ class ChatMessage extends StatelessWidget {
|
||||
),
|
||||
)).padding(bottom: 4, top: 4),
|
||||
switch (data.type) {
|
||||
'messages.new' => _ChatMessageText(data: data),
|
||||
'messages.new' => _ChatMessageText(
|
||||
data: data,
|
||||
onReply: onReply,
|
||||
onEdit: onEdit,
|
||||
onDelete: onDelete,
|
||||
),
|
||||
_ => _ChatMessageSystemNotify(data: data),
|
||||
},
|
||||
],
|
||||
@ -160,7 +195,10 @@ class ChatMessage extends StatelessWidget {
|
||||
],
|
||||
).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']!),
|
||||
if (data.preload?.attachments?.isNotEmpty ?? false)
|
||||
AttachmentList(
|
||||
@ -181,19 +219,75 @@ class ChatMessage extends StatelessWidget {
|
||||
|
||||
class _ChatMessageText extends StatelessWidget {
|
||||
final SnChatMessage data;
|
||||
final Function(SnChatMessage)? onReply;
|
||||
final Function(SnChatMessage)? onEdit;
|
||||
final Function(SnChatMessage)? onDelete;
|
||||
|
||||
const _ChatMessageText({required this.data});
|
||||
const _ChatMessageText({required this.data, this.onReply, this.onEdit, this.onDelete});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;
|
||||
|
||||
if (data.body['text'] != null && data.body['text'].isNotEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MarkdownTextContent(
|
||||
content: data.body['text'],
|
||||
isSelectable: true,
|
||||
isAutoWarp: true,
|
||||
SelectionArea(
|
||||
contextMenuBuilder: (context, editableTextState) {
|
||||
final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
|
||||
|
||||
if (onReply != null) {
|
||||
items.insert(
|
||||
0,
|
||||
ContextMenuButtonItem(
|
||||
label: 'reply'.tr(),
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
onReply?.call(data);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isOwner && onEdit != null) {
|
||||
items.insert(
|
||||
1,
|
||||
ContextMenuButtonItem(
|
||||
label: 'edit'.tr(),
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
onEdit?.call(data);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isOwner && onDelete != null) {
|
||||
items.insert(
|
||||
2,
|
||||
ContextMenuButtonItem(
|
||||
label: 'delete'.tr(),
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
onDelete?.call(data);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: editableTextState.contextMenuAnchors,
|
||||
buttonItems: items,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: MarkdownTextContent(
|
||||
content: data.body['text'],
|
||||
isAutoWarp: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (data.updatedAt != data.createdAt)
|
||||
Text(
|
||||
|
@ -48,6 +48,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
|
||||
void setEdit(SnChatMessage? value) {
|
||||
_contentController.text = value?.body['text'] ?? '';
|
||||
_attachments.clear();
|
||||
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
||||
setState(() => _editingMessage = value);
|
||||
}
|
||||
|
||||
@ -101,7 +103,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
},
|
||||
);
|
||||
|
||||
_attachments[i] = PostWriteMedia(item);
|
||||
setState(() {
|
||||
_attachments[i] = PostWriteMedia(item);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
@ -113,7 +117,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
// Send the message
|
||||
// NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type
|
||||
widget.controller.sendMessage(
|
||||
'messages.new',
|
||||
_editingMessage != null ? 'messages.edit' : 'messages.new',
|
||||
_contentController.text,
|
||||
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
||||
relatedId: _editingMessage?.id,
|
||||
@ -197,6 +201,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
InkWell(
|
||||
child: Text('cancel'.tr()),
|
||||
onTap: () {
|
||||
_attachments.clear();
|
||||
setState(() => _replyingMessage = null);
|
||||
},
|
||||
),
|
||||
@ -236,6 +241,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
InkWell(
|
||||
child: Text('cancel'.tr()),
|
||||
onTap: () {
|
||||
_attachments.clear();
|
||||
_contentController.clear();
|
||||
setState(() => _editingMessage = null);
|
||||
},
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
@ -16,45 +18,49 @@ class ConnectionIndicator extends StatelessWidget {
|
||||
listenable: ws,
|
||||
builder: (context, _) {
|
||||
final ua = context.read<UserProvider>();
|
||||
final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized;
|
||||
|
||||
return GestureDetector(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 8,
|
||||
top: MediaQuery.of(context).padding.top + 8,
|
||||
left: 24,
|
||||
right: 24,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (ws.isBusy)
|
||||
Text('serverConnecting').tr().textColor(
|
||||
Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else if (!ws.isConnected)
|
||||
Text('serverDisconnected').tr().textColor(
|
||||
Theme.of(context).colorScheme.onSecondaryContainer),
|
||||
],
|
||||
)
|
||||
: 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();
|
||||
}
|
||||
},
|
||||
return IgnorePointer(
|
||||
ignoring: !show,
|
||||
child: GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (ws.isBusy)
|
||||
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else if (!ws.isConnected)
|
||||
Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else
|
||||
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
||||
const Gap(8),
|
||||
if (ws.isBusy)
|
||||
const CircularProgressIndicator(strokeWidth: 2.5)
|
||||
.width(12)
|
||||
.height(12)
|
||||
.padding(horizontal: 4, right: 4)
|
||||
else if (!ws.isConnected)
|
||||
const Icon(Symbols.power_off, size: 18)
|
||||
else
|
||||
const Icon(Symbols.power, size: 18),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4)
|
||||
: const SizedBox.shrink(),
|
||||
).opacity(show ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.easeInOut,
|
||||
),
|
||||
onTap: () {
|
||||
if (!ws.isConnected && !ws.isBusy) {
|
||||
ws.connect();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
|
||||
class ContextMenuArea extends StatelessWidget {
|
||||
final ContextMenu contextMenu;
|
||||
@ -22,11 +23,10 @@ class ContextMenuArea extends StatelessWidget {
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
mousePosition = event.position;
|
||||
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
|
||||
if (!isCollapseDrawer) {
|
||||
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
if (!cfg.drawerIsCollapsed) {
|
||||
// Leave padding for side navigation
|
||||
mousePosition = isExpandDrawer
|
||||
mousePosition = cfg.drawerIsExpanded
|
||||
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
||||
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
extension AppPromptExtension on BuildContext {
|
||||
void showSnackbar(String content, {SnackBarAction? action}) {
|
||||
@ -111,7 +113,34 @@ extension AppPromptExtension on BuildContext {
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('dialogError').tr(),
|
||||
content: content,
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 20,
|
||||
children: [
|
||||
content,
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: 'needHelp'.tr(),
|
||||
children: [
|
||||
TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: 'needHelpLaunch'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(ctx).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: Theme.of(ctx).colorScheme.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlString('https://kb.solsynth.dev/solar-network');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
@ -128,17 +157,7 @@ extension ByteFormatter on int {
|
||||
if (this == 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
final dm = decimals < 0 ? 0 : decimals;
|
||||
final sizes = [
|
||||
'Bytes',
|
||||
'KiB',
|
||||
'MiB',
|
||||
'GiB',
|
||||
'TiB',
|
||||
'PiB',
|
||||
'EiB',
|
||||
'ZiB',
|
||||
'YiB'
|
||||
];
|
||||
final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
final i = (math.log(this) / math.log(k)).floor().toInt();
|
||||
return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
|
||||
}
|
||||
|
@ -7,12 +7,11 @@ import 'package:marquee/marquee.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/link_preview.dart';
|
||||
import 'package:surface/types/link.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import '../providers/link_preview.dart';
|
||||
|
||||
class LinkPreviewWidget extends StatefulWidget {
|
||||
final String text;
|
||||
|
||||
@ -81,8 +80,9 @@ class _LinkPreviewEntry extends StatelessWidget {
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AutoResizeUniversalImage(
|
||||
meta.image!,
|
||||
meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
@ -94,11 +94,14 @@ class _LinkPreviewEntry extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (meta.icon?.isNotEmpty ?? false)
|
||||
StyledWidget(
|
||||
meta.icon!.endsWith('.svg')
|
||||
? SvgPicture.network(meta.icon!)
|
||||
SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: meta.icon!.endsWith('.svg')
|
||||
? SvgPicture.network(meta.icon!, width: 36, height: 36)
|
||||
: UniversalImage(
|
||||
meta.icon!,
|
||||
noErrorWidget: true,
|
||||
width: 36,
|
||||
height: 36,
|
||||
cacheHeight: 36,
|
||||
|
@ -1,5 +1,3 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
@ -7,22 +5,18 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:markdown/markdown.dart' as markdown;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_sticker.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:syntax_highlight/syntax_highlight.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'attachment/attachment_zoom.dart';
|
||||
|
||||
class MarkdownTextContent extends StatelessWidget {
|
||||
final String content;
|
||||
final bool isSelectable;
|
||||
final bool isAutoWarp;
|
||||
final bool isEnlargeSticker;
|
||||
final TextScaler? textScaler;
|
||||
@ -31,14 +25,14 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
const MarkdownTextContent({
|
||||
super.key,
|
||||
required this.content,
|
||||
this.isSelectable = false,
|
||||
this.isAutoWarp = false,
|
||||
this.isEnlargeSticker = false,
|
||||
this.textScaler,
|
||||
this.attachments,
|
||||
});
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Markdown(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
@ -47,33 +41,33 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||
Theme.of(context),
|
||||
).copyWith(
|
||||
textScaler: textScaler,
|
||||
blockquote: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
blockquoteDecoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
horizontalRuleDecoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 1.0,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
codeblockDecoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
textScaler: textScaler,
|
||||
blockquote: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
blockquoteDecoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
horizontalRuleDecoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 1.0,
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 0.3,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||
)),
|
||||
builders: {
|
||||
'code': _MarkdownTextCodeElement(),
|
||||
},
|
||||
),
|
||||
),
|
||||
codeblockDecoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 0.3,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||
),
|
||||
code: GoogleFonts.robotoMono(height: 1),
|
||||
),
|
||||
builders: {},
|
||||
softLineBreak: true,
|
||||
extensionSet: markdown.ExtensionSet(
|
||||
<markdown.BlockSyntax>[
|
||||
@ -148,7 +142,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
);
|
||||
case 'attachments':
|
||||
final attachment = attachments?.firstWhere(
|
||||
(ele) => ele?.rid == segments[1],
|
||||
(ele) => ele?.rid == segments[1],
|
||||
orElse: () => null,
|
||||
);
|
||||
if (attachment != null) {
|
||||
@ -206,14 +200,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 {
|
||||
@ -253,46 +239,3 @@ class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class _MarkdownTextCodeElement extends MarkdownElementBuilder {
|
||||
@override
|
||||
Widget? visitElementAfter(
|
||||
markdown.Element element,
|
||||
TextStyle? preferredStyle,
|
||||
) {
|
||||
var language = '';
|
||||
|
||||
if (element.attributes['class'] != null) {
|
||||
String lg = element.attributes['class'] as String;
|
||||
language = lg.substring(9).trim();
|
||||
}
|
||||
return SizedBox(
|
||||
child: FutureBuilder(
|
||||
future: (() async {
|
||||
final docPath = '../../../';
|
||||
final highlightingPath = join(docPath, 'assets/highlighting', language);
|
||||
await Highlighter.initialize([highlightingPath]);
|
||||
return Highlighter(
|
||||
language: highlightingPath,
|
||||
theme: PlatformDispatcher.instance.platformBrightness == Brightness.light
|
||||
? await HighlighterTheme.loadLightTheme()
|
||||
: await HighlighterTheme.loadDarkTheme(),
|
||||
);
|
||||
})(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final highlighter = snapshot.data!;
|
||||
return Text.rich(
|
||||
highlighter.highlight(element.textContent.trim()),
|
||||
style: GoogleFonts.robotoMono(),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
element.textContent.trim(),
|
||||
style: GoogleFonts.robotoMono(),
|
||||
);
|
||||
},
|
||||
),
|
||||
).padding(all: 8);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/navigation.dart';
|
||||
import 'package:surface/widgets/version_label.dart';
|
||||
|
||||
@ -28,8 +32,9 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
listenable: nav,
|
||||
@ -44,6 +49,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
backgroundColor: backgroundColor,
|
||||
selectedIndex: nav.currentIndex,
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -18,9 +18,7 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context
|
||||
.read<NavigationProvider>()
|
||||
.autoDetectIndex(GoRouter.maybeOf(context));
|
||||
context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
|
||||
});
|
||||
}
|
||||
|
||||
@ -31,11 +29,11 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
|
||||
return ListenableBuilder(
|
||||
listenable: nav,
|
||||
builder: (context, _) {
|
||||
final destinations =
|
||||
nav.destinations.where((ele) => ele.isPinned).toList();
|
||||
final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
|
||||
|
||||
return NavigationRail(
|
||||
selectedIndex: nav.currentIndex,
|
||||
selectedIndex:
|
||||
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
|
||||
destinations: [
|
||||
...destinations.where((ele) => ele.isPinned).map((ele) {
|
||||
return NavigationRailDestination(
|
||||
|
@ -1,51 +1,94 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/navigation.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_bottom_navigation.dart';
|
||||
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
|
||||
import 'package:surface/widgets/navigation/app_rail_navigation.dart';
|
||||
import 'package:surface/widgets/notify_indicator.dart';
|
||||
|
||||
final globalRootScaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
class AppPageScaffold extends StatelessWidget {
|
||||
final String? title;
|
||||
class AppScaffold extends StatelessWidget {
|
||||
final Widget? body;
|
||||
final bool showAppBar;
|
||||
final bool showBottomNavigation;
|
||||
final PreferredSizeWidget? bottomNavigationBar;
|
||||
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,
|
||||
this.title,
|
||||
this.appBar,
|
||||
this.body,
|
||||
this.showAppBar = true,
|
||||
this.showBottomNavigation = false,
|
||||
this.floatingActionButton,
|
||||
this.floatingActionButtonLocation,
|
||||
this.floatingActionButtonAnimator,
|
||||
this.bottomNavigationBar,
|
||||
this.bottomSheet,
|
||||
this.drawer,
|
||||
this.endDrawer,
|
||||
this.onDrawerChanged,
|
||||
this.onEndDrawerChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = GoRouter.maybeOf(context);
|
||||
final routeName = state?.routerDelegate.currentConfiguration.last.route.name;
|
||||
|
||||
final autoTitle = state != null ? 'screen${routeName?.capitalize()}' : 'screen';
|
||||
final appBarHeight = appBar?.preferredSize.height ?? 0;
|
||||
final safeTop = MediaQuery.of(context).padding.top;
|
||||
|
||||
return Scaffold(
|
||||
appBar: showAppBar
|
||||
? AppBar(
|
||||
title: Text(title ?? autoTitle.tr()),
|
||||
)
|
||||
: null,
|
||||
body: body,
|
||||
extendBody: true,
|
||||
extendBodyBehindAppBar: true,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: SizedBox.expand(
|
||||
child: AppBackground(
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
|
||||
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
|
||||
final isCollapseDrawer = cfg.drawerIsCollapsed;
|
||||
final isExpandedDrawer = cfg.drawerIsExpanded;
|
||||
|
||||
final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name;
|
||||
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),
|
||||
],
|
||||
@ -95,62 +139,64 @@ class AppRootScaffold extends StatelessWidget {
|
||||
iconMouseDown: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
|
||||
return AppBackground(
|
||||
isRoot: true,
|
||||
child: Scaffold(
|
||||
key: globalRootScaffoldKey,
|
||||
body: Column(
|
||||
children: [
|
||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
final safeTop = MediaQuery.of(context).padding.top;
|
||||
|
||||
return Scaffold(
|
||||
key: globalRootScaffoldKey,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
|
||||
WindowTitleBarBox(
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||
children: [
|
||||
WindowTitleBarBox(
|
||||
child: MoveWindow(
|
||||
child: Text(
|
||||
'Solar Network',
|
||||
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,
|
||||
Expanded(child: innerWidget),
|
||||
],
|
||||
),
|
||||
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
|
||||
Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()),
|
||||
],
|
||||
),
|
||||
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
||||
drawerEdgeDragWidth: isPopable ? 0 : null,
|
||||
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
63
lib/widgets/notify_indicator.dart
Normal file
63
lib/widgets/notify_indicator.dart
Normal file
@ -0,0 +1,63 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/notification.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
|
||||
class NotifyIndicator extends StatelessWidget {
|
||||
const NotifyIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.read<UserProvider>();
|
||||
final nty = context.watch<NotificationProvider>();
|
||||
|
||||
final show = nty.notifications.isNotEmpty && ua.isAuthorized;
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: nty,
|
||||
builder: (context, _) {
|
||||
return IgnorePointer(
|
||||
ignoring: !show,
|
||||
child: GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
nty.notifications.lastOrNull?.title ??
|
||||
'notificationUnreadCount'.plural(nty.notifications.length),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (nty.notifications.lastOrNull?.body != null)
|
||||
Text(
|
||||
nty.notifications.lastOrNull!.body,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).padding(left: 4),
|
||||
const Gap(8),
|
||||
const Icon(Symbols.notifications_unread, size: 18),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4)
|
||||
: const SizedBox.shrink(),
|
||||
).opacity(show ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.easeInOut,
|
||||
),
|
||||
onTap: () {
|
||||
nty.clear();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -20,6 +22,7 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/reaction.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
@ -33,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/publisher_popover.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
class PostItem extends StatelessWidget {
|
||||
final SnPost data;
|
||||
@ -112,7 +116,7 @@ class PostItem extends StatelessWidget {
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
);
|
||||
} 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();
|
||||
@ -198,6 +202,12 @@ class PostItem extends StatelessWidget {
|
||||
).center();
|
||||
}
|
||||
|
||||
final displayableAttachments = data.preload?.attachments
|
||||
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
|
||||
.toList();
|
||||
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@ -247,17 +257,16 @@ class PostItem extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if ((data.preload?.attachments?.isNotEmpty ?? false) && data.type != 'article')
|
||||
if (displayableAttachments?.isNotEmpty ?? false)
|
||||
AttachmentList(
|
||||
data: data.preload!.attachments!,
|
||||
data: displayableAttachments!,
|
||||
bordered: true,
|
||||
gridded: true,
|
||||
maxHeight: showFullPost ? null : 480,
|
||||
minWidth: 640,
|
||||
maxWidth: MediaQuery.of(context).size.width - 20,
|
||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
if (data.body['content'] != null)
|
||||
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
||||
LinkPreviewWidget(
|
||||
text: data.body['content'],
|
||||
).padding(horizontal: 4),
|
||||
@ -339,7 +348,7 @@ class PostShareImageWidget extends StatelessWidget {
|
||||
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
|
||||
StyledWidget(AttachmentList(
|
||||
data: data.preload!.attachments!,
|
||||
gridded: true,
|
||||
columned: true,
|
||||
)).padding(horizontal: 16, bottom: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -811,6 +820,22 @@ class _PostContentHeader extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
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(
|
||||
onTap: onShare,
|
||||
child: Row(
|
||||
@ -874,13 +899,18 @@ class _PostContentBody extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (data.body['content'] == null) return const SizedBox.shrink();
|
||||
return MarkdownTextContent(
|
||||
isSelectable: isSelectable,
|
||||
final content = MarkdownTextContent(
|
||||
isAutoWarp: data.type == 'story',
|
||||
isEnlargeSticker: true,
|
||||
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
|
||||
content: data.body['content'],
|
||||
attachments: data.preload?.attachments,
|
||||
);
|
||||
|
||||
if (isSelectable) {
|
||||
return SelectionArea(child: content);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1170,3 +1200,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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class PostMiniEditor extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
final PostWriteController _writeController = PostWriteController();
|
||||
final PostWriteController _writeController = PostWriteController(doLoadFromTemporary: false);
|
||||
|
||||
bool _isFetching = false;
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -12,6 +13,7 @@ import 'package:surface/widgets/dialog.dart';
|
||||
class PostReactionPopup extends StatefulWidget {
|
||||
final SnPost data;
|
||||
final Function(Map<String, int> value, int attr, int delta)? onChanged;
|
||||
|
||||
const PostReactionPopup({super.key, required this.data, this.onChanged});
|
||||
|
||||
@override
|
||||
@ -59,6 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
||||
);
|
||||
}
|
||||
}
|
||||
HapticFeedback.mediumImpact();
|
||||
} catch (err) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
@ -84,9 +87,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
||||
children: [
|
||||
const Icon(Symbols.mood, size: 24),
|
||||
const Gap(16),
|
||||
Text('postReactions')
|
||||
.tr()
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
Text('postReactions').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Container(
|
||||
@ -102,9 +103,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
||||
Text('postReactionDownvote').plural(widget.data.totalDownvote),
|
||||
const Gap(24),
|
||||
Icon(
|
||||
widget.data.totalUpvote >= widget.data.totalDownvote
|
||||
? Symbols.trending_up
|
||||
: Symbols.trending_down,
|
||||
widget.data.totalUpvote >= widget.data.totalDownvote ? Symbols.trending_up : Symbols.trending_down,
|
||||
size: 16,
|
||||
),
|
||||
const Gap(8),
|
||||
|
@ -5,10 +5,9 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
import 'package:surface/providers/config.dart';
|
||||
// Keep this import to make the web image render work
|
||||
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
|
||||
class UniversalImage extends StatelessWidget {
|
||||
final String url;
|
||||
@ -55,17 +54,20 @@ class UniversalImage extends StatelessWidget {
|
||||
? null
|
||||
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: TweenAnimationBuilder(
|
||||
tween: Tween(
|
||||
begin: 0,
|
||||
end: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
|
||||
: 0,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxHeight: 80),
|
||||
child: Center(
|
||||
child: TweenAnimationBuilder(
|
||||
tween: Tween(
|
||||
begin: 0,
|
||||
end: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
|
||||
: 0,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -8,11 +8,13 @@ import Foundation
|
||||
import bitsdojo_window_macos
|
||||
import connectivity_plus
|
||||
import device_info_plus
|
||||
import file_picker
|
||||
import file_saver
|
||||
import file_selector_macos
|
||||
import firebase_analytics
|
||||
import firebase_core
|
||||
import firebase_messaging
|
||||
import flutter_inappwebview_macos
|
||||
import flutter_udid
|
||||
import flutter_webrtc
|
||||
import gal
|
||||
@ -35,11 +37,13 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
||||
|
@ -8,63 +8,65 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_picker (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_saver (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_selector_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- Firebase/Analytics (11.4.0):
|
||||
- Firebase/Analytics (11.6.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (11.4.0):
|
||||
- Firebase/Core (11.6.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 11.4.0)
|
||||
- Firebase/CoreOnly (11.4.0):
|
||||
- FirebaseCore (= 11.4.0)
|
||||
- Firebase/Messaging (11.4.0):
|
||||
- FirebaseAnalytics (~> 11.6.0)
|
||||
- Firebase/CoreOnly (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- Firebase/Messaging (11.6.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.4.0)
|
||||
- firebase_analytics (11.3.6):
|
||||
- Firebase/Analytics (= 11.4.0)
|
||||
- FirebaseMessaging (~> 11.6.0)
|
||||
- firebase_analytics (11.4.1):
|
||||
- Firebase/Analytics (= 11.6.0)
|
||||
- firebase_core
|
||||
- FlutterMacOS
|
||||
- firebase_core (3.9.0):
|
||||
- Firebase/CoreOnly (~> 11.4.0)
|
||||
- firebase_core (3.10.1):
|
||||
- Firebase/CoreOnly (~> 11.6.0)
|
||||
- FlutterMacOS
|
||||
- firebase_messaging (15.1.6):
|
||||
- Firebase/CoreOnly (~> 11.4.0)
|
||||
- Firebase/Messaging (~> 11.4.0)
|
||||
- firebase_messaging (15.2.1):
|
||||
- Firebase/CoreOnly (~> 11.6.0)
|
||||
- Firebase/Messaging (~> 11.6.0)
|
||||
- firebase_core
|
||||
- FlutterMacOS
|
||||
- FirebaseAnalytics (11.4.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.4.0)
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseAnalytics (11.6.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.4.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleAppMeasurement (= 11.4.0)
|
||||
- GoogleAppMeasurement (= 11.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (11.4.0):
|
||||
- FirebaseCoreInternal (~> 11.0)
|
||||
- FirebaseCore (11.6.0):
|
||||
- FirebaseCoreInternal (~> 11.6.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- FirebaseCoreInternal (11.6.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseInstallations (11.4.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseInstallations (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (11.4.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseMessaging (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -72,31 +74,34 @@ PODS:
|
||||
- GoogleUtilities/Reachability (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- flutter_inappwebview_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- OrderedSet (~> 6.0.3)
|
||||
- flutter_udid (0.0.1):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain
|
||||
- flutter_webrtc (0.12.2):
|
||||
- flutter_webrtc (0.12.6):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
- FlutterMacOS (1.0.0)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleAppMeasurement (11.4.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.4.0)
|
||||
- GoogleAppMeasurement (11.6.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.4.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.6.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
@ -134,7 +139,7 @@ PODS:
|
||||
- GoogleUtilities/Privacy
|
||||
- in_app_review (2.0.0):
|
||||
- FlutterMacOS
|
||||
- livekit_client (2.3.4):
|
||||
- livekit_client (2.3.5):
|
||||
- flutter_webrtc
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@ -149,6 +154,7 @@ PODS:
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- OrderedSet (6.0.3)
|
||||
- package_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- pasteboard (0.0.1):
|
||||
@ -181,11 +187,13 @@ DEPENDENCIES:
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
|
||||
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/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_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
@ -218,6 +226,7 @@ SPEC REPOS:
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- SAMKeychain
|
||||
- WebRTC-SDK
|
||||
@ -231,6 +240,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
|
||||
device_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||
file_picker:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
||||
file_saver:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
|
||||
file_selector_macos:
|
||||
@ -241,6 +252,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
|
||||
firebase_messaging:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
|
||||
flutter_inappwebview_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
|
||||
flutter_udid:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
|
||||
flutter_webrtc:
|
||||
@ -285,30 +298,33 @@ SPEC CHECKSUMS:
|
||||
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
|
||||
croppy: 25a638bd7d05411d8c697f481568f261037694fc
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
|
||||
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
|
||||
firebase_analytics: a80b3d6645f2f12d626fde928b61dae12e5ea2ef
|
||||
firebase_core: 1dfe1f4d02ad78be0277e320aa3d8384cf46231f
|
||||
firebase_messaging: 61f678060b69a7ae1013e3a939ec8e1c56ef6fcf
|
||||
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
|
||||
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
|
||||
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
|
||||
firebase_analytics: 91efc58e8e37964469fdd59ad11ba36bc97e75d6
|
||||
firebase_core: 75e003524565fb5bd80c9960bc5892e8475821cd
|
||||
firebase_messaging: 082a385eb98b5bb843a566cb30404859c4bd6e25
|
||||
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
|
||||
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
|
||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
||||
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
|
||||
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
|
||||
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
|
||||
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
|
||||
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
|
||||
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
|
||||
flutter_webrtc: 53c9e1285ab32dfb58afb1e1471416a877e23d7a
|
||||
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
|
||||
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
||||
livekit_client: b7ab91e79e657d7d40da16cb2f90d517cb72d406
|
||||
livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
|
||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
|
||||
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
|
240
pubspec.lock
240
pubspec.lock
@ -13,10 +13,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe
|
||||
sha256: e4f2a7ef31b0ab2c89d2bde35ef3e6e6aff1dce5e66069c6540b0e9cfe33ee6b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.48"
|
||||
version: "1.3.50"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
@ -266,10 +266,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d
|
||||
sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.1"
|
||||
version: "6.1.2"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -290,10 +290,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: croppy
|
||||
sha256: "14bb40fd6c1771b093a907ddbf24df9aa49a4e6e379dd630602eb446e30ec629"
|
||||
sha256: bf99b00023df0d7d047e04d27d496d87cbefd968f578d0bd30f342ff75570a12
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.3"
|
||||
cross_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -338,10 +338,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
|
||||
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.7"
|
||||
version: "2.3.8"
|
||||
dart_webrtc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -354,18 +354,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
|
||||
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
device_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "4fa68e53e26ab17b70ca39f072c285562cfc1589df5bb1e9295db90f6645f431"
|
||||
sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.2.0"
|
||||
version: "11.2.2"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -378,10 +378,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
|
||||
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.7.0"
|
||||
version: "5.8.0+1"
|
||||
dio_smart_retry:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -394,10 +394,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
|
||||
sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.1.0"
|
||||
dismissible_page:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -418,10 +418,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: easy_localization
|
||||
sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201
|
||||
sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
version: "3.0.7+1"
|
||||
easy_localization_loader:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -490,10 +490,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204
|
||||
sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.7"
|
||||
version: "8.3.1"
|
||||
file_saver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -538,34 +538,34 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_analytics
|
||||
sha256: "366140abb55418ea23060b779893fa997c2d8e1974a4d1cc4d9590933b65c5fd"
|
||||
sha256: eac382bbcd5ae78c1d1ce5619d13f5a7424429f4bf55df9e3ad5110da34d1060
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.3.6"
|
||||
version: "11.4.1"
|
||||
firebase_analytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_platform_interface
|
||||
sha256: "8e987cf977c0c8f4ad02d9950a9b25b1a9606899f37b66a322a43af05be0246b"
|
||||
sha256: a34db46c367265c4c961626e4b128bfb7b7e50958e7add4c27ba103f5f81b9b0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.8"
|
||||
version: "4.3.1"
|
||||
firebase_analytics_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_web
|
||||
sha256: "0b64ef9060d394bba3d3b4777f49ee098efeeea7b0afb04663c956de6a3da170"
|
||||
sha256: b6b4cef08e45e4c7d48476d9fc49fe9577081809a59026fe95b1a1b1eea165fa
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.10+5"
|
||||
version: "0.5.10+7"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde"
|
||||
sha256: d851c1ca98fd5a4c07c747f8c65dacc2edd84a4d9ac055d32a5f0342529069f5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.9.0"
|
||||
version: "3.10.1"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -586,26 +586,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "151a3ee68736abf293aab66d1317ade53c88abe1db09c75a0460aebf7767bbdf"
|
||||
sha256: e20ea2a0ecf9b0971575ab3ab42a6e285a94e50092c555b090c1a588a81b4d54
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.1.6"
|
||||
version: "15.2.1"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: f331ee51e40c243f90cc7bc059222dfec4e5df53125b08d31fb28961b00d2a9d
|
||||
sha256: c57a92b5ae1857ef4fe4ae2e73452b44d32e984e15ab8b53415ea1bb514bdabd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.49"
|
||||
version: "4.6.1"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: efaf3fdc54cd77e0eedb8e75f7f01c808828c64d052ddbf94d3009974e47d30f
|
||||
sha256: "83694a990d8525d6b01039240b97757298369622ca0253ad0ebcfed221bf8ee0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.9.5"
|
||||
version: "3.10.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -618,10 +618,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_chart
|
||||
sha256: c724234b05e378383e958f3e82ca84a3e1e3c06a0898462044dd8a24b1ee9864
|
||||
sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.70.0"
|
||||
version: "0.70.2"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -675,14 +675,78 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5"
|
||||
sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.2"
|
||||
version: "0.14.3"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -700,10 +764,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: "255b00afa1a7bad19727da6a7780cf3db6c3c12e68d302d85e0ff1fdf173db9e"
|
||||
sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4+3"
|
||||
version: "0.7.5"
|
||||
flutter_native_splash:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -740,10 +804,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123"
|
||||
sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.16"
|
||||
version: "2.0.17"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -766,10 +830,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_webrtc
|
||||
sha256: e82ffd0d0b79621c5554eed73509d7f5bd286d57fef29a573846785c65237fb1
|
||||
sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.5+hotfix.2"
|
||||
version: "0.12.7"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -814,18 +878,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.3"
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539"
|
||||
sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.6.2"
|
||||
version: "14.7.2"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -875,7 +939,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html
|
||||
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
|
||||
@ -886,10 +950,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
|
||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
version: "1.3.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -934,10 +998,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: aa6f1280b670861ac45220cc95adc59bb6ae130259d36f980ccb62220dc5e59f
|
||||
sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+19"
|
||||
version: "0.8.12+20"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -966,18 +1030,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
|
||||
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
version: "0.2.1+2"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
|
||||
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.0"
|
||||
version: "2.10.1"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1086,10 +1150,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: livekit_client
|
||||
sha256: a19bcf8640b45e0730b1e3e3e78be7882dad680c6ebe8ae75294fd8d4612450d
|
||||
sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.4+hotfix.2"
|
||||
version: "2.3.5"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1110,10 +1174,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: markdown
|
||||
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
|
||||
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.2"
|
||||
version: "7.3.0"
|
||||
marquee:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1270,10 +1334,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d"
|
||||
sha256: b15fad91c4d3d1f2b48c053dd41cb82da007c27407dc9ab5f9aa59881d0e39d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.2"
|
||||
version: "8.1.4"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1502,10 +1566,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
|
||||
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.5.0"
|
||||
qr:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1630,10 +1694,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400"
|
||||
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.1.3"
|
||||
version: "10.1.4"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1646,18 +1710,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a
|
||||
sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
version: "2.5.1"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d"
|
||||
sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.4"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1863,14 +1927,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0+3"
|
||||
syntax_highlight:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: syntax_highlight
|
||||
sha256: ee33b6aa82cc722bb9b40152a792181dee222353b486c0255fde666a3e3a4997
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1979,18 +2035,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
|
||||
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
version: "2.4.0"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
|
||||
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
version: "3.1.4"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -2003,18 +2059,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
|
||||
sha256: "7ed22c21d7fdcc88dd6ba7860384af438cd220b251ad65dfc142ab722fabef61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.15"
|
||||
version: "1.1.16"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb"
|
||||
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.12"
|
||||
version: "1.1.13"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2115,10 +2171,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
|
||||
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
webrtc_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2131,10 +2187,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29"
|
||||
sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.10.0"
|
||||
version: "5.10.1"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2160,7 +2216,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
|
@ -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
|
||||
# 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.
|
||||
version: 2.2.1+46
|
||||
version: 2.2.2+60
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@ -54,7 +54,6 @@ dependencies:
|
||||
flutter_markdown: ^0.7.4+1
|
||||
url_launcher: ^6.3.1
|
||||
flutter_animate: ^4.5.0
|
||||
syntax_highlight: ^0.4.0
|
||||
google_fonts: ^6.2.1
|
||||
path: ^1.9.0
|
||||
relative_time: ^5.0.0
|
||||
@ -116,6 +115,9 @@ dependencies:
|
||||
slide_countdown: ^2.0.2
|
||||
video_compress: ^3.1.3
|
||||
cached_network_image: ^3.4.1
|
||||
flutter_inappwebview: ^6.1.5
|
||||
html: ^0.15.5
|
||||
xml: ^6.5.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -1,9 +1,9 @@
|
||||
id = "solian-next"
|
||||
id = "solian"
|
||||
|
||||
[[locations]]
|
||||
id = "solian-next"
|
||||
host = ["sn-next.solsynth.dev"]
|
||||
path = ["/"]
|
||||
id = "solian"
|
||||
hosts = ["sn.solsynth.dev"]
|
||||
paths = ["/"]
|
||||
[[locations.destinations]]
|
||||
id = "solian-next-web"
|
||||
uri = "files:///workdir/solian-next?fallback=index.html&index=index.html"
|
||||
id = "solian-web"
|
||||
uri = "files:///workdir/solian?fallback=index.html&index=index.html"
|
||||
|
@ -16,6 +16,8 @@
|
||||
-->
|
||||
<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 content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
@ -11,6 +11,7 @@
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.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_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
#include <gal/gal_plugin_c_api.h>
|
||||
@ -34,6 +35,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
||||
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
|
||||
FlutterUdidPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
|
||||
FlutterWebRTCPluginRegisterWithRegistrar(
|
||||
|
@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_saver
|
||||
file_selector_windows
|
||||
firebase_core
|
||||
flutter_inappwebview_windows
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
gal
|
||||
|
Reference in New Issue
Block a user