Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
3818328afe | |||
11627e2455 | |||
3f82c06ff8 | |||
2350f59131 | |||
9fe7c9530a | |||
52f1826e91 | |||
28a4c86dbf | |||
85e48ce03b | |||
efef61a8ea | |||
10ead95af9 | |||
838ee4d55d | |||
13e42429a9 | |||
c6ce3fe2b7 | |||
ae9a7eb0fd | |||
5d6fb2442f | |||
5a85985534 | |||
c80499db03 | |||
b8dcdb2315 | |||
b7b921f1f4 | |||
319d5c7d7f | |||
4b5b001739 | |||
db8871a455 | |||
38dcaa6066 | |||
03275b46ca | |||
cf3b482fef | |||
aa4c04d4ef | |||
73b82f65e4 | |||
9471fe40fe | |||
0d1e18735e | |||
8bb62b5992 | |||
1e8a6dea5b | |||
5c2804cc4d | |||
0dbb8f132a | |||
3395f3dbd0 | |||
d258ba776e |
@ -26,7 +26,7 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:launchMode="singleInstance"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
|
@ -12,9 +12,9 @@ post {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"alias": "AteChip",
|
||||
"name": "Cat ate chips",
|
||||
"attachment_id": "d0b692cc64054463",
|
||||
"pack_id": 2
|
||||
"alias": "Meltdown",
|
||||
"name": "Meltdown",
|
||||
"attachment_id": "IpDPHEbWDDCbBofX",
|
||||
"pack_id": 4
|
||||
}
|
||||
}
|
||||
|
11
api/Paperclip/Stickers/Get Sticker Packs.bru
Normal file
11
api/Paperclip/Stickers/Get Sticker Packs.bru
Normal file
@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Get Sticker Packs
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/cgi/uc/stickers/packs
|
||||
body: none
|
||||
auth: none
|
||||
}
|
15
api/Paperclip/Stickers/Get Stickers.bru
Normal file
15
api/Paperclip/Stickers/Get Stickers.bru
Normal file
@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: Get Stickers
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/cgi/uc/stickers?take=10
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
params:query {
|
||||
take: 10
|
||||
}
|
@ -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
|
||||
}
|
||||
|
26
api/Passport/Developer Notify One User.bru
Normal file
26
api/Passport/Developer Notify One User.bru
Normal file
@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Developer Notify One User
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/id/dev/notify/1
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"client_id": "{{third_client_id}}",
|
||||
"client_secret":"{{third_client_tk}}",
|
||||
"type": "general",
|
||||
"subject": "测试",
|
||||
"subtitle": "Alphabot です",
|
||||
"content": "全新通知动画",
|
||||
"metadata": {
|
||||
"image": "D2EDbcrsTugs3xk5"
|
||||
},
|
||||
"priority": 10
|
||||
}
|
||||
}
|
20
api/Wallet/Create Order.bru
Normal file
20
api/Wallet/Create Order.bru
Normal file
@ -0,0 +1,20 @@
|
||||
meta {
|
||||
name: Create Order
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/wa/orders
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"client_id": "highland-mc",
|
||||
"client_secret": "(3^DLAvo3v",
|
||||
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
|
||||
"amount": 500
|
||||
}
|
||||
}
|
21
api/Wallet/Create Transaction.bru
Normal file
21
api/Wallet/Create Transaction.bru
Normal file
@ -0,0 +1,21 @@
|
||||
meta {
|
||||
name: Create Transaction
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/wa/transactions
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"client_id": "alphabot",
|
||||
"client_secret": "_uR0sVnHTh",
|
||||
"remark": "新年红包",
|
||||
"amount": 9705,
|
||||
"payee_id": 2
|
||||
}
|
||||
}
|
20
api/Wallet/Get Order.bru
Normal file
20
api/Wallet/Get Order.bru
Normal file
@ -0,0 +1,20 @@
|
||||
meta {
|
||||
name: Get Order
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/cgi/wa/orders/4
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"client_id": "highland-mc",
|
||||
"client_secret": "(3^DLAvo3v",
|
||||
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
|
||||
"amount": 500
|
||||
}
|
||||
}
|
20
api/Wallet/Get Transaction.bru
Normal file
20
api/Wallet/Get Transaction.bru
Normal file
@ -0,0 +1,20 @@
|
||||
meta {
|
||||
name: Get Transaction
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{endpoint}}/cgi/wa/transactions/67
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"client_id": "highland-mc",
|
||||
"client_secret": "(3^DLAvo3v",
|
||||
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
|
||||
"amount": 500
|
||||
}
|
||||
}
|
11
api/WatchTower/Run Database Maintenance.bru
Normal file
11
api/WatchTower/Run Database Maintenance.bru
Normal file
@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Run Database Maintenance
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/wt/maintenance/database
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
BIN
assets/icon/tray-icon.ico
Normal file
BIN
assets/icon/tray-icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
assets/icon/tray-icon.png
Normal file
BIN
assets/icon/tray-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 228 KiB |
@ -17,6 +17,9 @@
|
||||
"screenAccountProfileEdit": "Edit Profile",
|
||||
"screenAbuseReport": "Abuse Reports",
|
||||
"screenSettings": "Settings",
|
||||
"screenAccountSettings": "Account Settings",
|
||||
"screenFactorSettings": "Auth Factors",
|
||||
"screenAccountWallet": "Wallet",
|
||||
"screenNews": "News",
|
||||
"screenAlbum": "Album",
|
||||
"screenChat": "Chat",
|
||||
@ -104,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",
|
||||
@ -114,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.",
|
||||
@ -180,6 +199,9 @@
|
||||
"other": "{} comments"
|
||||
},
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsDisplayLanguage": "Display Language",
|
||||
"settingsDisplayLanguageDescription": "Set the application language.",
|
||||
"settingsDisplayLanguageSystem": "Follow System",
|
||||
"settingsAppBarTransparent": "Transparent App Bar",
|
||||
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
|
||||
"settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
|
||||
@ -219,6 +241,8 @@
|
||||
"settingsMisc": "Misc",
|
||||
"settingsMiscAbout": "About",
|
||||
"settingsMiscAboutDescription": "View the version information of Solian.",
|
||||
"settingsAccountLanguage": "Account Language",
|
||||
"settingsAccountLanguageDescription": "Set the language for email, notification, and other account-related content.",
|
||||
"sensitiveContent": "Sensitive Content",
|
||||
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
||||
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
||||
@ -532,11 +556,15 @@
|
||||
"postImageShareAds": "Explore posts on the Solar Network",
|
||||
"postShare": "Share",
|
||||
"postShareImage": "Share via Image",
|
||||
"postGetInsight": "Get Insight",
|
||||
"postGetInsightTitle": "AI Insight",
|
||||
"postGetInsightDescription": "AI may make mistakes, check important information.",
|
||||
"appInitializing": "Initializing",
|
||||
"poweredBy": "Powered by {}",
|
||||
"shareIntent": "Share",
|
||||
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
||||
"shareIntentPostStory": "Post a Story",
|
||||
"shareIntentSendChannel": "Share to Channel",
|
||||
"updateAvailable": "Update Available",
|
||||
"updateOngoing": "Updating, please wait...",
|
||||
"custom": "Custom",
|
||||
@ -549,6 +577,7 @@
|
||||
"colorSchemeWhite": "White",
|
||||
"colorSchemeBlack": "Black",
|
||||
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
||||
"postFeaturedComment": "Featured Comment",
|
||||
"postCategoryTechnology": "Technology",
|
||||
"postCategoryGaming": "Gaming",
|
||||
"postCategoryLife": "Life",
|
||||
@ -565,5 +594,21 @@
|
||||
"newsReadingFromReader": "You're reading from HyperNet.Reader",
|
||||
"newsReadingFromOriginal": "You're reading the original article",
|
||||
"newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.",
|
||||
"newsToday": "Today's News"
|
||||
"newsToday": "Today's News",
|
||||
"totpPostSetup": "One More Thing",
|
||||
"totpPostSetupDescription": "Scan the QR Code below with Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden or any of kind of authenticator app which supports TOTP.",
|
||||
"totpNeverShare": "Never share this QR Code",
|
||||
"needHelp": "Need Help?",
|
||||
"needHelpLaunch": "Check out our Goatpedia!",
|
||||
"walletCreate": "Create a Wallet",
|
||||
"walletCreateSubtitle": "Create a wallet to start using Source Points",
|
||||
"walletCreatePassword": "Set a payment password for your new wallet below",
|
||||
"walletCurrencyShort": "SRC",
|
||||
"walletCurrency": {
|
||||
"one": "{} Source Point",
|
||||
"other": "{} Source Points"
|
||||
},
|
||||
"aiThinkingProcess": "AI Thinking Process",
|
||||
"accountSettingsApplied": "Account settings have been applied.",
|
||||
"trayMenuExit": "Exit"
|
||||
}
|
||||
|
@ -15,6 +15,9 @@
|
||||
"screenAccountProfileEdit": "编辑资料",
|
||||
"screenAbuseReport": "滥用检举",
|
||||
"screenSettings": "设置",
|
||||
"screenAccountSettings": "账号设置",
|
||||
"screenFactorSettings": "验证因子",
|
||||
"screenAccountWallet": "钱包",
|
||||
"screenNews": "新闻",
|
||||
"screenAlbum": "相册",
|
||||
"screenChat": "聊天",
|
||||
@ -88,8 +91,18 @@
|
||||
},
|
||||
"loginEnterPassword": "验证代码",
|
||||
"loginSuccess": "登录为 {}",
|
||||
"authFactorDelete": "删除验证因子",
|
||||
"authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
|
||||
"authFactorPassword": "密码",
|
||||
"authFactorPasswordDescription": "注册时选择设置的密码。",
|
||||
"authFactorEmail": "电邮一次性验证码",
|
||||
"authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
|
||||
"authFactorTOTP": "时序验证码",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
|
||||
"authFactorInAppNotify": "应用内通知验证码",
|
||||
"authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
|
||||
"authFactorAdd": "添加新验证因子",
|
||||
"authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
|
||||
"accountIntroTitle": "喜欢您来!",
|
||||
"accountIntroSubtitle": "登陆以探索更广大的世界。",
|
||||
"accountLogout": "退出登录",
|
||||
@ -98,8 +111,14 @@
|
||||
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
|
||||
"accountPublishers": "你的发布者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帐号设置",
|
||||
"accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
|
||||
"accountProfileEdit": "编辑资料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
|
||||
"accountWallet": "钱包",
|
||||
"accountWalletSubtitle": "查看你的余额和交易记录。",
|
||||
"factorSettings": "验证因子",
|
||||
"factorSettingsSubtitle": "管理你的登陆验证方式。",
|
||||
"accountProfileEditApplied": "个人资料修改已被应用。",
|
||||
"publishersNew": "新发布者",
|
||||
"publisherNewSubtitle": "创建一个新的公共身份。",
|
||||
@ -178,6 +197,9 @@
|
||||
"other": "{} 条评论"
|
||||
},
|
||||
"settingsAppearance": "外观",
|
||||
"settingsDisplayLanguage": "显示语言",
|
||||
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
|
||||
"settingsDisplayLanguageSystem": "跟随系统",
|
||||
"settingsBackgroundImage": "背景图片",
|
||||
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
|
||||
"settingsBackgroundImageClear": "清除现存背景图",
|
||||
@ -217,6 +239,8 @@
|
||||
"settingsMisc": "杂项",
|
||||
"settingsMiscAbout": "关于",
|
||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||
"settingsAccountLanguage": "帐号偏好语言",
|
||||
"settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。",
|
||||
"sensitiveContent": "敏感内容",
|
||||
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
||||
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
||||
@ -530,11 +554,15 @@
|
||||
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖图",
|
||||
"postGetInsight": "获取见解",
|
||||
"postGetInsightTitle": "AI 见解",
|
||||
"postGetInsightDescription": "AI 可能会出错,检查信息真实性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
"shareIntentDescription": "您想对您分享的内容做些什么?",
|
||||
"shareIntentPostStory": "发布动态",
|
||||
"shareIntentSendChannel": "分享到聊天频道",
|
||||
"updateAvailable": "检测到更新可用",
|
||||
"updateOngoing": "正在更新,请稍后……",
|
||||
"custom": "自定义",
|
||||
@ -547,6 +575,7 @@
|
||||
"colorSchemeWhite": "白色",
|
||||
"colorSchemeBlack": "黑色",
|
||||
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
||||
"postFeaturedComment": "精选评论",
|
||||
"postCategoryTechnology": "技术",
|
||||
"postCategoryGaming": "游戏",
|
||||
"postCategoryLife": "生活",
|
||||
@ -563,5 +592,21 @@
|
||||
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
|
||||
"newsReadingFromOriginal": "你正在阅读原始文章",
|
||||
"newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。",
|
||||
"newsToday": "快讯"
|
||||
"newsToday": "快讯",
|
||||
"totpPostSetup": "还有一件事",
|
||||
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的验证器扫描本 QR Code 来添加。",
|
||||
"totpNeverShare": "永远不要分享这个 QR Code",
|
||||
"needHelp": "需要帮助?",
|
||||
"needHelpLaunch": "查看我们的山羊维基!",
|
||||
"walletCreate": "创建钱包",
|
||||
"walletCreateSubtitle": "创建于一个钱包来开始使用源点。",
|
||||
"walletCreatePassword": "在下方设置你的付款密码",
|
||||
"walletCurrencyShort": "源点",
|
||||
"walletCurrency": {
|
||||
"one": "{} 源点",
|
||||
"other": "{} 源点"
|
||||
},
|
||||
"aiThinkingProcess": "AI 思考过程",
|
||||
"accountSettingsApplied": "帐号设置已应用。",
|
||||
"trayMenuExit": "退出"
|
||||
}
|
||||
|
@ -15,6 +15,9 @@
|
||||
"screenAccountProfileEdit": "編輯資料",
|
||||
"screenAbuseReport": "濫用檢舉",
|
||||
"screenSettings": "設置",
|
||||
"screenAccountSettings": "賬號設置",
|
||||
"screenFactorSettings": "驗證因子",
|
||||
"screenAccountWallet": "錢包",
|
||||
"screenNews": "新聞",
|
||||
"screenAlbum": "相冊",
|
||||
"screenChat": "聊天",
|
||||
@ -88,8 +91,18 @@
|
||||
},
|
||||
"loginEnterPassword": "驗證代碼",
|
||||
"loginSuccess": "登錄為 {}",
|
||||
"authFactorDelete": "刪除驗證因子",
|
||||
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||
"authFactorPassword": "密碼",
|
||||
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||
"authFactorEmail": "電郵一次性驗證碼",
|
||||
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||
"authFactorTOTP": "時序驗證碼",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||
"authFactorAdd": "添加新驗證因子",
|
||||
"authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。",
|
||||
"accountIntroTitle": "喜歡您來!",
|
||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||
"accountLogout": "退出登錄",
|
||||
@ -98,8 +111,14 @@
|
||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||
"accountPublishers": "你的發佈者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帳號設置",
|
||||
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||
"accountProfileEdit": "編輯資料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
|
||||
"accountWallet": "錢包",
|
||||
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||
"factorSettings": "驗證因子",
|
||||
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||
"publishersNew": "新發布者",
|
||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||
@ -178,6 +197,9 @@
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsDisplayLanguage": "顯示語言",
|
||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||
@ -217,6 +239,8 @@
|
||||
"settingsMisc": "雜項",
|
||||
"settingsMiscAbout": "關於",
|
||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||
"settingsAccountLanguage": "帳號偏好語言",
|
||||
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
|
||||
"sensitiveContent": "敏感內容",
|
||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||
@ -530,11 +554,15 @@
|
||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖圖",
|
||||
"postGetInsight": "獲取見解",
|
||||
"postGetInsightTitle": "AI 見解",
|
||||
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||
"shareIntentPostStory": "發佈動態",
|
||||
"shareIntentSendChannel": "分享到聊天頻道",
|
||||
"updateAvailable": "檢測到更新可用",
|
||||
"updateOngoing": "正在更新,請稍後……",
|
||||
"custom": "自定義",
|
||||
@ -547,6 +575,7 @@
|
||||
"colorSchemeWhite": "白色",
|
||||
"colorSchemeBlack": "黑色",
|
||||
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
||||
"postFeaturedComment": "精選評論",
|
||||
"postCategoryTechnology": "技術",
|
||||
"postCategoryGaming": "遊戲",
|
||||
"postCategoryLife": "生活",
|
||||
@ -563,5 +592,20 @@
|
||||
"newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
|
||||
"newsReadingFromOriginal": "你正在閲讀原始文章",
|
||||
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
|
||||
"newsToday": "快訊"
|
||||
"newsToday": "快訊",
|
||||
"totpPostSetup": "還有一件事",
|
||||
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。",
|
||||
"totpNeverShare": "永遠不要分享這個 QR Code",
|
||||
"needHelp": "需要幫助?",
|
||||
"needHelpLaunch": "查看我們的山羊維基!",
|
||||
"walletCreate": "創建錢包",
|
||||
"walletCreateSubtitle": "創建於一個錢包來開始使用源點。",
|
||||
"walletCreatePassword": "在下方設置你的付款密碼",
|
||||
"walletCurrencyShort": "源點",
|
||||
"walletCurrency": {
|
||||
"one": "{} 源點",
|
||||
"other": "{} 源點"
|
||||
},
|
||||
"aiThinkingProcess": "AI 思考過程",
|
||||
"accountSettingsApplied": "帳號設置已應用。"
|
||||
}
|
||||
|
@ -15,6 +15,9 @@
|
||||
"screenAccountProfileEdit": "編輯資料",
|
||||
"screenAbuseReport": "濫用檢舉",
|
||||
"screenSettings": "設置",
|
||||
"screenAccountSettings": "賬號設置",
|
||||
"screenFactorSettings": "驗證因子",
|
||||
"screenAccountWallet": "錢包",
|
||||
"screenNews": "新聞",
|
||||
"screenAlbum": "相冊",
|
||||
"screenChat": "聊天",
|
||||
@ -88,8 +91,18 @@
|
||||
},
|
||||
"loginEnterPassword": "驗證代碼",
|
||||
"loginSuccess": "登錄為 {}",
|
||||
"authFactorDelete": "刪除驗證因子",
|
||||
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||
"authFactorPassword": "密碼",
|
||||
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||
"authFactorEmail": "電郵一次性驗證碼",
|
||||
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||
"authFactorTOTP": "時序驗證碼",
|
||||
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||
"authFactorAdd": "添加新驗證因子",
|
||||
"authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。",
|
||||
"accountIntroTitle": "喜歡您來!",
|
||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||
"accountLogout": "退出登錄",
|
||||
@ -98,8 +111,14 @@
|
||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||
"accountPublishers": "你的發佈者",
|
||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||
"accountSettings": "帳號設置",
|
||||
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||
"accountProfileEdit": "編輯資料",
|
||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
|
||||
"accountWallet": "錢包",
|
||||
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||
"factorSettings": "驗證因子",
|
||||
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||
"publishersNew": "新發布者",
|
||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||
@ -178,6 +197,9 @@
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsDisplayLanguage": "顯示語言",
|
||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||
@ -217,6 +239,8 @@
|
||||
"settingsMisc": "雜項",
|
||||
"settingsMiscAbout": "關於",
|
||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||
"settingsAccountLanguage": "帳號偏好語言",
|
||||
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
|
||||
"sensitiveContent": "敏感內容",
|
||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||
@ -530,11 +554,15 @@
|
||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||
"postShare": "分享",
|
||||
"postShareImage": "分享帖圖",
|
||||
"postGetInsight": "獲取見解",
|
||||
"postGetInsightTitle": "AI 見解",
|
||||
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||
"appInitializing": "正在初始化",
|
||||
"poweredBy": "由 {} 提供支持",
|
||||
"shareIntent": "分享",
|
||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||
"shareIntentPostStory": "發佈動態",
|
||||
"shareIntentSendChannel": "分享到聊天頻道",
|
||||
"updateAvailable": "檢測到更新可用",
|
||||
"updateOngoing": "正在更新,請稍後……",
|
||||
"custom": "自定義",
|
||||
@ -547,6 +575,7 @@
|
||||
"colorSchemeWhite": "白色",
|
||||
"colorSchemeBlack": "黑色",
|
||||
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
||||
"postFeaturedComment": "精選評論",
|
||||
"postCategoryTechnology": "技術",
|
||||
"postCategoryGaming": "遊戲",
|
||||
"postCategoryLife": "生活",
|
||||
@ -563,5 +592,20 @@
|
||||
"newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章",
|
||||
"newsReadingFromOriginal": "你正在閱讀原始文章",
|
||||
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
|
||||
"newsToday": "快訊"
|
||||
"newsToday": "快訊",
|
||||
"totpPostSetup": "還有一件事",
|
||||
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。",
|
||||
"totpNeverShare": "永遠不要分享這個 QR Code",
|
||||
"needHelp": "需要幫助?",
|
||||
"needHelpLaunch": "查看我們的山羊維基!",
|
||||
"walletCreate": "創建錢包",
|
||||
"walletCreateSubtitle": "創建於一個錢包來開始使用源點。",
|
||||
"walletCreatePassword": "在下方設置你的付款密碼",
|
||||
"walletCurrencyShort": "源點",
|
||||
"walletCurrency": {
|
||||
"one": "{} 源點",
|
||||
"other": "{} 源點"
|
||||
},
|
||||
"aiThinkingProcess": "AI 思考過程",
|
||||
"accountSettingsApplied": "帳號設置已應用。"
|
||||
}
|
||||
|
@ -43,58 +43,58 @@ PODS:
|
||||
- Flutter
|
||||
- file_saver (0.0.1):
|
||||
- Flutter
|
||||
- Firebase/Analytics (11.6.0):
|
||||
- Firebase/Analytics (11.7.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (11.6.0):
|
||||
- Firebase/Core (11.7.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 11.6.0)
|
||||
- Firebase/CoreOnly (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- Firebase/Messaging (11.6.0):
|
||||
- FirebaseAnalytics (~> 11.7.0)
|
||||
- Firebase/CoreOnly (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- Firebase/Messaging (11.7.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.6.0)
|
||||
- firebase_analytics (11.4.0):
|
||||
- Firebase/Analytics (= 11.6.0)
|
||||
- FirebaseMessaging (~> 11.7.0)
|
||||
- firebase_analytics (11.4.2):
|
||||
- Firebase/Analytics (= 11.7.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_core (3.10.0):
|
||||
- Firebase/CoreOnly (= 11.6.0)
|
||||
- firebase_core (3.11.0):
|
||||
- Firebase/CoreOnly (= 11.7.0)
|
||||
- Flutter
|
||||
- firebase_messaging (15.2.0):
|
||||
- Firebase/Messaging (= 11.6.0)
|
||||
- firebase_messaging (15.2.2):
|
||||
- Firebase/Messaging (= 11.7.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseAnalytics (11.6.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseAnalytics (11.7.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.7.0)
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleAppMeasurement (= 11.6.0)
|
||||
- GoogleAppMeasurement (= 11.7.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (11.6.0):
|
||||
- FirebaseCoreInternal (~> 11.6.0)
|
||||
- FirebaseCore (11.7.0):
|
||||
- FirebaseCoreInternal (~> 11.7.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- FirebaseCoreInternal (11.6.0):
|
||||
- FirebaseCoreInternal (11.7.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseInstallations (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseMessaging (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -123,21 +123,21 @@ PODS:
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleAppMeasurement (11.6.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
|
||||
- GoogleAppMeasurement (11.7.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.7.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.6.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.7.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.7.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
@ -179,7 +179,7 @@ PODS:
|
||||
- Flutter
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- Kingfisher (8.1.3)
|
||||
- Kingfisher (8.2.0)
|
||||
- livekit_client (2.3.5):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
@ -379,17 +379,17 @@ SPEC CHECKSUMS:
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
|
||||
firebase_analytics: 07bd7cfbac54bfcdccf2bb2530f9a65486f7ef3f
|
||||
firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10
|
||||
firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c
|
||||
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
|
||||
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
|
||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
||||
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
|
||||
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
|
||||
Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4
|
||||
firebase_analytics: 7236e6115c1b4e62c2270faa29c052a317e31107
|
||||
firebase_core: aa979ae726f00b3ef4ccf59dfb96170af84efbd4
|
||||
firebase_messaging: 3af84b6a90aeac4d7a67fbf4c43a91e7083bea1f
|
||||
FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec
|
||||
FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4
|
||||
FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881
|
||||
FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9
|
||||
FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
@ -397,13 +397,13 @@ SPEC CHECKSUMS:
|
||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
|
||||
GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
|
||||
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
|
||||
livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
|
106
lib/main.dart
106
lib/main.dart
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:croppy/croppy.dart';
|
||||
@ -10,8 +11,10 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
@ -40,6 +43,7 @@ import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
@ -206,7 +210,7 @@ class _AppSplashScreen extends StatefulWidget {
|
||||
State<_AppSplashScreen> createState() => _AppSplashScreenState();
|
||||
}
|
||||
|
||||
class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
void _tryRequestRating() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.containsKey('first_boot_time')) {
|
||||
@ -281,6 +285,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
final notify = context.read<NotificationProvider>();
|
||||
notify.listen();
|
||||
await notify.registerPushNotifications();
|
||||
if (!mounted) return;
|
||||
final sticker = context.read<SnStickerProvider>();
|
||||
await sticker.listStickerEagerly();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
await context.showErrorDialog(err);
|
||||
@ -291,9 +298,62 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
await widgetUpdateRandomPost();
|
||||
}
|
||||
|
||||
Future<void> _hotkeyInitialization() async {
|
||||
if (kIsWeb) return;
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
HotKey quitHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.keyQ,
|
||||
modifiers: [HotKeyModifier.meta],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
await hotKeyManager.register(quitHotKey, keyUpHandler: (_) {
|
||||
_appLifecycleListener?.dispose();
|
||||
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _trayInitialization() async {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||
|
||||
final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png';
|
||||
final appVersion = await PackageInfo.fromPlatform();
|
||||
|
||||
trayManager.addListener(this);
|
||||
await trayManager.setIcon(icon);
|
||||
|
||||
Menu menu = Menu(
|
||||
items: [
|
||||
MenuItem(
|
||||
key: 'version_label',
|
||||
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
|
||||
disabled: true,
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
key: 'exit',
|
||||
label: 'trayMenuExit'.tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
await trayManager.setContextMenu(menu);
|
||||
}
|
||||
|
||||
AppLifecycleListener? _appLifecycleListener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
||||
_appLifecycleListener = AppLifecycleListener(
|
||||
onExitRequested: _onExitRequested,
|
||||
);
|
||||
}
|
||||
|
||||
_trayInitialization();
|
||||
_hotkeyInitialization();
|
||||
_initialize().then((_) {
|
||||
_postInitialization();
|
||||
_tryRequestRating();
|
||||
@ -301,6 +361,50 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<AppExitResponse> _onExitRequested() async {
|
||||
appWindow.hide();
|
||||
return AppExitResponse.cancel;
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconMouseDown() {
|
||||
if (Platform.isWindows) {
|
||||
context.read<NotificationProvider>().clearTray();
|
||||
appWindow.show();
|
||||
} else {
|
||||
trayManager.popUpContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconRightMouseDown() {
|
||||
if (Platform.isWindows) {
|
||||
trayManager.popUpContextMenu();
|
||||
} else {
|
||||
context.read<NotificationProvider>().clearTray();
|
||||
appWindow.show();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayMenuItemClick(MenuItem menuItem) {
|
||||
switch (menuItem.key) {
|
||||
case 'exit':
|
||||
_appLifecycleListener?.dispose();
|
||||
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
|
||||
trayManager.removeListener(this);
|
||||
hotKeyManager.unregisterAll();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
@ -12,6 +12,7 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/notification.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
|
||||
class NotificationProvider extends ChangeNotifier {
|
||||
late final SnNetworkProvider _sn;
|
||||
@ -71,22 +72,48 @@ class NotificationProvider extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
int showingCount = 0;
|
||||
int showingTrayCount = 0;
|
||||
List<SnNotification> notifications = List.empty(growable: true);
|
||||
|
||||
void listen() {
|
||||
_ws.stream.stream.listen((event) {
|
||||
if (event.method == 'notifications.new') {
|
||||
final notification = SnNotification.fromJson(event.payload!);
|
||||
if (showingCount < 0) showingCount = 0;
|
||||
showingCount++;
|
||||
showingTrayCount++;
|
||||
notifications.add(notification);
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (showingCount >= 0) showingCount--;
|
||||
notifyListeners();
|
||||
});
|
||||
notifyListeners();
|
||||
updateTray();
|
||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||
if (doHaptic) HapticFeedback.lightImpact();
|
||||
if (doHaptic) HapticFeedback.mediumImpact();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void clearTray() {
|
||||
showingTrayCount = 0;
|
||||
updateTray();
|
||||
}
|
||||
|
||||
void updateTray() {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||
if (notifications.isEmpty) {
|
||||
trayManager.setTitle('');
|
||||
} else {
|
||||
trayManager.setTitle(' ${notifications.length.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
showingCount = 0;
|
||||
notifications.clear();
|
||||
updateTray();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,10 @@ class SnStickerProvider {
|
||||
late final SnNetworkProvider _sn;
|
||||
final Map<String, SnSticker?> _cache = {};
|
||||
|
||||
final Map<int, List<SnSticker>> stickersByPack = {};
|
||||
|
||||
List<SnSticker> get stickers => _cache.values.where((ele) => ele != null).cast<SnSticker>().toList();
|
||||
|
||||
SnStickerProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
}
|
||||
@ -17,6 +21,12 @@ class SnStickerProvider {
|
||||
return _cache.containsKey(alias) && _cache[alias] == null;
|
||||
}
|
||||
|
||||
void _cacheSticker(SnSticker sticker) {
|
||||
_cache['${sticker.pack.prefix}:${sticker.alias}'] = sticker;
|
||||
if (stickersByPack[sticker.pack.id] == null) stickersByPack[sticker.pack.id] = List.empty(growable: true);
|
||||
if (!stickersByPack[sticker.pack.id]!.contains(sticker)) stickersByPack[sticker.pack.id]!.add(sticker);
|
||||
}
|
||||
|
||||
Future<SnSticker?> lookupSticker(String alias) async {
|
||||
if (_cache.containsKey(alias)) {
|
||||
return _cache[alias];
|
||||
@ -25,7 +35,7 @@ class SnStickerProvider {
|
||||
try {
|
||||
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
|
||||
final sticker = SnSticker.fromJson(resp.data);
|
||||
_cache[alias] = sticker;
|
||||
_cacheSticker(sticker);
|
||||
|
||||
return sticker;
|
||||
} catch (err) {
|
||||
@ -35,4 +45,30 @@ class SnStickerProvider {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> listStickerEagerly() async {
|
||||
var count = await listSticker();
|
||||
for (var page = 1; count > 0; count -= 10) {
|
||||
await listSticker(page: page);
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> listSticker({int page = 0}) async {
|
||||
try {
|
||||
final resp = await _sn.client.get('/cgi/uc/stickers', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': page * 10,
|
||||
});
|
||||
final data = resp.data;
|
||||
final stickers = List.from(data['data']).map((ele) => SnSticker.fromJson(ele));
|
||||
for (final sticker in stickers) {
|
||||
_cacheSticker(sticker);
|
||||
}
|
||||
return data['count'] as int;
|
||||
} catch (err) {
|
||||
log('[Sticker] Failed to list stickers: $err');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,4 +53,11 @@ class UserProvider extends ChangeNotifier {
|
||||
user = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setLanguage(String? value) {
|
||||
if (value == null) return;
|
||||
if (user == null) return;
|
||||
user = user!.copyWith(language: value);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,16 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
await connect();
|
||||
}
|
||||
|
||||
Completer<void>? _connectCompleter;
|
||||
|
||||
Future<void> connect({noRetry = false}) async {
|
||||
if(_connectCompleter != null) {
|
||||
await _connectCompleter!.future;
|
||||
_connectCompleter = null;
|
||||
}
|
||||
|
||||
_connectCompleter = Completer<void>();
|
||||
|
||||
if (!_ua.isAuthorized) return;
|
||||
if (isConnected || conn != null) {
|
||||
disconnect();
|
||||
@ -64,12 +73,13 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
log('Retry connecting to websocket in 3 seconds...');
|
||||
return Future.delayed(
|
||||
const Duration(seconds: 3),
|
||||
() => connect(noRetry: true),
|
||||
() => connect(noRetry: true),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
isBusy = false;
|
||||
notifyListeners();
|
||||
_connectCompleter!.complete();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,6 +47,7 @@ class HomeWidgetProvider {
|
||||
}
|
||||
|
||||
Future<void> widgetUpdateRandomPost() async {
|
||||
if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) return;
|
||||
final snc = await SnNetworkProvider.createOffContextClient();
|
||||
final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1');
|
||||
final post = SnPost.fromJson(resp.data['data'][0]);
|
||||
|
107
lib/router.dart
107
lib/router.dart
@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:surface/screens/abuse_report.dart';
|
||||
import 'package:surface/screens/account.dart';
|
||||
import 'package:surface/screens/account/account_settings.dart';
|
||||
import 'package:surface/screens/account/factor_settings.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
import 'package:surface/screens/account/profile_edit.dart';
|
||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||
@ -31,6 +33,7 @@ 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_scaffold.dart';
|
||||
@ -70,7 +73,7 @@ final _appRoutes = [
|
||||
postRepostId: int.tryParse(
|
||||
state.uri.queryParameters['reposting'] ?? '',
|
||||
),
|
||||
extraProps: state.extra as PostEditorExtraProps?,
|
||||
extraProps: state.extra as PostEditorExtra?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
@ -96,11 +99,52 @@ final _appRoutes = [
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
builder: (context, state) => const AccountScreen(),
|
||||
),
|
||||
GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
|
||||
GoRoute(
|
||||
path: '/wallet',
|
||||
name: 'accountWallet',
|
||||
builder: (context, state) => const WalletScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'accountSettings',
|
||||
builder: (context, state) => AccountSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings/factors',
|
||||
name: 'factorSettings',
|
||||
builder: (context, state) => FactorSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => ProfileEditScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers',
|
||||
name: 'accountPublishers',
|
||||
builder: (context, state) => PublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/new',
|
||||
name: 'accountPublisherNew',
|
||||
builder: (context, state) => AccountPublisherNewScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/edit/:name',
|
||||
name: 'accountPublisherEdit',
|
||||
builder: (context, state) => AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
),
|
||||
]),
|
||||
GoRoute(
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
@ -112,6 +156,7 @@ final _appRoutes = [
|
||||
builder: (context, state) => ChatRoomScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
extra: state.extra as ChatRoomScreenExtra?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
@ -161,20 +206,15 @@ final _appRoutes = [
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/news',
|
||||
name: 'news',
|
||||
builder: (context, state) => const NewsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:hash',
|
||||
name: 'newsDetail',
|
||||
builder: (context, state) => NewsDetailScreen(
|
||||
hash: state.pathParameters['hash']!,
|
||||
),
|
||||
GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
|
||||
GoRoute(
|
||||
path: '/:hash',
|
||||
name: 'newsDetail',
|
||||
builder: (context, state) => NewsDetailScreen(
|
||||
hash: state.pathParameters['hash']!,
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]),
|
||||
GoRoute(
|
||||
path: '/album',
|
||||
name: 'album',
|
||||
@ -205,35 +245,6 @@ final _appRoutes = [
|
||||
name: 'abuseReport',
|
||||
builder: (context, state) => AbuseReportScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => ProfileEditScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers',
|
||||
name: 'accountPublishers',
|
||||
builder: (context, state) => PublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers/new',
|
||||
name: 'accountPublisherNew',
|
||||
builder: (context, state) => AccountPublisherNewScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers/edit/:name',
|
||||
name: 'accountPublisherEdit',
|
||||
builder: (context, state) => AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
@ -13,6 +15,7 @@ import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class AccountScreen extends StatelessWidget {
|
||||
const AccountScreen({super.key});
|
||||
@ -20,11 +23,51 @@ class AccountScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text("screenAccount").tr(),
|
||||
title: Text(
|
||||
"screenAccount",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 5.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
).tr(),
|
||||
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
||||
? Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 56 + MediaQuery.of(context).padding.top,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 10,
|
||||
sigmaY: 10,
|
||||
),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(
|
||||
clampDouble(10 * 0.1, 0, 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.settings, fill: 1),
|
||||
@ -83,16 +126,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
);
|
||||
}).padding(all: 20),
|
||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||
ListTile(
|
||||
title: Text('accountProfileEdit').tr(),
|
||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.contact_page),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountProfileEdit');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountPublishers').tr(),
|
||||
subtitle: Text('accountPublishersSubtitle').tr(),
|
||||
@ -113,6 +146,36 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
GoRouter.of(context).pushNamed('abuseReport');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('factorSettings').tr(),
|
||||
subtitle: Text('factorSettingsSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.lock),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('factorSettings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountWallet').tr(),
|
||||
subtitle: Text('accountWalletSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.wallet),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountWallet');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountSettings').tr(),
|
||||
subtitle: Text('accountSettingsSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.manage_accounts),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountSettings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountLogout').tr(),
|
||||
subtitle: Text('accountLogoutSubtitle').tr(),
|
||||
@ -134,33 +197,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
await Hive.initFlutter();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountDeletion'.tr()),
|
||||
subtitle: Text('accountDeletionActionDescription'.tr()),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.person_cancel),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'accountDeletion'.tr(),
|
||||
'accountDeletionDescription'.tr(),
|
||||
)
|
||||
.then((value) {
|
||||
if (!value || !context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
sn.client.post('/cgi/id/users/me/deletion').then((value) {
|
||||
if (context.mounted) {
|
||||
context.showSnackbar('accountDeletionSubmitted'.tr());
|
||||
}
|
||||
}).catchError((err) {
|
||||
if (context.mounted) {
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
126
lib/screens/account/account_settings.dart
Normal file
126
lib/screens/account/account_settings.dart
Normal file
@ -0,0 +1,126 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:intl/locale.dart';
|
||||
|
||||
class AccountSettingsScreen extends StatelessWidget {
|
||||
const AccountSettingsScreen({super.key});
|
||||
|
||||
Future<void> _setAccountLanguage(BuildContext context, Locale? value) async {
|
||||
if (value == null) return;
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client.put('/cgi/id/users/me/language', data: {
|
||||
'language': value.toString(),
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
context.showSnackbar('accountSettingsApplied'.tr());
|
||||
await ua.refreshUser();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountSettings').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text('settingsAccountLanguage').tr(),
|
||||
subtitle: Text('settingsAccountLanguageDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.translate),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<Locale?>(
|
||||
isExpanded: true,
|
||||
items: [
|
||||
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
|
||||
return DropdownMenuItem<Locale?>(
|
||||
value: Locale.parse(ele.toString()),
|
||||
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
|
||||
);
|
||||
}),
|
||||
],
|
||||
value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'),
|
||||
onChanged: (Locale? value) {
|
||||
if (value == null) return;
|
||||
_setAccountLanguage(context, value);
|
||||
ua.setLanguage(value.toString());
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 5,
|
||||
),
|
||||
height: 40,
|
||||
width: 160,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountProfileEdit').tr(),
|
||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.contact_page),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountProfileEdit');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountDeletion'.tr()),
|
||||
subtitle: Text('accountDeletionActionDescription'.tr()),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.person_cancel),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'accountDeletion'.tr(),
|
||||
'accountDeletionDescription'.tr(),
|
||||
)
|
||||
.then((value) {
|
||||
if (!value || !context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
sn.client.post('/cgi/id/users/me/deletion').then((value) {
|
||||
if (context.mounted) {
|
||||
context.showSnackbar('accountDeletionSubmitted'.tr());
|
||||
}
|
||||
}).catchError((err) {
|
||||
if (context.mounted) {
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
294
lib/screens/account/factor_settings.dart
Normal file
294
lib/screens/account/factor_settings.dart
Normal file
@ -0,0 +1,294 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/auth.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
final Map<int, (String, String, IconData)> kFactorTypes = {
|
||||
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
|
||||
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
|
||||
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
|
||||
3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active),
|
||||
};
|
||||
|
||||
class FactorSettingsScreen extends StatefulWidget {
|
||||
const FactorSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FactorSettingsScreen> createState() => _FactorSettingsScreenState();
|
||||
}
|
||||
|
||||
class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
||||
bool _isBusy = false;
|
||||
List<SnAuthFactor>? _factors;
|
||||
|
||||
Future<void> _fetchFactors() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/me/factors');
|
||||
_factors = List<SnAuthFactor>.from(
|
||||
resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchFactors();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenFactorSettings').tr(),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(
|
||||
isActive: _isBusy,
|
||||
),
|
||||
ListTile(
|
||||
title: Text('authFactorAdd').tr(),
|
||||
subtitle: Text('authFactorAddSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.add),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _FactorNewDialog(
|
||||
currentlyHave: _factors!,
|
||||
),
|
||||
).then((val) {
|
||||
if (val == true) _fetchFactors();
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchFactors,
|
||||
child: ListView.builder(
|
||||
itemCount: _factors?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final ele = _factors![idx];
|
||||
return ListTile(
|
||||
title: Text(kFactorTypes[ele.type]!.$1).tr(),
|
||||
subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 12),
|
||||
leading: Icon(kFactorTypes[ele.type]!.$3),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: ele.type > 0
|
||||
? () {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'authFactorDelete'.tr(),
|
||||
'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]),
|
||||
)
|
||||
.then((val) async {
|
||||
if (!val) return;
|
||||
try {
|
||||
if (!context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/id/users/me/factors/${ele.id}');
|
||||
_fetchFactors();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FactorNewDialog extends StatefulWidget {
|
||||
final List<SnAuthFactor> currentlyHave;
|
||||
|
||||
const _FactorNewDialog({required this.currentlyHave});
|
||||
|
||||
@override
|
||||
State<_FactorNewDialog> createState() => _FactorNewDialogState();
|
||||
}
|
||||
|
||||
class _FactorNewDialogState extends State<_FactorNewDialog> {
|
||||
int? _factorType;
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _submit() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.post('/cgi/id/users/me/factors', data: {
|
||||
'type': _factorType,
|
||||
});
|
||||
final factor = SnAuthFactor.fromJson(resp.data);
|
||||
if (!mounted) return;
|
||||
if (factor.type == 2) {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _FactorTotpFactorDialog(factor: factor),
|
||||
);
|
||||
}
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(true);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('authFactorAdd').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<int>(
|
||||
hint: Text(
|
||||
'Select Item',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
value: _factorType,
|
||||
items: kFactorTypes.entries.map(
|
||||
(ele) {
|
||||
final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key);
|
||||
return DropdownMenuItem<int>(
|
||||
enabled: !contains,
|
||||
value: ele.key,
|
||||
child: Text(
|
||||
ele.value.$1.tr(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
).opacity(contains ? 0.75 : 1),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
onChanged: (val) => setState(() {
|
||||
_factorType = val;
|
||||
}),
|
||||
buttonStyleData: ButtonStyleData(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => Navigator.of(context).pop(),
|
||||
child: Text('dialogCancel').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _submit(),
|
||||
child: Text('dialogConfirm').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FactorTotpFactorDialog extends StatelessWidget {
|
||||
final SnAuthFactor factor;
|
||||
|
||||
const _FactorTotpFactorDialog({required this.factor});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
'totpPostSetup',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr().width(280),
|
||||
),
|
||||
const Gap(4),
|
||||
Center(
|
||||
child: Text(
|
||||
'totpPostSetupDescription',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
).tr().width(280),
|
||||
),
|
||||
const Gap(16),
|
||||
QrImageView(
|
||||
padding: EdgeInsets.zero,
|
||||
data: factor.config!['url'],
|
||||
errorCorrectionLevel: QrErrorCorrectLevel.H,
|
||||
version: QrVersions.auto,
|
||||
size: 160,
|
||||
gapless: true,
|
||||
eyeStyle: QrEyeStyle(
|
||||
eyeShape: QrEyeShape.circle,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
dataModuleStyle: QrDataModuleStyle(
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Center(
|
||||
child: Text(
|
||||
'totpNeverShare',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
).tr().bold().width(280),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/account/factor_settings.dart';
|
||||
import 'package:surface/types/auth.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
@ -14,11 +15,6 @@ import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import '../../providers/websocket.dart';
|
||||
|
||||
final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
|
||||
0: ('authFactorPassword'.tr(), Symbols.password, false),
|
||||
1: ('authFactorEmail'.tr(), Symbols.email, true),
|
||||
};
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@ -212,7 +208,9 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
autofillHints: [
|
||||
(_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode
|
||||
widget.factor!.type == 0
|
||||
? AutofillHints.password
|
||||
: AutofillHints.oneTimeCode
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
@ -267,7 +265,8 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
||||
bool _isBusy = false;
|
||||
int? _factorPicked;
|
||||
|
||||
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
||||
|
||||
void _performGetFactorCode() async {
|
||||
if (_factorPicked == null) return;
|
||||
@ -328,11 +327,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
||||
),
|
||||
),
|
||||
secondary: Icon(
|
||||
_factorLabelMap[x.type]?.$2 ?? Symbols.question_mark,
|
||||
kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
|
||||
),
|
||||
title: Text(
|
||||
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
|
||||
),
|
||||
kFactorTypes[x.type]?.$1 ?? 'unknown',
|
||||
).tr(),
|
||||
enabled: !widget.ticket!.factorTrail.contains(x.id),
|
||||
value: _factorPicked == x.id,
|
||||
onChanged: (value) {
|
||||
@ -408,11 +407,14 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final lookupResp = await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
||||
final lookupResp =
|
||||
await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
||||
await sn.client.post('/cgi/id/users/me/password-reset', data: {
|
||||
'user_id': lookupResp.data['id'],
|
||||
});
|
||||
if (mounted) context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||
if (mounted) {
|
||||
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
@ -437,7 +439,8 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
widget.onTicket(result.ticket);
|
||||
|
||||
// Pull factors
|
||||
final factorResp = await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
||||
final factorResp =
|
||||
await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
||||
'ticketId': result.ticket!.id.toString(),
|
||||
});
|
||||
widget.onFactor(
|
||||
@ -531,7 +534,10 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
'termAcceptNextWithAgree'.tr(),
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha((255 * 0.75).round()),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
|
@ -44,6 +44,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
'nick': nickname,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'language': EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
@ -10,8 +10,10 @@ import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/account_select.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
@ -20,6 +22,7 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
class ChannelDetailScreen extends StatefulWidget {
|
||||
final String scope;
|
||||
final String alias;
|
||||
|
||||
const ChannelDetailScreen({
|
||||
super.key,
|
||||
required this.scope,
|
||||
@ -55,8 +58,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client
|
||||
.get('/cgi/im/channels/${_channel!.keyPath}/members/me');
|
||||
final resp = await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/members/me');
|
||||
_profile = SnChannelMember.fromJson(resp.data);
|
||||
_notifyLevel = _profile!.notify;
|
||||
if (!mounted) return;
|
||||
@ -143,6 +145,25 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addMember(SnAccount related) async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post(
|
||||
'/cgi/im/channels/${_channel!.keyPath}/members',
|
||||
data: {'related': related.name},
|
||||
);
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('channelMemberAdded'.tr());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _showChannelProfileDetail() {
|
||||
showDialog(
|
||||
context: context,
|
||||
@ -166,13 +187,16 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showMemberAdd() {
|
||||
showModalBottomSheet(
|
||||
void _showMemberAdd() async {
|
||||
final user = await showModalBottomSheet<SnAccount?>(
|
||||
context: context,
|
||||
builder: (context) => _NewChannelMemberWidget(
|
||||
channel: _channel!,
|
||||
builder: (context) => AccountSelect(
|
||||
title: 'channelMemberAdd'.tr(),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (user == null) return;
|
||||
_addMember(user);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -221,11 +245,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('channelDetailPersonalRegion')
|
||||
.bold()
|
||||
.fontSize(17)
|
||||
.tr()
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
Text('channelDetailPersonalRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.notifications),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
@ -264,8 +284,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
),
|
||||
ListTile(
|
||||
leading: AccountImage(
|
||||
content:
|
||||
ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
||||
content: ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
||||
radius: 18,
|
||||
),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
@ -284,8 +303,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('channelActionLeave').tr(),
|
||||
subtitle: Text('channelActionLeaveDescription').tr(),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: _leaveChannel,
|
||||
),
|
||||
],
|
||||
@ -293,11 +311,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('channelDetailMemberRegion')
|
||||
.bold()
|
||||
.fontSize(17)
|
||||
.tr()
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
Text('channelDetailMemberRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.group),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
@ -319,11 +333,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('channelDetailAdminRegion')
|
||||
.bold()
|
||||
.fontSize(17)
|
||||
.tr()
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
Text('channelDetailAdminRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.edit),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
@ -362,18 +372,17 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
class _ChannelProfileDetailDialog extends StatefulWidget {
|
||||
final SnChannel channel;
|
||||
final SnChannelMember current;
|
||||
|
||||
const _ChannelProfileDetailDialog({
|
||||
required this.channel,
|
||||
required this.current,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ChannelProfileDetailDialog> createState() =>
|
||||
_ChannelProfileDetailDialogState();
|
||||
State<_ChannelProfileDetailDialog> createState() => _ChannelProfileDetailDialogState();
|
||||
}
|
||||
|
||||
class _ChannelProfileDetailDialogState
|
||||
extends State<_ChannelProfileDetailDialog> {
|
||||
class _ChannelProfileDetailDialogState extends State<_ChannelProfileDetailDialog> {
|
||||
bool _isBusy = false;
|
||||
|
||||
final TextEditingController _nickController = TextEditingController();
|
||||
@ -444,11 +453,11 @@ class _ChannelProfileDetailDialogState
|
||||
|
||||
class _ChannelMemberListWidget extends StatefulWidget {
|
||||
final SnChannel channel;
|
||||
|
||||
const _ChannelMemberListWidget({required this.channel});
|
||||
|
||||
@override
|
||||
State<_ChannelMemberListWidget> createState() =>
|
||||
_ChannelMemberListWidgetState();
|
||||
State<_ChannelMemberListWidget> createState() => _ChannelMemberListWidgetState();
|
||||
}
|
||||
|
||||
class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
@ -463,12 +472,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
try {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get(
|
||||
'/cgi/im/channels/${widget.channel.keyPath}/members',
|
||||
queryParameters: {
|
||||
'take': 10,
|
||||
'offset': 0,
|
||||
});
|
||||
final resp = await sn.client.get('/cgi/im/channels/${widget.channel.keyPath}/members', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': 0,
|
||||
});
|
||||
final out = List<SnChannelMember>.from(
|
||||
resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
|
||||
);
|
||||
@ -526,9 +533,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
children: [
|
||||
const Icon(Symbols.group, size: 24),
|
||||
const Gap(16),
|
||||
Text('channelMemberManage')
|
||||
.tr()
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
Text('channelMemberManage').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Expanded(
|
||||
@ -539,8 +544,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
},
|
||||
child: InfiniteList(
|
||||
itemCount: _members.length,
|
||||
hasReachedMax:
|
||||
_totalCount != null && _members.length >= _totalCount!,
|
||||
hasReachedMax: _totalCount != null && _members.length >= _totalCount!,
|
||||
isLoading: _isBusy,
|
||||
onFetchData: _fetchMembers,
|
||||
itemBuilder: (context, index) {
|
||||
@ -551,8 +555,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
||||
),
|
||||
title: Text(
|
||||
ud.getAccountFromCache(member.accountId)?.name ??
|
||||
'unknown'.tr(),
|
||||
ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
||||
),
|
||||
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
||||
trailing: SizedBox(
|
||||
@ -562,8 +565,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed:
|
||||
_isUpdating ? null : () => _deleteMember(member),
|
||||
onPressed: _isUpdating ? null : () => _deleteMember(member),
|
||||
icon: const Icon(Symbols.person_remove),
|
||||
),
|
||||
],
|
||||
@ -578,83 +580,3 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NewChannelMemberWidget extends StatefulWidget {
|
||||
final SnChannel channel;
|
||||
const _NewChannelMemberWidget({required this.channel});
|
||||
|
||||
@override
|
||||
State<_NewChannelMemberWidget> createState() =>
|
||||
_NewChannelMemberWidgetState();
|
||||
}
|
||||
|
||||
class _NewChannelMemberWidgetState extends State<_NewChannelMemberWidget> {
|
||||
bool _isBusy = false;
|
||||
|
||||
final TextEditingController _relatedController = TextEditingController();
|
||||
|
||||
Future<void> _performAction() async {
|
||||
if (_relatedController.text.isEmpty) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post(
|
||||
'/cgi/im/channels/${widget.channel.keyPath}/members',
|
||||
data: {
|
||||
'related': _relatedController.text,
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
context.showSnackbar('channelMemberAdded'.tr());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_relatedController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StyledWidget(Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'channelMemberAdd',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr(),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
controller: _relatedController,
|
||||
readOnly: _isBusy,
|
||||
autocorrect: false,
|
||||
autofocus: true,
|
||||
textCapitalization: TextCapitalization.none,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldMemberRelatedName'.tr(),
|
||||
suffix: SizedBox(
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
onPressed: _isBusy ? null : () => _performAction(),
|
||||
icon: Icon(Symbols.send),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
)
|
||||
],
|
||||
)).padding(all: 24);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -9,9 +10,12 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/chat_message_controller.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
||||
@ -23,14 +27,19 @@ import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
import '../../providers/user_directory.dart';
|
||||
import '../../providers/userinfo.dart';
|
||||
class ChatRoomScreenExtra {
|
||||
final String? initialText;
|
||||
final List<PostWriteMedia>? initialAttachments;
|
||||
|
||||
ChatRoomScreenExtra({this.initialText, this.initialAttachments});
|
||||
}
|
||||
|
||||
class ChatRoomScreen extends StatefulWidget {
|
||||
final String scope;
|
||||
final String alias;
|
||||
final ChatRoomScreenExtra? extra;
|
||||
|
||||
const ChatRoomScreen({super.key, required this.scope, required this.alias});
|
||||
const ChatRoomScreen({super.key, required this.scope, required this.alias, this.extra});
|
||||
|
||||
@override
|
||||
State<ChatRoomScreen> createState() => _ChatRoomScreenState();
|
||||
@ -177,8 +186,23 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
_messageController = ChatMessageController(context);
|
||||
_fetchChannel().then((_) async {
|
||||
await _messageController.initialize(_channel!);
|
||||
await _messageController.checkUpdate();
|
||||
await _fetchOngoingCall();
|
||||
|
||||
if (widget.extra != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
log('[ChatInput] Setting initial text and attachments...');
|
||||
if (widget.extra!.initialText != null) {
|
||||
_inputGlobalKey.currentState?.setInitialText(widget.extra!.initialText!);
|
||||
}
|
||||
if (widget.extra!.initialAttachments != null) {
|
||||
_inputGlobalKey.currentState?.setInitialAttachments(widget.extra!.initialAttachments!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
_messageController.checkUpdate(),
|
||||
_fetchOngoingCall(),
|
||||
]);
|
||||
});
|
||||
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
|
@ -7,6 +7,7 @@ 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';
|
||||
@ -96,6 +97,8 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: ExpandableFab(
|
||||
@ -243,8 +246,10 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
),
|
||||
openColor: Colors.transparent,
|
||||
openElevation: 0,
|
||||
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75),
|
||||
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)),
|
||||
),
|
||||
|
@ -6,15 +6,15 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/relationship.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/account_select.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
import '../providers/userinfo.dart';
|
||||
import '../widgets/unauthorized_hint.dart';
|
||||
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||
|
||||
const kFriendStatus = {
|
||||
0: 'friendStatusPending',
|
||||
@ -168,6 +168,24 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _sendRequest(SnAccount user) async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/id/users/me/relations', data: {
|
||||
'related': user.name,
|
||||
});
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('friendRequestSent'.tr());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -199,11 +217,16 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Symbols.add),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
onPressed: () async {
|
||||
final user = await showModalBottomSheet<SnAccount?>(
|
||||
context: context,
|
||||
builder: (context) => _NewFriendWidget(),
|
||||
builder: (context) => AccountSelect(
|
||||
title: 'friendNew'.tr(),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (user == null) return;
|
||||
_sendRequest(user);
|
||||
},
|
||||
),
|
||||
body: Column(
|
||||
@ -231,8 +254,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: _showBlocks,
|
||||
),
|
||||
if (_requests.isNotEmpty || _blocks.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
@ -264,16 +286,12 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _changeRelation(relation, 2),
|
||||
onTap: _isUpdating ? null : () => _changeRelation(relation, 2),
|
||||
child: Text('friendBlock').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
InkWell(
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _deleteRelation(relation),
|
||||
onTap: _isUpdating ? null : () => _deleteRelation(relation),
|
||||
child: Text('friendDeleteAction').tr(),
|
||||
),
|
||||
],
|
||||
@ -293,83 +311,9 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _NewFriendWidget extends StatefulWidget {
|
||||
const _NewFriendWidget();
|
||||
|
||||
@override
|
||||
State<_NewFriendWidget> createState() => _NewFriendWidgetState();
|
||||
}
|
||||
|
||||
class _NewFriendWidgetState extends State<_NewFriendWidget> {
|
||||
bool _isBusy = false;
|
||||
|
||||
final TextEditingController _relatedController = TextEditingController();
|
||||
|
||||
Future<void> _sendRequest() async {
|
||||
if (_relatedController.text.isEmpty) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/id/users/me/relations', data: {
|
||||
'related': _relatedController.text,
|
||||
});
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
context.showSnackbar('friendRequestSent'.tr());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_relatedController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StyledWidget(Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'friendNew',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr(),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
controller: _relatedController,
|
||||
readOnly: _isBusy,
|
||||
autocorrect: false,
|
||||
autofocus: true,
|
||||
textCapitalization: TextCapitalization.none,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldFriendRelatedName'.tr(),
|
||||
suffix: SizedBox(
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
onPressed: _isBusy ? null : () => _sendRequest(),
|
||||
icon: Icon(Symbols.send),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
)
|
||||
],
|
||||
)).padding(all: 24);
|
||||
}
|
||||
}
|
||||
|
||||
class _FriendshipListWidget extends StatefulWidget {
|
||||
final List<SnRelationship> relations;
|
||||
|
||||
const _FriendshipListWidget({required this.relations});
|
||||
|
||||
@override
|
||||
@ -476,9 +420,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(kFriendStatus[relation.status] ?? 'unknown')
|
||||
.tr()
|
||||
.opacity(0.75),
|
||||
Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75),
|
||||
if (relation.status == 0)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
@ -499,8 +441,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap:
|
||||
_isBusy ? null : () => _changeRelation(relation, 1),
|
||||
onTap: _isBusy ? null : () => _changeRelation(relation, 1),
|
||||
child: Text('friendUnblock').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
|
@ -51,7 +51,7 @@ class HomeScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
static const List<HomeScreenDashEntry> kCards = [
|
||||
late final List<HomeScreenDashEntry> kCards = [
|
||||
HomeScreenDashEntry(
|
||||
name: 'dashEntryRecommendation',
|
||||
child: _HomeDashRecommendationPostWidget(),
|
||||
@ -69,7 +69,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
HomeScreenDashEntry(
|
||||
name: 'dashEntryTodayNews',
|
||||
child: _HomeDashTodayNews(),
|
||||
cols: 2,
|
||||
cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2,
|
||||
),
|
||||
];
|
||||
|
||||
@ -288,12 +288,13 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
_article!.title,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18),
|
||||
maxLines: 2,
|
||||
maxLines: MediaQuery.of(context).size.width >= 640 ? 2 : 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
@ -302,20 +303,18 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75);
|
||||
}
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75);
|
||||
}),
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
onTap: () {
|
||||
@ -515,6 +514,11 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
'+${_todayRecord!.resultExperience} EXP',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (_todayRecord!.resultCoin >= 0)
|
||||
Text(
|
||||
'+${_todayRecord!.resultCoin} ${'walletCurrencyShort'.tr()}',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -175,54 +175,57 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
),
|
||||
if (_articleFragment != null && _isReadingFromReader)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
|
||||
Builder(builder: (context) {
|
||||
final htmlDescription = parse(_article!.description);
|
||||
return Text(
|
||||
htmlDescription.children.map((ele) => ele.text.trim()).join(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
);
|
||||
}),
|
||||
Builder(builder: (context) {
|
||||
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||
return Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75);
|
||||
}),
|
||||
Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75),
|
||||
const Divider(),
|
||||
..._parseHtmlToWidgets(_articleFragment!.children),
|
||||
const Divider(),
|
||||
InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Reference from original website',
|
||||
style: TextStyle(decoration: TextDecoration.underline),
|
||||
),
|
||||
const Gap(4),
|
||||
Icon(Icons.launch, size: 16),
|
||||
],
|
||||
).opacity(0.85),
|
||||
onTap: () {
|
||||
launchUrlString(_article!.url);
|
||||
},
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
).padding(horizontal: 12, vertical: 16),
|
||||
),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
|
||||
Builder(builder: (context) {
|
||||
final htmlDescription = parse(_article!.description);
|
||||
return Text(
|
||||
htmlDescription.children.map((ele) => ele.text.trim()).join(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
);
|
||||
}),
|
||||
Builder(builder: (context) {
|
||||
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||
return Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75);
|
||||
}),
|
||||
Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75),
|
||||
const Divider(),
|
||||
..._parseHtmlToWidgets(_articleFragment!.children),
|
||||
const Divider(),
|
||||
InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Reference from original website',
|
||||
style: TextStyle(decoration: TextDecoration.underline),
|
||||
),
|
||||
const Gap(4),
|
||||
Icon(Icons.launch, size: 16),
|
||||
],
|
||||
).opacity(0.85),
|
||||
onTap: () {
|
||||
launchUrlString(_article!.url);
|
||||
},
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
).padding(horizontal: 12, vertical: 16),
|
||||
),
|
||||
).center(),
|
||||
)
|
||||
else if (_article != null)
|
||||
Expanded(
|
||||
|
@ -70,11 +70,16 @@ class _NewsScreenState extends State<NewsScreen> {
|
||||
sliver: SliverAppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNews').tr(),
|
||||
floating: true,
|
||||
snap: true,
|
||||
bottom: TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(child: Text('newsAllSources'.tr())),
|
||||
for (final source in _sources!) Tab(child: Text(source.label)),
|
||||
Tab(child: Text('newsAllSources'.tr()).textColor(Theme.of(context).appBarTheme.foregroundColor)),
|
||||
for (final source in _sources!)
|
||||
Tab(
|
||||
child: Text(source.label).textColor(Theme.of(context).appBarTheme.foregroundColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -146,80 +151,87 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchArticles,
|
||||
child: InfiniteList(
|
||||
isLoading: _isBusy,
|
||||
itemCount: _articles.length,
|
||||
hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
|
||||
onFetchData: () {
|
||||
_fetchArticles();
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final article = _articles[index];
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchArticles,
|
||||
child: InfiniteList(
|
||||
isLoading: _isBusy,
|
||||
itemCount: _articles.length,
|
||||
hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
|
||||
onFetchData: () {
|
||||
_fetchArticles();
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final article = _articles[index];
|
||||
|
||||
final baseUri = Uri.parse(article.url);
|
||||
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||
final baseUri = Uri.parse(article.url);
|
||||
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||
|
||||
final htmlDescription = parse(article.description);
|
||||
final date = article.publishedAt ?? article.createdAt;
|
||||
final htmlDescription = parse(article.description);
|
||||
final date = article.publishedAt ?? article.createdAt;
|
||||
|
||||
return Card(
|
||||
child: InkWell(
|
||||
radius: 8,
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'newsDetail',
|
||||
pathParameters: {'hash': article.hash},
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(8),
|
||||
topLeft: Radius.circular(8),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AutoResizeUniversalImage(
|
||||
article.thumbnail.startsWith('http') ? article.thumbnail : '$baseUrl/${article.thumbnail}',
|
||||
return Card(
|
||||
child: InkWell(
|
||||
radius: 8,
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'newsDetail',
|
||||
pathParameters: {'hash': article.hash},
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(8),
|
||||
topLeft: Radius.circular(8),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AutoResizeUniversalImage(
|
||||
article.thumbnail.startsWith('http')
|
||||
? article.thumbnail
|
||||
: '$baseUrl/${article.thumbnail}',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
|
||||
.textStyle(Theme.of(context).textTheme.bodyMedium!)
|
||||
.padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(widget.allSources.where((x) => x.id == article.source).first.label)
|
||||
.textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
const Gap(16),
|
||||
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
|
||||
.textStyle(Theme.of(context).textTheme.bodyMedium!)
|
||||
.padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(widget.allSources.where((x) => x.id == article.source).first.label)
|
||||
.textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75).padding(horizontal: 16),
|
||||
Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75).padding(horizontal: 16),
|
||||
const Gap(16),
|
||||
],
|
||||
).opacity(0.75).padding(horizontal: 16),
|
||||
Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75).padding(horizontal: 16),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/notification.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/notification.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
@ -21,6 +22,16 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
import '../providers/userinfo.dart';
|
||||
import '../widgets/unauthorized_hint.dart';
|
||||
|
||||
const Map<String, IconData> kNotificationTopicIcons = {
|
||||
'general': Symbols.notifications,
|
||||
'passport.security.alert': Symbols.gpp_maybe,
|
||||
'passport.security.otp': Symbols.password,
|
||||
'interactive.subscription': Symbols.subscriptions,
|
||||
'interactive.feedback': Symbols.add_reaction,
|
||||
'messaging.callStart': Symbols.call_received,
|
||||
'wallet.transaction.new': Symbols.receipt,
|
||||
};
|
||||
|
||||
class NotificationScreen extends StatefulWidget {
|
||||
const NotificationScreen({super.key});
|
||||
|
||||
@ -36,13 +47,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
final List<SnNotification> _notifications = List.empty(growable: true);
|
||||
int? _totalCount;
|
||||
|
||||
static const Map<String, IconData> kNotificationTopicIcons = {
|
||||
'passport.security.alert': Symbols.gpp_maybe,
|
||||
'interactive.subscription': Symbols.subscriptions,
|
||||
'interactive.feedback': Symbols.add_reaction,
|
||||
'messaging.callStart': Symbols.call_received,
|
||||
};
|
||||
|
||||
Future<void> _fetchNotifications() async {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) return;
|
||||
@ -51,6 +55,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final nty = context.read<NotificationProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/notifications?take=10');
|
||||
_totalCount = resp.data['count'];
|
||||
_notifications.addAll(
|
||||
@ -59,6 +64,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
.cast<SnNotification>() ??
|
||||
[],
|
||||
);
|
||||
nty.updateTray();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -85,9 +91,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final nty = context.read<NotificationProvider>();
|
||||
final resp = await sn.client.put('/cgi/id/notifications/read/all');
|
||||
_notifications.clear();
|
||||
_fetchNotifications();
|
||||
nty.clear();
|
||||
|
||||
if (!mounted) return;
|
||||
context.showSnackbar(
|
||||
|
@ -1,11 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
@ -20,13 +26,15 @@ import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class PostEditorExtraProps {
|
||||
import '../../types/attachment.dart';
|
||||
|
||||
class PostEditorExtra {
|
||||
final String? text;
|
||||
final String? title;
|
||||
final String? description;
|
||||
final List<PostWriteMedia>? attachments;
|
||||
|
||||
const PostEditorExtraProps({
|
||||
const PostEditorExtra({
|
||||
this.text,
|
||||
this.title,
|
||||
this.description,
|
||||
@ -39,7 +47,7 @@ class PostEditorScreen extends StatefulWidget {
|
||||
final int? postEditId;
|
||||
final int? postReplyId;
|
||||
final int? postRepostId;
|
||||
final PostEditorExtraProps? extraProps;
|
||||
final PostEditorExtra? extraProps;
|
||||
|
||||
const PostEditorScreen({
|
||||
super.key,
|
||||
@ -94,15 +102,39 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
final HotKey _pasteHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.keyV,
|
||||
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
|
||||
void _registerHotKey() {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||
hotKeyManager.register(_pasteHotKey, keyDownHandler: (_) async {
|
||||
final imageBytes = await Pasteboard.image;
|
||||
if (imageBytes == null) return;
|
||||
_writeController.addAttachments([
|
||||
PostWriteMedia.fromBytes(
|
||||
imageBytes,
|
||||
'attachmentPastedImage'.tr(),
|
||||
SnMediaType.image,
|
||||
),
|
||||
]);
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_writeController.dispose();
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registerHotKey();
|
||||
if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) {
|
||||
context.showErrorDialog('Unknown post type');
|
||||
Navigator.pop(context);
|
||||
@ -153,6 +185,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
),
|
||||
]),
|
||||
maxLines: 2,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
@ -250,121 +283,130 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
// Replying Notice
|
||||
if (_writeController.replyingPost != null)
|
||||
Column(
|
||||
children: [
|
||||
ExpansionTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.reply).padding(left: 4),
|
||||
title: Text('postReplyingNotice')
|
||||
.fontSize(15)
|
||||
.tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
|
||||
children: <Widget>[PostItem(data: _writeController.replyingPost!)],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
// Reposting Notice
|
||||
if (_writeController.repostingPost != null)
|
||||
Column(
|
||||
children: [
|
||||
ExpansionTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.forward).padding(left: 4),
|
||||
title: Text('postRepostingNotice')
|
||||
.fontSize(15)
|
||||
.tr(args: ['@${_writeController.repostingPost!.publisher.name}']),
|
||||
children: <Widget>[
|
||||
PostItem(
|
||||
data: _writeController.repostingPost!,
|
||||
)
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 160),
|
||||
child: Column(
|
||||
children: [
|
||||
// Replying Notice
|
||||
if (_writeController.replyingPost != null)
|
||||
Column(
|
||||
children: [
|
||||
ExpansionTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.reply).padding(left: 4),
|
||||
title: Text('postReplyingNotice')
|
||||
.fontSize(15)
|
||||
.tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
|
||||
children: <Widget>[PostItem(data: _writeController.replyingPost!)],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
// Editing Notice
|
||||
if (_writeController.editingPost != null)
|
||||
Column(
|
||||
children: [
|
||||
ExpansionTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.edit_note).padding(left: 4),
|
||||
title: Text('postEditingNotice')
|
||||
.fontSize(15)
|
||||
.tr(args: ['@${_writeController.editingPost!.publisher.name}']),
|
||||
children: <Widget>[PostItem(data: _writeController.editingPost!)],
|
||||
// Reposting Notice
|
||||
if (_writeController.repostingPost != null)
|
||||
Column(
|
||||
children: [
|
||||
ExpansionTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.forward).padding(left: 4),
|
||||
title: Text('postRepostingNotice')
|
||||
.fontSize(15)
|
||||
.tr(args: ['@${_writeController.repostingPost!.publisher.name}']),
|
||||
children: <Widget>[
|
||||
PostItem(
|
||||
data: _writeController.repostingPost!,
|
||||
)
|
||||
],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
// Content Input Area
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: TextField(
|
||||
controller: _writeController.contentController,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'fieldPostContent'.tr(),
|
||||
hintStyle: TextStyle(fontSize: 14),
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
// Editing Notice
|
||||
if (_writeController.editingPost != null)
|
||||
Column(
|
||||
children: [
|
||||
ExpansionTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.edit_note).padding(left: 4),
|
||||
title: Text('postEditingNotice')
|
||||
.fontSize(15)
|
||||
.tr(args: ['@${_writeController.editingPost!.publisher.name}']),
|
||||
children: <Widget>[PostItem(data: _writeController.editingPost!)],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
// Content Input Area
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: TextField(
|
||||
controller: _writeController.contentController,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'fieldPostContent'.tr(),
|
||||
hintStyle: TextStyle(fontSize: 14),
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
]
|
||||
.expandIndexed(
|
||||
(idx, ele) => [
|
||||
if (idx != 0 || _writeController.isRelatedNull) const Gap(8),
|
||||
ele,
|
||||
],
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
]
|
||||
.expandIndexed(
|
||||
(idx, ele) => [
|
||||
if (idx != 0 || _writeController.isRelatedNull) const Gap(8),
|
||||
ele,
|
||||
],
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: PostMediaPendingList(
|
||||
thumbnail: _writeController.thumbnail,
|
||||
attachments: _writeController.attachments,
|
||||
isBusy: _writeController.isBusy,
|
||||
onUpload: (int idx) async {
|
||||
await _writeController.uploadSingleAttachment(context, idx);
|
||||
},
|
||||
onPostSetThumbnail: (int? idx) {
|
||||
_writeController.setThumbnail(idx);
|
||||
},
|
||||
onInsertLink: (int idx) async {
|
||||
_writeController.contentController.text +=
|
||||
'\n';
|
||||
},
|
||||
onUpdate: (int idx, PostWriteMedia updatedMedia) async {
|
||||
_writeController.setIsBusy(true);
|
||||
try {
|
||||
_writeController.setAttachmentAt(idx, updatedMedia);
|
||||
} finally {
|
||||
_writeController.setIsBusy(false);
|
||||
}
|
||||
},
|
||||
onRemove: (int idx) async {
|
||||
_writeController.setIsBusy(true);
|
||||
try {
|
||||
_writeController.removeAttachmentAt(idx);
|
||||
} finally {
|
||||
_writeController.setIsBusy(false);
|
||||
}
|
||||
},
|
||||
onUpdateBusy: (state) => _writeController.setIsBusy(state),
|
||||
).padding(bottom: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
|
||||
PostMediaPendingList(
|
||||
thumbnail: _writeController.thumbnail,
|
||||
attachments: _writeController.attachments,
|
||||
isBusy: _writeController.isBusy,
|
||||
onUpload: (int idx) async {
|
||||
await _writeController.uploadSingleAttachment(context, idx);
|
||||
},
|
||||
onPostSetThumbnail: (int? idx) {
|
||||
_writeController.setThumbnail(idx);
|
||||
},
|
||||
onInsertLink: (int idx) async {
|
||||
_writeController.contentController.text +=
|
||||
'\n';
|
||||
},
|
||||
onUpdate: (int idx, PostWriteMedia updatedMedia) async {
|
||||
_writeController.setIsBusy(true);
|
||||
try {
|
||||
_writeController.setAttachmentAt(idx, updatedMedia);
|
||||
} finally {
|
||||
_writeController.setIsBusy(false);
|
||||
}
|
||||
},
|
||||
onRemove: (int idx) async {
|
||||
_writeController.setIsBusy(true);
|
||||
try {
|
||||
_writeController.removeAttachmentAt(idx);
|
||||
} finally {
|
||||
_writeController.setIsBusy(false);
|
||||
}
|
||||
},
|
||||
onUpdateBusy: (state) => _writeController.setIsBusy(state),
|
||||
).padding(bottom: 8),
|
||||
Material(
|
||||
elevation: 2,
|
||||
child: Column(
|
||||
|
@ -8,9 +8,11 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/account_select.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
@ -229,13 +231,35 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showMemberAdd() {
|
||||
showModalBottomSheet(
|
||||
Future<void> _addMember(SnAccount related) async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post(
|
||||
'/cgi/id/realms/${widget.realm!.alias}/members',
|
||||
data: {'related': related.name},
|
||||
);
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('realmMemberAdded'.tr());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _showMemberAdd() async {
|
||||
final user = await showModalBottomSheet<SnAccount?>(
|
||||
context: context,
|
||||
builder: (context) => _NewRealmMemberWidget(
|
||||
realm: widget.realm!,
|
||||
builder: (context) => AccountSelect(
|
||||
title: 'realmMemberAdd'.tr(),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (user == null) return;
|
||||
_addMember(user);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -293,85 +317,6 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _NewRealmMemberWidget extends StatefulWidget {
|
||||
final SnRealm realm;
|
||||
|
||||
const _NewRealmMemberWidget({required this.realm});
|
||||
|
||||
@override
|
||||
State<_NewRealmMemberWidget> createState() => _NewRealmMemberWidgetState();
|
||||
}
|
||||
|
||||
class _NewRealmMemberWidgetState extends State<_NewRealmMemberWidget> {
|
||||
bool _isBusy = false;
|
||||
|
||||
final TextEditingController _relatedController = TextEditingController();
|
||||
|
||||
Future<void> _performAction() async {
|
||||
if (_relatedController.text.isEmpty) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post(
|
||||
'/cgi/id/realms/${widget.realm.alias}/members',
|
||||
data: {
|
||||
'related': _relatedController.text,
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
context.showSnackbar('channelMemberAdded'.tr());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_relatedController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StyledWidget(Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'realmMemberAdd',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr(),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
controller: _relatedController,
|
||||
readOnly: _isBusy,
|
||||
autocorrect: false,
|
||||
autofocus: true,
|
||||
textCapitalization: TextCapitalization.none,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldMemberRelatedName'.tr(),
|
||||
suffix: SizedBox(
|
||||
height: 24,
|
||||
child: IconButton(
|
||||
onPressed: _isBusy ? null : () => _performAction(),
|
||||
icon: Icon(Symbols.send),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
)
|
||||
],
|
||||
)).padding(all: 24);
|
||||
}
|
||||
}
|
||||
|
||||
class _RealmSettingsWidget extends StatefulWidget {
|
||||
final SnRealm? realm;
|
||||
final Function() onUpdate;
|
||||
|
@ -82,6 +82,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('settingsAppearance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
ListTile(
|
||||
title: Text('settingsDisplayLanguage').tr(),
|
||||
subtitle: Text('settingsDisplayLanguageDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.translate),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<Locale?>(
|
||||
isExpanded: true,
|
||||
items: [
|
||||
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
|
||||
return DropdownMenuItem<Locale?>(
|
||||
value: ele,
|
||||
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
|
||||
);
|
||||
}),
|
||||
DropdownMenuItem<Locale?>(
|
||||
value: null,
|
||||
child: Text('settingsDisplayLanguageSystem').tr().fontSize(14),
|
||||
),
|
||||
],
|
||||
value: EasyLocalization.of(context)!.currentLocale,
|
||||
onChanged: (Locale? value) {
|
||||
if (value != null) {
|
||||
EasyLocalization.of(context)!.setLocale(value);
|
||||
} else {
|
||||
EasyLocalization.of(context)!.resetLocale();
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 5,
|
||||
),
|
||||
height: 40,
|
||||
width: 160,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb)
|
||||
ListTile(
|
||||
title: Text('settingsBackgroundImage').tr(),
|
||||
@ -147,30 +189,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value);
|
||||
final color = await showDialog<Color?>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
pickerColor: pickerColor,
|
||||
onColorChanged: (color) => pickerColor = color,
|
||||
enableAlpha: false,
|
||||
hexInputBar: true,
|
||||
builder: (context) =>
|
||||
AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
pickerColor: pickerColor,
|
||||
onColorChanged: (color) => pickerColor = color,
|
||||
enableAlpha: false,
|
||||
hexInputBar: true,
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('dialogDismiss').tr(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('dialogConfirm').tr(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(pickerColor);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('dialogDismiss').tr(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('dialogConfirm').tr(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(pickerColor);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (color == null || !context.mounted) return;
|
||||
@ -206,11 +249,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
value: _prefs.getInt(kAppColorSchemeStoreKey) == null
|
||||
? 1
|
||||
: kColorSchemes.values
|
||||
.toList()
|
||||
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
|
||||
.toList()
|
||||
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
|
||||
onChanged: (int? value) {
|
||||
if (value != null && value != -1) {
|
||||
_prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values.elementAt(value).value);
|
||||
_prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values
|
||||
.elementAt(value)
|
||||
.value);
|
||||
final th = context.read<ThemeProvider>();
|
||||
th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value));
|
||||
setState(() {});
|
||||
@ -342,7 +387,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
('Custom', _serverUrlController.text),
|
||||
]
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
(item) =>
|
||||
DropdownMenuItem<String>(
|
||||
value: item.$2,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
@ -354,7 +400,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
.toList(),
|
||||
value: _serverUrlController.text,
|
||||
onChanged: (String? value) {
|
||||
@ -409,11 +455,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
isExpanded: true,
|
||||
items: kImageQualityLevel.entries
|
||||
.map(
|
||||
(item) => DropdownMenuItem<FilterQuality>(
|
||||
(item) =>
|
||||
DropdownMenuItem<FilterQuality>(
|
||||
value: item.value,
|
||||
child: Text(item.key).tr().fontSize(14),
|
||||
),
|
||||
)
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (FilterQuality? value) {
|
||||
if (value == null) return;
|
||||
|
@ -8,9 +8,20 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/chat/room.dart';
|
||||
import 'package:surface/screens/post/post_editor.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
|
||||
class AppSharingListener extends StatefulWidget {
|
||||
final Widget child;
|
||||
@ -51,20 +62,39 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
pathParameters: {
|
||||
'mode': 'stories',
|
||||
},
|
||||
extra: PostEditorExtraProps(
|
||||
extra: PostEditorExtra(
|
||||
text: value
|
||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||
.map((e) => e.path).join('\n'),
|
||||
.map((e) => e.path)
|
||||
.join('\n'),
|
||||
attachments: value
|
||||
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(),
|
||||
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
|
||||
.contains(e.type))
|
||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
leading: Icon(Icons.chat_outlined),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('shareIntentSendChannel').tr(),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _ShareIntentChannelSelect(value: value),
|
||||
).then((val) {
|
||||
if (!context.mounted) return;
|
||||
if (val == true) Navigator.pop(context);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
).width(280),
|
||||
)
|
||||
],
|
||||
),
|
||||
@ -103,7 +133,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||
_initialize();
|
||||
_initialHandle();
|
||||
}
|
||||
@ -120,3 +150,193 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
class _ShareIntentChannelSelect extends StatefulWidget {
|
||||
final Iterable<SharedMediaFile> value;
|
||||
|
||||
const _ShareIntentChannelSelect({required this.value});
|
||||
|
||||
@override
|
||||
State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState();
|
||||
}
|
||||
|
||||
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||
bool _isBusy = true;
|
||||
|
||||
List<SnChannel>? _channels;
|
||||
Map<int, SnChatMessage>? _lastMessages;
|
||||
|
||||
void _refreshChannels() {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) {
|
||||
setState(() => _isBusy = false);
|
||||
return;
|
||||
}
|
||||
|
||||
final chan = context.read<ChatChannelProvider>();
|
||||
chan.fetchChannels().listen((channels) async {
|
||||
final lastMessages = await chan.getLastMessages(channels);
|
||||
_lastMessages = {for (final val in lastMessages) val.channelId: val};
|
||||
channels.sort((a, b) {
|
||||
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) {
|
||||
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt);
|
||||
}
|
||||
if (_lastMessages!.containsKey(a.id)) return -1;
|
||||
if (_lastMessages!.containsKey(b.id)) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (!mounted) return;
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
for (final channel in channels) {
|
||||
if (channel.type == 1) {
|
||||
await ud.listAccount(
|
||||
channel.members
|
||||
?.cast<SnChannelMember?>()
|
||||
.map((ele) => ele?.accountId)
|
||||
.where((ele) => ele != null)
|
||||
.toSet() ??
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) setState(() => _channels = channels);
|
||||
})
|
||||
..onError((err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
setState(() => _isBusy = false);
|
||||
})
|
||||
..onDone(() {
|
||||
if (!mounted) return;
|
||||
setState(() => _isBusy = false);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refreshChannels();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.read<UserProvider>();
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.chat, size: 24),
|
||||
const Gap(16),
|
||||
Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||
child: ListView.builder(
|
||||
itemCount: _channels?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final channel = _channels![idx];
|
||||
final lastMessage = _lastMessages?[channel.id];
|
||||
|
||||
if (channel.type == 1) {
|
||||
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||
(ele) => ele?.accountId != ua.user?.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text(
|
||||
'channelDirectMessageDescription'.tr(args: [
|
||||
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
||||
]),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {
|
||||
'scope': channel.realm?.alias ?? 'global',
|
||||
'alias': channel.alias,
|
||||
},
|
||||
).then((value) {
|
||||
if (mounted) _refreshChannels();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
title: Text(channel.name),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text(
|
||||
channel.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: null,
|
||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, true);
|
||||
GoRouter.of(context)
|
||||
.pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {
|
||||
'scope': channel.realm?.alias ?? 'global',
|
||||
'alias': channel.alias,
|
||||
},
|
||||
extra: ChatRoomScreenExtra(
|
||||
initialText: widget.value
|
||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||
.map((e) => e.path)
|
||||
.join('\n'),
|
||||
initialAttachments: widget.value
|
||||
.where((e) =>
|
||||
[SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
.then((value) {
|
||||
if (value == true) _refreshChannels();
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
279
lib/screens/wallet.dart
Normal file
279
lib/screens/wallet.dart
Normal file
@ -0,0 +1,279 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/wallet.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class WalletScreen extends StatefulWidget {
|
||||
const WalletScreen({super.key});
|
||||
|
||||
@override
|
||||
State<WalletScreen> createState() => _WalletScreenState();
|
||||
}
|
||||
|
||||
class _WalletScreenState extends State<WalletScreen> {
|
||||
bool _isBusy = false;
|
||||
SnWallet? _wallet;
|
||||
|
||||
Future<void> _fetchWallet() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/wa/wallets/me');
|
||||
_wallet = SnWallet.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchWallet();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountWallet').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (_wallet == null)
|
||||
Expanded(
|
||||
child: _CreateWalletWidget(
|
||||
onCreate: () {
|
||||
_fetchWallet();
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
child: Icon(Symbols.wallet, size: 28),
|
||||
),
|
||||
const Gap(12),
|
||||
SizedBox(width: double.infinity),
|
||||
Text(
|
||||
NumberFormat.compactCurrency(
|
||||
locale: EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
symbol: '${'walletCurrencyShort'.tr()} ',
|
||||
decimalDigits: 2,
|
||||
).format(double.parse(_wallet!.balance)),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text('walletCurrency'.plural(double.parse(_wallet!.balance))),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 24),
|
||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||
if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletTransactionList extends StatefulWidget {
|
||||
final SnWallet myself;
|
||||
|
||||
const _WalletTransactionList({required this.myself});
|
||||
|
||||
@override
|
||||
State<_WalletTransactionList> createState() => _WalletTransactionListState();
|
||||
}
|
||||
|
||||
class _WalletTransactionListState extends State<_WalletTransactionList> {
|
||||
bool _isBusy = false;
|
||||
int? _totalCount;
|
||||
final List<SnTransaction> _transactions = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchTransactions() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/wa/transactions/me', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _transactions.length,
|
||||
});
|
||||
_totalCount = resp.data['count'];
|
||||
_transactions.addAll(
|
||||
resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchTransactions();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchTransactions,
|
||||
child: InfiniteList(
|
||||
itemCount: _transactions.length,
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!,
|
||||
onFetchData: () {
|
||||
_fetchTransactions();
|
||||
},
|
||||
itemBuilder: (context, idx) {
|
||||
final ele = _transactions[idx];
|
||||
final isIncoming = ele.payeeId == widget.myself.id;
|
||||
return ListTile(
|
||||
leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made),
|
||||
title: Text(
|
||||
'${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
|
||||
style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(ele.remark),
|
||||
const Gap(2),
|
||||
Text(
|
||||
DateFormat(
|
||||
null,
|
||||
EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
).format(ele.createdAt),
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateWalletWidget extends StatefulWidget {
|
||||
final Function()? onCreate;
|
||||
|
||||
const _CreateWalletWidget({required this.onCreate});
|
||||
|
||||
@override
|
||||
State<_CreateWalletWidget> createState() => _CreateWalletWidgetState();
|
||||
}
|
||||
|
||||
class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _createWallet() async {
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
final password = await showDialog<String?>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('walletCreate').tr(),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('walletCreatePassword').tr(),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
controller: passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPassword'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text('cancel').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop(passwordController.text);
|
||||
},
|
||||
child: Text('next').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
passwordController.dispose();
|
||||
});
|
||||
if (password == null || password.isEmpty) return;
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/wa/wallets/me', data: {
|
||||
'password': password,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
child: Icon(Symbols.add, size: 28),
|
||||
),
|
||||
const Gap(12),
|
||||
Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(),
|
||||
const Gap(8),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: _isBusy ? null : () => _createWallet(),
|
||||
child: Text('next').tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ Future<ThemeData> createAppTheme(
|
||||
brightness: brightness,
|
||||
);
|
||||
|
||||
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||
final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||
|
||||
return ThemeData(
|
||||
@ -51,9 +51,9 @@ Future<ThemeData> createAppTheme(
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: hasAppBarBlurry ? 0 : null,
|
||||
backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
|
||||
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||
elevation: hasAppBarTransparent ? 0 : null,
|
||||
backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
|
||||
foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||
),
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
|
@ -15,12 +15,13 @@ class SnAccount with _$SnAccount {
|
||||
required DateTime? deletedAt,
|
||||
required DateTime? confirmedAt,
|
||||
required List<SnAccountContact>? contacts,
|
||||
required String avatar,
|
||||
required String banner,
|
||||
@Default("") String avatar,
|
||||
@Default("") String banner,
|
||||
required String description,
|
||||
required String name,
|
||||
required String nick,
|
||||
required Map<String, dynamic> permNodes,
|
||||
required String language,
|
||||
required SnAccountProfile? profile,
|
||||
@Default([]) List<SnAccountBadge> badges,
|
||||
required DateTime? suspendedAt,
|
||||
|
@ -33,6 +33,7 @@ mixin _$SnAccount {
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
String get nick => throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> get permNodes => throw _privateConstructorUsedError;
|
||||
String get language => throw _privateConstructorUsedError;
|
||||
SnAccountProfile? get profile => throw _privateConstructorUsedError;
|
||||
List<SnAccountBadge> get badges => throw _privateConstructorUsedError;
|
||||
DateTime? get suspendedAt => throw _privateConstructorUsedError;
|
||||
@ -69,6 +70,7 @@ abstract class $SnAccountCopyWith<$Res> {
|
||||
String name,
|
||||
String nick,
|
||||
Map<String, dynamic> permNodes,
|
||||
String language,
|
||||
SnAccountProfile? profile,
|
||||
List<SnAccountBadge> badges,
|
||||
DateTime? suspendedAt,
|
||||
@ -107,6 +109,7 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
|
||||
Object? name = null,
|
||||
Object? nick = null,
|
||||
Object? permNodes = null,
|
||||
Object? language = null,
|
||||
Object? profile = freezed,
|
||||
Object? badges = null,
|
||||
Object? suspendedAt = freezed,
|
||||
@ -164,6 +167,10 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
|
||||
? _value.permNodes
|
||||
: permNodes // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
language: null == language
|
||||
? _value.language
|
||||
: language // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
profile: freezed == profile
|
||||
? _value.profile
|
||||
: profile // ignore: cast_nullable_to_non_nullable
|
||||
@ -231,6 +238,7 @@ abstract class _$$SnAccountImplCopyWith<$Res>
|
||||
String name,
|
||||
String nick,
|
||||
Map<String, dynamic> permNodes,
|
||||
String language,
|
||||
SnAccountProfile? profile,
|
||||
List<SnAccountBadge> badges,
|
||||
DateTime? suspendedAt,
|
||||
@ -268,6 +276,7 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
||||
Object? name = null,
|
||||
Object? nick = null,
|
||||
Object? permNodes = null,
|
||||
Object? language = null,
|
||||
Object? profile = freezed,
|
||||
Object? badges = null,
|
||||
Object? suspendedAt = freezed,
|
||||
@ -325,6 +334,10 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
||||
? _value._permNodes
|
||||
: permNodes // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
language: null == language
|
||||
? _value.language
|
||||
: language // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
profile: freezed == profile
|
||||
? _value.profile
|
||||
: profile // ignore: cast_nullable_to_non_nullable
|
||||
@ -367,12 +380,13 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
required this.deletedAt,
|
||||
required this.confirmedAt,
|
||||
required final List<SnAccountContact>? contacts,
|
||||
required this.avatar,
|
||||
required this.banner,
|
||||
this.avatar = "",
|
||||
this.banner = "",
|
||||
required this.description,
|
||||
required this.name,
|
||||
required this.nick,
|
||||
required final Map<String, dynamic> permNodes,
|
||||
required this.language,
|
||||
required this.profile,
|
||||
final List<SnAccountBadge> badges = const [],
|
||||
required this.suspendedAt,
|
||||
@ -410,8 +424,10 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final String avatar;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String banner;
|
||||
@override
|
||||
final String description;
|
||||
@ -427,6 +443,8 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
return EqualUnmodifiableMapView(_permNodes);
|
||||
}
|
||||
|
||||
@override
|
||||
final String language;
|
||||
@override
|
||||
final SnAccountProfile? profile;
|
||||
final List<SnAccountBadge> _badges;
|
||||
@ -451,7 +469,7 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -477,6 +495,8 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
(identical(other.nick, nick) || other.nick == nick) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._permNodes, _permNodes) &&
|
||||
(identical(other.language, language) ||
|
||||
other.language == language) &&
|
||||
(identical(other.profile, profile) || other.profile == profile) &&
|
||||
const DeepCollectionEquality().equals(other._badges, _badges) &&
|
||||
(identical(other.suspendedAt, suspendedAt) ||
|
||||
@ -507,6 +527,7 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
name,
|
||||
nick,
|
||||
const DeepCollectionEquality().hash(_permNodes),
|
||||
language,
|
||||
profile,
|
||||
const DeepCollectionEquality().hash(_badges),
|
||||
suspendedAt,
|
||||
@ -540,12 +561,13 @@ abstract class _SnAccount extends SnAccount {
|
||||
required final DateTime? deletedAt,
|
||||
required final DateTime? confirmedAt,
|
||||
required final List<SnAccountContact>? contacts,
|
||||
required final String avatar,
|
||||
required final String banner,
|
||||
final String avatar,
|
||||
final String banner,
|
||||
required final String description,
|
||||
required final String name,
|
||||
required final String nick,
|
||||
required final Map<String, dynamic> permNodes,
|
||||
required final String language,
|
||||
required final SnAccountProfile? profile,
|
||||
final List<SnAccountBadge> badges,
|
||||
required final DateTime? suspendedAt,
|
||||
@ -584,6 +606,8 @@ abstract class _SnAccount extends SnAccount {
|
||||
@override
|
||||
Map<String, dynamic> get permNodes;
|
||||
@override
|
||||
String get language;
|
||||
@override
|
||||
SnAccountProfile? get profile;
|
||||
@override
|
||||
List<SnAccountBadge> get badges;
|
||||
|
@ -20,12 +20,13 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
|
||||
contacts: (json['contacts'] as List<dynamic>?)
|
||||
?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
avatar: json['avatar'] as String,
|
||||
banner: json['banner'] as String,
|
||||
avatar: json['avatar'] as String? ?? "",
|
||||
banner: json['banner'] as String? ?? "",
|
||||
description: json['description'] as String,
|
||||
name: json['name'] as String,
|
||||
nick: json['nick'] as String,
|
||||
permNodes: json['perm_nodes'] as Map<String, dynamic>,
|
||||
language: json['language'] as String,
|
||||
profile: json['profile'] == null
|
||||
? null
|
||||
: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
|
||||
@ -56,6 +57,7 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
|
||||
'name': instance.name,
|
||||
'nick': instance.nick,
|
||||
'perm_nodes': instance.permNodes,
|
||||
'language': instance.language,
|
||||
'profile': instance.profile?.toJson(),
|
||||
'badges': instance.badges.map((e) => e.toJson()).toList(),
|
||||
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
||||
|
@ -16,6 +16,7 @@ class SnCheckInRecord with _$SnCheckInRecord {
|
||||
required DateTime? deletedAt,
|
||||
required int resultTier,
|
||||
required int resultExperience,
|
||||
required double resultCoin,
|
||||
required List<int> resultModifiers,
|
||||
required int accountId,
|
||||
}) = _SnCheckInRecord;
|
||||
|
@ -26,6 +26,7 @@ mixin _$SnCheckInRecord {
|
||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||
int get resultTier => throw _privateConstructorUsedError;
|
||||
int get resultExperience => throw _privateConstructorUsedError;
|
||||
double get resultCoin => throw _privateConstructorUsedError;
|
||||
List<int> get resultModifiers => throw _privateConstructorUsedError;
|
||||
int get accountId => throw _privateConstructorUsedError;
|
||||
|
||||
@ -52,6 +53,7 @@ abstract class $SnCheckInRecordCopyWith<$Res> {
|
||||
DateTime? deletedAt,
|
||||
int resultTier,
|
||||
int resultExperience,
|
||||
double resultCoin,
|
||||
List<int> resultModifiers,
|
||||
int accountId});
|
||||
}
|
||||
@ -77,6 +79,7 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
||||
Object? deletedAt = freezed,
|
||||
Object? resultTier = null,
|
||||
Object? resultExperience = null,
|
||||
Object? resultCoin = null,
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
@ -105,6 +108,10 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
||||
? _value.resultExperience
|
||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultCoin: null == resultCoin
|
||||
? _value.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _value.resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
@ -132,6 +139,7 @@ abstract class _$$SnCheckInRecordImplCopyWith<$Res>
|
||||
DateTime? deletedAt,
|
||||
int resultTier,
|
||||
int resultExperience,
|
||||
double resultCoin,
|
||||
List<int> resultModifiers,
|
||||
int accountId});
|
||||
}
|
||||
@ -155,6 +163,7 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
||||
Object? deletedAt = freezed,
|
||||
Object? resultTier = null,
|
||||
Object? resultExperience = null,
|
||||
Object? resultCoin = null,
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
@ -183,6 +192,10 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
||||
? _value.resultExperience
|
||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultCoin: null == resultCoin
|
||||
? _value.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _value._resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
@ -205,6 +218,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
required this.deletedAt,
|
||||
required this.resultTier,
|
||||
required this.resultExperience,
|
||||
required this.resultCoin,
|
||||
required final List<int> resultModifiers,
|
||||
required this.accountId})
|
||||
: _resultModifiers = resultModifiers,
|
||||
@ -225,6 +239,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
final int resultTier;
|
||||
@override
|
||||
final int resultExperience;
|
||||
@override
|
||||
final double resultCoin;
|
||||
final List<int> _resultModifiers;
|
||||
@override
|
||||
List<int> get resultModifiers {
|
||||
@ -238,7 +254,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -257,6 +273,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
other.resultTier == resultTier) &&
|
||||
(identical(other.resultExperience, resultExperience) ||
|
||||
other.resultExperience == resultExperience) &&
|
||||
(identical(other.resultCoin, resultCoin) ||
|
||||
other.resultCoin == resultCoin) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._resultModifiers, _resultModifiers) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
@ -273,6 +291,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
||||
deletedAt,
|
||||
resultTier,
|
||||
resultExperience,
|
||||
resultCoin,
|
||||
const DeepCollectionEquality().hash(_resultModifiers),
|
||||
accountId);
|
||||
|
||||
@ -301,6 +320,7 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
|
||||
required final DateTime? deletedAt,
|
||||
required final int resultTier,
|
||||
required final int resultExperience,
|
||||
required final double resultCoin,
|
||||
required final List<int> resultModifiers,
|
||||
required final int accountId}) = _$SnCheckInRecordImpl;
|
||||
const _SnCheckInRecord._() : super._();
|
||||
@ -321,6 +341,8 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
|
||||
@override
|
||||
int get resultExperience;
|
||||
@override
|
||||
double get resultCoin;
|
||||
@override
|
||||
List<int> get resultModifiers;
|
||||
@override
|
||||
int get accountId;
|
||||
|
@ -17,6 +17,7 @@ _$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson(
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
resultTier: (json['result_tier'] as num).toInt(),
|
||||
resultExperience: (json['result_experience'] as num).toInt(),
|
||||
resultCoin: (json['result_coin'] as num).toDouble(),
|
||||
resultModifiers: (json['result_modifiers'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
@ -32,6 +33,7 @@ Map<String, dynamic> _$$SnCheckInRecordImplToJson(
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'result_tier': instance.resultTier,
|
||||
'result_experience': instance.resultExperience,
|
||||
'result_coin': instance.resultCoin,
|
||||
'result_modifiers': instance.resultModifiers,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
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,
|
||||
};
|
@ -196,68 +196,71 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||
child: ScrollConfiguration(
|
||||
behavior: _AttachmentListScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
padding: widget.padding,
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.data.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
|
||||
child: AspectRatio(
|
||||
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
return AspectRatio(
|
||||
aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||
child: ScrollConfiguration(
|
||||
behavior: _AttachmentListScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
padding: widget.padding,
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.data.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
|
||||
child: AspectRatio(
|
||||
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AttachmentItem(
|
||||
data: widget.data[idx],
|
||||
heroTag: heroTags[idx],
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AttachmentItem(
|
||||
data: widget.data[idx],
|
||||
heroTag: heroTags[idx],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(
|
||||
label: Text('${idx + 1}/${widget.data.length}'),
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(
|
||||
label: Text('${idx + 1}/${widget.data.length}'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -152,6 +152,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Stack(
|
||||
children: [
|
||||
Builder(builder: (context) {
|
||||
|
@ -1,17 +1,28 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' show min;
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/chat_message_controller.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_sticker.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class ChatMessageInput extends StatefulWidget {
|
||||
final ChatMessageController controller;
|
||||
@ -32,9 +43,30 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
final TextEditingController _contentController = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
final HotKey _pasteHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.keyV,
|
||||
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
|
||||
void _registerHotKey() {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||
hotKeyManager.register(_pasteHotKey, keyDownHandler: (_) async {
|
||||
final imageBytes = await Pasteboard.image;
|
||||
if (imageBytes == null) return;
|
||||
_attachments.add(PostWriteMedia.fromBytes(
|
||||
imageBytes,
|
||||
'attachmentPastedImage'.tr(),
|
||||
SnMediaType.image,
|
||||
));
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registerHotKey();
|
||||
_contentController.addListener(() {
|
||||
if (_contentController.text.isNotEmpty) {
|
||||
widget.controller.pingTypingStatus();
|
||||
@ -46,6 +78,16 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
setState(() => _replyingMessage = value);
|
||||
}
|
||||
|
||||
void setInitialText(String? value) {
|
||||
_contentController.text = value ?? '';
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setInitialAttachments(List<PostWriteMedia>? value) {
|
||||
_attachments.addAll(value ?? []);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setEdit(SnChatMessage? value) {
|
||||
_contentController.text = value?.body['text'] ?? '';
|
||||
_attachments.clear();
|
||||
@ -134,10 +176,34 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
|
||||
final List<PostWriteMedia> _attachments = List.empty(growable: true);
|
||||
|
||||
OverlayEntry? _overlayEntry;
|
||||
|
||||
void _showEmojiPicker(BuildContext context) {
|
||||
final overlay = Overlay.of(context);
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||
right: 16,
|
||||
child: _StickerPicker(
|
||||
originalText: _contentController.text,
|
||||
onDismiss: () => _dismissEmojiPicker(),
|
||||
onInsert: (str) => _contentController.text = str,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
overlay.insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
void _dismissEmojiPicker() {
|
||||
_overlayEntry?.remove();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contentController.dispose();
|
||||
_focusNode.dispose();
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -279,6 +345,19 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Symbols.mood,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
onPressed: () {
|
||||
_showEmojiPicker(context);
|
||||
},
|
||||
),
|
||||
AddPostMediaButton(
|
||||
onAdd: (items) {
|
||||
setState(() {
|
||||
@ -304,3 +383,107 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StickerPicker extends StatelessWidget {
|
||||
final String originalText;
|
||||
final Function? onDismiss;
|
||||
final Function(String)? onInsert;
|
||||
|
||||
const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sticker = context.read<SnStickerProvider>();
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
onDismiss?.call();
|
||||
},
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: min(360, MediaQuery.of(context).size.width), maxHeight: 240),
|
||||
child: Material(
|
||||
elevation: 8,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: sticker.stickersByPack.entries
|
||||
.map((e) {
|
||||
return <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 8),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(e.value.first.pack.name).bold(),
|
||||
Text(e.value.first.pack.description),
|
||||
],
|
||||
),
|
||||
),
|
||||
GridView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 48,
|
||||
childAspectRatio: 1.0,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemCount: e.value.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final element = e.value[index];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final withSpace = originalText.isNotEmpty;
|
||||
onInsert?.call(
|
||||
'$originalText${withSpace ? ' ' : ''}:${element.pack.prefix}${element.alias}:');
|
||||
onDismiss?.call();
|
||||
},
|
||||
child: Tooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ':${element.pack.prefix}${element.alias}:\n',
|
||||
style: GoogleFonts.robotoMono()),
|
||||
TextSpan(text: element.name).bold(),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: UniversalImage(
|
||||
sn.getAttachmentUrl(element.attachment.rid),
|
||||
width: 48,
|
||||
height: 48,
|
||||
cacheHeight: 48,
|
||||
cacheWidth: 48,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
})
|
||||
.expand((ele) => ele)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
|
||||
@ -13,6 +14,9 @@ class ConnectionIndicator extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ws = context.watch<WebSocketProvider>();
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
|
||||
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: ws,
|
||||
@ -22,45 +26,50 @@ class ConnectionIndicator extends StatelessWidget {
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: !show,
|
||||
child: GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (ws.isBusy)
|
||||
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else if (!ws.isConnected)
|
||||
Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else
|
||||
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
||||
const Gap(8),
|
||||
if (ws.isBusy)
|
||||
const CircularProgressIndicator(strokeWidth: 2.5)
|
||||
.width(12)
|
||||
.height(12)
|
||||
.padding(horizontal: 4, right: 4)
|
||||
else if (!ws.isConnected)
|
||||
const Icon(Symbols.power_off, size: 18)
|
||||
else
|
||||
const Icon(Symbols.power, size: 18),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4)
|
||||
: const SizedBox.shrink(),
|
||||
).opacity(show ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.easeInOut,
|
||||
),
|
||||
onTap: () {
|
||||
if (!ws.isConnected && !ws.isBusy) {
|
||||
ws.connect();
|
||||
}
|
||||
},
|
||||
),
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (ws.isBusy)
|
||||
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else if (!ws.isConnected)
|
||||
Text('serverDisconnected')
|
||||
.tr()
|
||||
.textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else
|
||||
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
||||
const Gap(8),
|
||||
if (ws.isBusy)
|
||||
const CircularProgressIndicator(strokeWidth: 2.5)
|
||||
.width(12)
|
||||
.height(12)
|
||||
.padding(horizontal: 4, right: 4)
|
||||
else if (!ws.isConnected)
|
||||
const Icon(Symbols.power_off, size: 18)
|
||||
else
|
||||
const Icon(Symbols.power, size: 18),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4)
|
||||
: const SizedBox.shrink(),
|
||||
).opacity(show ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.easeInOut,
|
||||
),
|
||||
onTap: () {
|
||||
if (!ws.isConnected && !ws.isBusy) {
|
||||
ws.connect();
|
||||
}
|
||||
},
|
||||
),
|
||||
).padding(left: marginLeft),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -28,7 +28,7 @@ class ContextMenuArea extends StatelessWidget {
|
||||
// Leave padding for side navigation
|
||||
mousePosition = cfg.drawerIsExpanded
|
||||
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
||||
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
|
||||
: mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
|
||||
}
|
||||
},
|
||||
child: GestureDetector(
|
||||
|
@ -2,7 +2,9 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
extension AppPromptExtension on BuildContext {
|
||||
void showSnackbar(String content, {SnackBarAction? action}) {
|
||||
@ -111,7 +113,34 @@ extension AppPromptExtension on BuildContext {
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('dialogError').tr(),
|
||||
content: content,
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 20,
|
||||
children: [
|
||||
content,
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: 'needHelp'.tr(),
|
||||
children: [
|
||||
TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: 'needHelpLaunch'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(ctx).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: Theme.of(ctx).colorScheme.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlString('https://kb.solsynth.dev/solar-network');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
@ -128,17 +157,7 @@ extension ByteFormatter on int {
|
||||
if (this == 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
final dm = decimals < 0 ? 0 : decimals;
|
||||
final sizes = [
|
||||
'Bytes',
|
||||
'KiB',
|
||||
'MiB',
|
||||
'GiB',
|
||||
'TiB',
|
||||
'PiB',
|
||||
'EiB',
|
||||
'ZiB',
|
||||
'YiB'
|
||||
];
|
||||
final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
final i = (math.log(this) / math.log(k)).floor().toInt();
|
||||
return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
final bool isAutoWarp;
|
||||
final bool isEnlargeSticker;
|
||||
final TextScaler? textScaler;
|
||||
final Color? textColor;
|
||||
final List<SnAttachment?>? attachments;
|
||||
|
||||
const MarkdownTextContent({
|
||||
@ -28,6 +29,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
this.isAutoWarp = false,
|
||||
this.isEnlargeSticker = false,
|
||||
this.textScaler,
|
||||
this.textColor,
|
||||
this.attachments,
|
||||
});
|
||||
|
||||
@ -42,6 +44,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
Theme.of(context),
|
||||
).copyWith(
|
||||
textScaler: textScaler,
|
||||
p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null,
|
||||
blockquote: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@ -126,14 +129,27 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
future: st.lookupSticker(alias),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return UniversalImage(
|
||||
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
|
||||
fit: BoxFit.cover,
|
||||
width: size,
|
||||
height: size,
|
||||
cacheHeight: size,
|
||||
cacheWidth: size,
|
||||
);
|
||||
return GestureDetector(
|
||||
child: UniversalImage(
|
||||
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
|
||||
fit: BoxFit.contain,
|
||||
width: size,
|
||||
height: size,
|
||||
cacheHeight: size,
|
||||
cacheWidth: size,
|
||||
),
|
||||
onTap: () {
|
||||
if (snapshot.data == null) return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: [snapshot.data!.attachment],
|
||||
initialIndex: 0,
|
||||
heroTags: [const Uuid().v4()],
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
@ -142,7 +158,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
);
|
||||
case 'attachments':
|
||||
final attachment = attachments?.firstWhere(
|
||||
(ele) => ele?.rid == segments[1],
|
||||
(ele) => ele?.rid == segments[1],
|
||||
orElse: () => null,
|
||||
);
|
||||
if (attachment != null) {
|
||||
|
@ -31,34 +31,37 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
|
||||
builder: (context, _) {
|
||||
final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
|
||||
|
||||
return NavigationRail(
|
||||
selectedIndex:
|
||||
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
|
||||
destinations: [
|
||||
...destinations.where((ele) => ele.isPinned).map((ele) {
|
||||
return NavigationRailDestination(
|
||||
icon: ele.icon,
|
||||
label: Text(ele.label).tr(),
|
||||
);
|
||||
}),
|
||||
],
|
||||
trailing: Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: StyledWidget(
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.menu),
|
||||
onPressed: () {
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
),
|
||||
).padding(bottom: 16),
|
||||
return SizedBox(
|
||||
width: 80,
|
||||
child: NavigationRail(
|
||||
selectedIndex:
|
||||
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
|
||||
destinations: [
|
||||
...destinations.where((ele) => ele.isPinned).map((ele) {
|
||||
return NavigationRailDestination(
|
||||
icon: ele.icon,
|
||||
label: Text(ele.label).tr(),
|
||||
);
|
||||
}),
|
||||
],
|
||||
trailing: Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: StyledWidget(
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.menu),
|
||||
onPressed: () {
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
),
|
||||
).padding(bottom: 16),
|
||||
),
|
||||
),
|
||||
onDestinationSelected: (idx) {
|
||||
nav.setIndex(idx);
|
||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||
},
|
||||
),
|
||||
onDestinationSelected: (idx) {
|
||||
nav.setIndex(idx);
|
||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -140,6 +140,7 @@ class AppRootScaffold extends StatelessWidget {
|
||||
);
|
||||
|
||||
final safeTop = MediaQuery.of(context).padding.top;
|
||||
final safeBottom = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
return Scaffold(
|
||||
key: globalRootScaffoldKey,
|
||||
@ -191,7 +192,10 @@ class AppRootScaffold extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
|
||||
Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()),
|
||||
if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE))
|
||||
Positioned(bottom: safeBottom > 0 ? safeBottom : 16, left: 0, right: 0, child: ConnectionIndicator())
|
||||
else
|
||||
Positioned(top: safeTop > 0 ? safeTop : 16, left: 0, right: 0, child: ConnectionIndicator()),
|
||||
],
|
||||
),
|
||||
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
||||
|
@ -1,60 +1,181 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/notification.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/notification.dart';
|
||||
import 'package:surface/types/notification.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class NotifyIndicator extends StatelessWidget {
|
||||
import 'markdown_content.dart';
|
||||
|
||||
class NotifyIndicator extends StatefulWidget {
|
||||
const NotifyIndicator({super.key});
|
||||
|
||||
@override
|
||||
State<NotifyIndicator> createState() => _NotifyIndicatorState();
|
||||
}
|
||||
|
||||
class _NotifyIndicatorState extends State<NotifyIndicator> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
|
||||
void _markOneAsRead(SnNotification notification) async {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) return;
|
||||
|
||||
if (notification.id == 0) return;
|
||||
if (notification.readAt != null) return;
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.put('/cgi/id/notifications/read/${notification.id}');
|
||||
|
||||
if (!mounted) return;
|
||||
context.showSnackbar(
|
||||
'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']),
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
final nty = context.watch<NotificationProvider>();
|
||||
|
||||
final show = nty.notifications.isNotEmpty && ua.isAuthorized;
|
||||
final isMobile = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
|
||||
|
||||
final show = nty.showingCount > 0 && ua.isAuthorized;
|
||||
|
||||
if (show) {
|
||||
_animationController.animateTo(1);
|
||||
} else {
|
||||
_animationController.animateTo(0);
|
||||
}
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: nty,
|
||||
builder: (context, _) {
|
||||
final current = nty.notifications.lastOrNull;
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: !show,
|
||||
child: GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
nty.notifications.lastOrNull?.title ??
|
||||
'notificationUnreadCount'.plural(nty.notifications.length),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (nty.notifications.lastOrNull?.body != null)
|
||||
Text(
|
||||
nty.notifications.lastOrNull!.body,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).padding(left: 4),
|
||||
const Gap(8),
|
||||
const Icon(Symbols.notifications_unread, size: 18),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4)
|
||||
: const SizedBox.shrink(),
|
||||
).opacity(show ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.easeInOut,
|
||||
child: Animate(
|
||||
autoPlay: false,
|
||||
controller: _animationController,
|
||||
effects: [
|
||||
SlideEffect(
|
||||
begin: isMobile ? Offset(0, -1) : Offset(1, 0),
|
||||
end: Offset(0, 0),
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
),
|
||||
FadeEffect(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
],
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
width: double.infinity,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: isMobile ? MediaQuery.of(context).size.width - 16 : 360,
|
||||
),
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (current?.metadata['avatar'] != null)
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundImage: UniversalImage.provider(
|
||||
sn.getAttachmentUrl(current!.metadata['avatar']),
|
||||
),
|
||||
)
|
||||
else
|
||||
Icon(kNotificationTopicIcons[current?.topic] ?? Symbols.notifications),
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
current?.title ?? 'Notification',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (current?.subtitle?.isNotEmpty ?? false)
|
||||
Text(
|
||||
current!.subtitle!,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
MarkdownTextContent(
|
||||
content: current?.body ?? '',
|
||||
isAutoWarp: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(DateFormat('HH:mm').format(current?.createdAt.toLocal() ?? DateTime.now()))
|
||||
.fontSize(12)
|
||||
.padding(right: 2),
|
||||
const Gap(6),
|
||||
if (current?.metadata['image'] != null)
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(current?.metadata['image']),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
nty.clear();
|
||||
if (current != null) {
|
||||
_markOneAsRead(current);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -1,10 +1,12 @@
|
||||
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';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@ -34,6 +36,7 @@ import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||
import 'package:surface/widgets/post/post_reaction.dart';
|
||||
import 'package:surface/widgets/post/publisher_popover.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
class PostItem extends StatelessWidget {
|
||||
final SnPost data;
|
||||
@ -186,6 +189,7 @@ class PostItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
|
||||
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
|
||||
_PostBottomAction(
|
||||
data: data,
|
||||
showComments: showComments,
|
||||
@ -267,6 +271,7 @@ class PostItem extends StatelessWidget {
|
||||
LinkPreviewWidget(
|
||||
text: data.body['content'],
|
||||
).padding(horizontal: 4),
|
||||
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: Column(
|
||||
@ -817,6 +822,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(
|
||||
@ -1106,6 +1127,95 @@ class _PostTruncatedHint extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PostFeaturedComment extends StatefulWidget {
|
||||
final SnPost data;
|
||||
final double? maxWidth;
|
||||
|
||||
const _PostFeaturedComment({required this.data, this.maxWidth});
|
||||
|
||||
@override
|
||||
State<_PostFeaturedComment> createState() => _PostFeaturedCommentState();
|
||||
}
|
||||
|
||||
class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
||||
SnPost? _featuredComment;
|
||||
|
||||
Future<void> _fetchComments() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
|
||||
'take': 1,
|
||||
});
|
||||
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.data.metric.replyCount > 0) {
|
||||
_fetchComments();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.data.metric.replyCount == 0) return const SizedBox.shrink();
|
||||
if (_featuredComment == null) return const SizedBox.shrink();
|
||||
|
||||
return AnimateWidgetExtensions(Container(
|
||||
constraints: BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
width: double.infinity,
|
||||
child: Material(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => PostCommentListPopup(
|
||||
postId: widget.data.id,
|
||||
commentCount: widget.data.metric.replyCount,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('postFeaturedComment', style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 16)).tr(),
|
||||
const Gap(4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 12,
|
||||
backgroundImage: UniversalImage.provider(
|
||||
_featuredComment!.publisher.avatar,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: MarkdownTextContent(
|
||||
content: _featuredComment!.body['content'],
|
||||
isAutoWarp: true,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
)).animate().fadeIn(duration: 300.ms, curve: Curves.easeInOut);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostAbuseReportDialog extends StatefulWidget {
|
||||
final SnPost data;
|
||||
|
||||
@ -1181,3 +1291,96 @@ class _PostAbuseReportDialogState extends State<_PostAbuseReportDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostGetInsightSheet extends StatefulWidget {
|
||||
final int postId;
|
||||
|
||||
const _PostGetInsightSheet({required this.postId});
|
||||
|
||||
@override
|
||||
State<_PostGetInsightSheet> createState() => _PostGetInsightSheetState();
|
||||
}
|
||||
|
||||
class _PostGetInsightSheetState extends State<_PostGetInsightSheet> {
|
||||
String? _response;
|
||||
String? _thinkingProcess;
|
||||
|
||||
Future<void> _fetchResponse() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/posts/${widget.postId}/insight',
|
||||
options: Options(
|
||||
sendTimeout: const Duration(minutes: 10),
|
||||
receiveTimeout: const Duration(minutes: 10),
|
||||
));
|
||||
final out = resp.data['response'] as String;
|
||||
final document = XmlDocument.parse(out);
|
||||
_thinkingProcess = document.getElement('think')?.innerText.trim();
|
||||
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
|
||||
setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchResponse();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.book_4_spark, size: 24),
|
||||
const Gap(16),
|
||||
Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
const Gap(4),
|
||||
Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20),
|
||||
const Gap(4),
|
||||
if (_response == null)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (_thinkingProcess != null && _thinkingProcess!.isNotEmpty)
|
||||
ExpansionTile(
|
||||
leading: const Icon(Symbols.info),
|
||||
title: Text('aiThinkingProcess'.tr()),
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
minTileHeight: 32,
|
||||
children: [
|
||||
SelectableText(
|
||||
_thinkingProcess!,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic),
|
||||
).padding(horizontal: 20, vertical: 8),
|
||||
],
|
||||
).padding(vertical: 8),
|
||||
SelectionArea(
|
||||
child: MarkdownTextContent(
|
||||
content: _response!,
|
||||
),
|
||||
).padding(horizontal: 20, top: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
||||
);
|
||||
}
|
||||
}
|
||||
HapticFeedback.mediumImpact();
|
||||
HapticFeedback.heavyImpact();
|
||||
} catch (err) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
|
@ -5,10 +5,9 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
import 'package:surface/providers/config.dart';
|
||||
// Keep this import to make the web image render work
|
||||
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
|
||||
class UniversalImage extends StatelessWidget {
|
||||
final String url;
|
||||
|
@ -11,9 +11,11 @@
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_udid/flutter_udid_plugin.h>
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
#include <hotkey_manager_linux/hotkey_manager_linux_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
#include <media_kit_video/media_kit_video_plugin.h>
|
||||
#include <pasteboard/pasteboard_plugin.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
@ -32,6 +34,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
||||
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
||||
g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
|
||||
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
|
||||
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
|
||||
@ -41,6 +46,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
|
||||
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
|
||||
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
|
||||
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
@ -8,9 +8,11 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
hotkey_manager_linux
|
||||
media_kit_libs_linux
|
||||
media_kit_video
|
||||
pasteboard
|
||||
tray_manager
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
@ -8,6 +8,7 @@ import Foundation
|
||||
import bitsdojo_window_macos
|
||||
import connectivity_plus
|
||||
import device_info_plus
|
||||
import file_picker
|
||||
import file_saver
|
||||
import file_selector_macos
|
||||
import firebase_analytics
|
||||
@ -17,6 +18,7 @@ import flutter_inappwebview_macos
|
||||
import flutter_udid
|
||||
import flutter_webrtc
|
||||
import gal
|
||||
import hotkey_manager_macos
|
||||
import in_app_review
|
||||
import livekit_client
|
||||
import media_kit_libs_macos_video
|
||||
@ -28,6 +30,7 @@ import screen_brightness_macos
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
import tray_manager
|
||||
import url_launcher_macos
|
||||
import video_compress
|
||||
import wakelock_plus
|
||||
@ -36,6 +39,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
|
||||
@ -45,6 +49,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
||||
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
|
||||
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
||||
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
|
||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||
@ -56,6 +61,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
|
@ -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.6.0):
|
||||
- Firebase/Analytics (11.7.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (11.6.0):
|
||||
- Firebase/Core (11.7.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 11.6.0)
|
||||
- Firebase/CoreOnly (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- Firebase/Messaging (11.6.0):
|
||||
- FirebaseAnalytics (~> 11.7.0)
|
||||
- Firebase/CoreOnly (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- Firebase/Messaging (11.7.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.6.0)
|
||||
- firebase_analytics (11.4.0):
|
||||
- Firebase/Analytics (= 11.6.0)
|
||||
- FirebaseMessaging (~> 11.7.0)
|
||||
- firebase_analytics (11.4.2):
|
||||
- Firebase/Analytics (= 11.7.0)
|
||||
- firebase_core
|
||||
- FlutterMacOS
|
||||
- firebase_core (3.10.0):
|
||||
- Firebase/CoreOnly (~> 11.6.0)
|
||||
- firebase_core (3.11.0):
|
||||
- Firebase/CoreOnly (~> 11.7.0)
|
||||
- FlutterMacOS
|
||||
- firebase_messaging (15.2.0):
|
||||
- Firebase/CoreOnly (~> 11.6.0)
|
||||
- Firebase/Messaging (~> 11.6.0)
|
||||
- firebase_messaging (15.2.2):
|
||||
- Firebase/CoreOnly (~> 11.7.0)
|
||||
- Firebase/Messaging (~> 11.7.0)
|
||||
- firebase_core
|
||||
- FlutterMacOS
|
||||
- FirebaseAnalytics (11.6.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseAnalytics (11.7.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.7.0)
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleAppMeasurement (= 11.6.0)
|
||||
- GoogleAppMeasurement (= 11.7.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (11.6.0):
|
||||
- FirebaseCoreInternal (~> 11.6.0)
|
||||
- FirebaseCore (11.7.0):
|
||||
- FirebaseCoreInternal (~> 11.7.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- FirebaseCoreInternal (11.6.0):
|
||||
- FirebaseCoreInternal (11.7.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseInstallations (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseInstallations (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (11.6.0):
|
||||
- FirebaseCore (~> 11.6.0)
|
||||
- FirebaseMessaging (11.7.0):
|
||||
- FirebaseCore (~> 11.7.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -85,21 +87,21 @@ PODS:
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleAppMeasurement (11.6.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
|
||||
- GoogleAppMeasurement (11.7.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.7.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.6.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.7.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.7.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
@ -135,6 +137,10 @@ PODS:
|
||||
- GoogleUtilities/UserDefaults (8.0.2):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- HotKey (0.2.1)
|
||||
- hotkey_manager_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- HotKey
|
||||
- in_app_review (2.0.0):
|
||||
- FlutterMacOS
|
||||
- livekit_client (2.3.5):
|
||||
@ -172,6 +178,8 @@ PODS:
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- tray_manager (0.0.1):
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- video_compress (0.3.0):
|
||||
@ -185,6 +193,7 @@ 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`)
|
||||
@ -195,6 +204,7 @@ DEPENDENCIES:
|
||||
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
||||
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
|
||||
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
||||
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
||||
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||
@ -207,6 +217,7 @@ DEPENDENCIES:
|
||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
@ -222,6 +233,7 @@ SPEC REPOS:
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- HotKey
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
@ -237,6 +249,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:
|
||||
@ -257,6 +271,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral
|
||||
gal:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
||||
hotkey_manager_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
|
||||
in_app_review:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
|
||||
livekit_client:
|
||||
@ -281,6 +297,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
sqflite_darwin:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||
tray_manager:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
video_compress:
|
||||
@ -293,25 +311,28 @@ SPEC CHECKSUMS:
|
||||
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
|
||||
croppy: 25a638bd7d05411d8c697f481568f261037694fc
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
|
||||
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
|
||||
firebase_analytics: 5249f87da6fed852901581aab2602e0280ec2fdb
|
||||
firebase_core: 6d9bb8b0ea817e8fe0d928177d42275b45fdba6f
|
||||
firebase_messaging: ae8e88b586e4d50abc7cac5bacf74d21967fd226
|
||||
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
|
||||
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
|
||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
||||
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
|
||||
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
|
||||
Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4
|
||||
firebase_analytics: 41d88c024a7756462a803e36236ba74f24cdc2c5
|
||||
firebase_core: 751d3d919b95d4ae46ab049d0d64d42d4eec086b
|
||||
firebase_messaging: cc174f19945e9541e140e3cb0118448e59b38c6c
|
||||
FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec
|
||||
FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4
|
||||
FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881
|
||||
FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9
|
||||
FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c
|
||||
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
|
||||
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
|
||||
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
|
||||
GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
|
||||
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
|
||||
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
||||
livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
|
||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||
@ -328,6 +349,7 @@ SPEC CHECKSUMS:
|
||||
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
|
186
pubspec.lock
186
pubspec.lock
@ -13,10 +13,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "27899c95f9e7ec06c8310e6e0eac967707714b9f1450c4a58fa00ca011a4a8ae"
|
||||
sha256: e051259913915ea5bc8fe18664596bea08592fd123930605d562969cd7315fcd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.49"
|
||||
version: "1.3.51"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
@ -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:
|
||||
@ -362,10 +362,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: b37d37c2f912ad4e8ec694187de87d05de2a3cb82b465ff1f65f65a2d05de544
|
||||
sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.2.1"
|
||||
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: "498c6cb8468e348a556709c745d92a52173ab3a9b906aa0593393f0787f201ea"
|
||||
sha256: "47428047a0778f72af53a3c7cb5d556e1cb25e2327cc8aa40d544971dc6245b2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.4.0"
|
||||
version: "11.4.2"
|
||||
firebase_analytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_platform_interface
|
||||
sha256: ccbb350554e98afdb4b59852689292d194d31232a2647b5012a66622b3711df9
|
||||
sha256: "1076f4b041f76143e14878c70f0758f17fe5910c0cd992db9e93bd3c3584512b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
version: "4.3.2"
|
||||
firebase_analytics_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_web
|
||||
sha256: "68e1f18fc16482c211c658e739c25f015b202a260d9ad8249c6d3d7963b8105f"
|
||||
sha256: "8f6dd64ea6d28b7f5b9e739d183a9e1c7f17027794a3e9aba1879621d42426ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.10+6"
|
||||
version: "0.5.10+8"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "0307c1fde82e2b8b97e0be2dab93612aff9a72f31ebe9bfac66ed8b37ef7c568"
|
||||
sha256: "93dc4dd12f9b02c5767f235307f609e61ed9211047132d07f9e02c668f0bfc33"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.10.0"
|
||||
version: "3.11.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -578,34 +578,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: fbc008cf390d909b823763064b63afefe9f02d8afdb13eb3f485b871afee956b
|
||||
sha256: "0e13c80f0de8acaa5d0519cbe23c8b4cc138a2d5d508b5755c861bdfc9762678"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.19.0"
|
||||
version: "2.20.0"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "48a8a59197c1c5174060ba9aa1e0036e9b5a0d28a0cc22d19c1fcabc67fafe3c"
|
||||
sha256: "3dee3b0cbfe719e64773cb7d1cad57c58b2235a8c136f5715fe733a54058c783"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
version: "15.2.2"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "9770a8e91f54296829dcaa61ce9b7c2f9ae9abbf99976dd3103a60470d5264dd"
|
||||
sha256: e9ea726b9bb864fc6223bb66422bd9877b9973ae51967754a769b0d01e201c1e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
version: "4.6.2"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: "329ca4ef45ec616abe6f1d5e58feed0934a50840a65aa327052354ad3c64ed77"
|
||||
sha256: "5f7b40e8bf861a37f8b8196e347d8a919750421a45f0b45d1bb74e98fa72726e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.10.0"
|
||||
version: "3.10.2"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -764,10 +764,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487
|
||||
sha256: b3ff1ef5fb3924ee02b4d38b974ffae3969d50603e68787684ee9dd45f6f144a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.5"
|
||||
version: "0.7.6+1"
|
||||
flutter_native_splash:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -830,10 +830,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_webrtc
|
||||
sha256: "188401cc3275bc4f1f965babdff6cac612a4b46572f1e49f49db8af5361d5712"
|
||||
sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.6"
|
||||
version: "0.12.7"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -878,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: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d"
|
||||
sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.6.3"
|
||||
version: "14.7.2"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -938,6 +938,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
hotkey_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hotkey_manager
|
||||
sha256: "06f0655b76c8dd322fb7101dc615afbdbf39c3d3414df9e059c33892104479cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
hotkey_manager_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotkey_manager_linux
|
||||
sha256: "83676bda8210a3377bc6f1977f193bc1dbdd4c46f1bdd02875f44b6eff9a8473"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
hotkey_manager_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotkey_manager_macos
|
||||
sha256: "03b5967e64357b9ac05188ea4a5df6fe4ed4205762cb80aaccf8916ee1713c96"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
hotkey_manager_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotkey_manager_platform_interface
|
||||
sha256: "98ffca25b8cc9081552902747b2942e3bc37855389a4218c9d50ca316b653b13"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
hotkey_manager_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotkey_manager_windows
|
||||
sha256: "0d03ced9fe563ed0b68f0a0e1b22c9ffe26eb8053cb960e401f68a4f070e0117"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -950,10 +990,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:
|
||||
@ -1030,10 +1070,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
|
||||
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
version: "0.2.1+2"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1282,6 +1322,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.5"
|
||||
menu_base:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: menu_base
|
||||
sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1334,18 +1382,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790"
|
||||
sha256: c447a3c3e7be4addf129b8f9ab6a4bd5d166b78918223e223b61fddf4d07e254
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.3"
|
||||
version: "8.2.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b
|
||||
sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.1.0"
|
||||
pasteboard:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1710,18 +1758,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: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5"
|
||||
sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.4.4"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1778,6 +1826,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
shortid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shortid
|
||||
sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -1951,6 +2007,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
tray_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tray_manager
|
||||
sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1959,6 +2023,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uni_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uni_platform
|
||||
sha256: e02213a7ee5352212412ca026afd41d269eb00d982faa552f419ffc2debfad84
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
universal_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2059,10 +2131,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
|
||||
sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.15"
|
||||
version: "1.1.18"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2171,10 +2243,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:
|
||||
@ -2187,10 +2259,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:
|
||||
@ -2216,7 +2288,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.2+57
|
||||
version: 2.3.2+63
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@ -117,6 +117,9 @@ dependencies:
|
||||
cached_network_image: ^3.4.1
|
||||
flutter_inappwebview: ^6.1.5
|
||||
html: ^0.15.5
|
||||
xml: ^6.5.0
|
||||
tray_manager: ^0.3.2
|
||||
hotkey_manager: ^0.2.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -151,6 +154,8 @@ flutter:
|
||||
- assets/icon/icon.png
|
||||
- assets/icon/icon-dark.png
|
||||
- assets/icon/icon-light-radius.png
|
||||
- assets/icon/tray-icon.ico
|
||||
- assets/icon/tray-icon.png
|
||||
- assets/translations/
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
|
@ -15,6 +15,7 @@
|
||||
#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>
|
||||
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
|
||||
#include <livekit_client/live_kit_plugin.h>
|
||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
@ -22,6 +23,7 @@
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <screen_brightness_windows/screen_brightness_windows_plugin.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
@ -43,6 +45,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
||||
GalPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("GalPluginCApi"));
|
||||
HotkeyManagerWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
|
||||
LiveKitPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("LiveKitPlugin"));
|
||||
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
||||
@ -57,6 +61,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
TrayManagerPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
gal
|
||||
hotkey_manager_windows
|
||||
livekit_client
|
||||
media_kit_libs_windows_video
|
||||
media_kit_video
|
||||
@ -19,6 +20,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
permission_handler_windows
|
||||
screen_brightness_windows
|
||||
share_plus
|
||||
tray_manager
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
Reference in New Issue
Block a user