Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
6e6c3f42f6 | |||
dc38b46b2c | |||
b4990308e9 | |||
237abe564d | |||
71b41d470a | |||
7052b5b635 | |||
f356e08f79 | |||
152872db65 | |||
dfe117d04f | |||
caf63f0cbe | |||
b8f5cc82f9 | |||
360bc50f21 | |||
2de93a0486 | |||
02227852f8 | |||
ad16de595b | |||
9f8c8923d9 | |||
060bfa4887 | |||
e68ada2d04 | |||
d6013078bd | |||
5976d61997 | |||
b492db90ca | |||
c9f69fed2c | |||
d2f4e7a969 | |||
aecd04e0b9 | |||
e5212419ae | |||
ec7650a920 | |||
7b96013406 | |||
fc5a79b29b | |||
4146820be5 | |||
9ec0f1ff19 | |||
ac2aec48aa | |||
58421e5d5e | |||
172d0d24fb | |||
71899dd4f2 | |||
02ffe9866d | |||
1b7e668b3f | |||
f03d80ba88 | |||
14ee6845ed | |||
8fe6c2be46 | |||
78e765f69d | |||
ddd6ff7eee | |||
b8f379796f | |||
3a10e9280c | |||
65fe06de22 | |||
e44320e0fe | |||
f2d913ffec | |||
e88dea8858 | |||
813679b161 | |||
9d4ce6ca8c |
11
api/Interactive/Trigger Fediverse Scan.bru
Normal file
11
api/Interactive/Trigger Fediverse Scan.bru
Normal file
@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Trigger Fediverse Scan
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/co/admin/fediverse
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
@ -5,14 +5,14 @@ meta {
|
||||
}
|
||||
|
||||
put {
|
||||
url: {{endpoint}}/cgi/id/reports/abuse/3/status
|
||||
url: {{endpoint}}/cgi/id/reports/abuse/6/status
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"status": "processed",
|
||||
"message": "相关附件已经进行评级处理,未来会将该项权限下放到帖主以及社区成员。"
|
||||
"status": "rejected",
|
||||
"message": "Not a good reason"
|
||||
}
|
||||
}
|
||||
|
@ -15,12 +15,10 @@ body:json {
|
||||
"client_id": "{{third_client_id}}",
|
||||
"client_secret":"{{third_client_tk}}",
|
||||
"type": "general",
|
||||
"subject": "新年快乐!",
|
||||
"subtitle": "一条来自 Solar Network 团队的信息",
|
||||
"content": "今天是农历正月初一,小羊祝您新年快乐 🎉",
|
||||
"metadata": {
|
||||
"image": "D2EDbcrsTugs3xk5"
|
||||
},
|
||||
"subject": "关于迁移服务器完成的提示",
|
||||
"subtitle": "一条来自 Solar Network 团队的运营信息",
|
||||
"content": "我们已经将所有用户数据迁移到新版服务器,刚刚发布新的 DNS,因为部分 DNS 缓存的影响。可能更改不会生效,可以使用 nslookup / ping 检查解析地址是否未 8. 开头,您可以主动刷新 DNS。谢谢!",
|
||||
"metadata": {},
|
||||
"priority": 10
|
||||
}
|
||||
}
|
||||
|
@ -7,5 +7,5 @@ meta {
|
||||
get {
|
||||
url: {{endpoint}}/cgi/re/well-known/sources
|
||||
body: none
|
||||
auth: none
|
||||
auth: inherit
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ post {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"sources": ["taiwan-ltn"],
|
||||
"sources": ["taiwan-pts"],
|
||||
"eager": true
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +130,7 @@
|
||||
"accountPublishersSubtitle": "Manage your publish identities.",
|
||||
"accountSettings": "Account Settings",
|
||||
"accountSettingsSubtitle": "Manage your account and make it yours.",
|
||||
"accountProfileEdit": "Edit your profile",
|
||||
"accountProfileEdit": "Edit Profile",
|
||||
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
|
||||
"accountWallet": "Wallet",
|
||||
"accountWalletSubtitle": "View your balance and transactions.",
|
||||
@ -207,6 +207,7 @@
|
||||
"one": "{} comment",
|
||||
"other": "{} comments"
|
||||
},
|
||||
"postCommentExpand": "Show comments",
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsCustomFonts": "Custom Fonts",
|
||||
"settingsCustomFontsDescription": "Set custom fonts for the application.",
|
||||
@ -337,6 +338,7 @@
|
||||
"fieldAttachmentRandomId": "Random ID",
|
||||
"fieldAttachmentAlt": "Alternative text",
|
||||
"addAttachmentFromAlbum": "Add from album",
|
||||
"addAttachmentFromFiles": "Add from files",
|
||||
"addAttachmentFromClipboard": "Paste file",
|
||||
"addAttachmentFromCameraPhoto": "Take photo",
|
||||
"addAttachmentFromCameraVideo": "Take video",
|
||||
@ -791,5 +793,102 @@
|
||||
"fieldAccountStatusClearAt": "Clear At",
|
||||
"accountStatusNegative": "Negative",
|
||||
"accountStatusNeutral": "Neutral",
|
||||
"accountStatusPositive": "Positive"
|
||||
"accountStatusPositive": "Positive",
|
||||
"mixedFeed": "Mixed Feed",
|
||||
"mixedFeedDescription": "The Explore screen may not only display the user's posts, but may also contain other content. However, this mode does not apply to classification and filtering.",
|
||||
"filterFeed": "Exploring Adjust",
|
||||
"feedUnknownItem": "Unable to display this content, the current version of the client does not support the type of content, please try to update the application afterwards.",
|
||||
"serviceStatusOperational": "All services operational",
|
||||
"serviceStatusDowngraded": "Some services downgraded",
|
||||
"serviceStatusFailed": "All services unavailable",
|
||||
"serviceStatusFailedDescription": "The server is down or the maintenance is just finished.",
|
||||
"serviceNameInsights": "Summarize and Insights",
|
||||
"serviceNameInteractive": "Posts, Reactions and Explore",
|
||||
"serviceNameReader": "News and Link Previews",
|
||||
"serviceNameMessaging": "Chat",
|
||||
"serviceNameMatrix": "Matrix Software and Game Marketplace",
|
||||
"serviceNamePaperclip": "Attachments, Images and Files",
|
||||
"serviceNameWallet": "Source Points Wallet",
|
||||
"serviceNamePassport": "Authorization and Authentication",
|
||||
"accountActionEvent": "Action Events",
|
||||
"accountActionEventDescription": "View your action event logs.",
|
||||
"eventMetadata": "Metadata",
|
||||
"accountAuthTickets": "Auth Sessions",
|
||||
"accountAuthTicketsDescription": "View and manage your auth sessions.",
|
||||
"authTicketCreatedAt": "Issued at {}",
|
||||
"authTicketExpiredAt": "Expired at {}",
|
||||
"authTicketLastGrantAt": "Last granted at {}",
|
||||
"authTicketCurrent": "Current",
|
||||
"accountUnconfirmedTitle": "Unconfirmed Account",
|
||||
"accountUnconfirmedSubtitle": "Your account is unconfirmed, which will make most features unavailable and your account will be destroyed in 24 hours. You should receive an email in your inbox with a confirmation link.",
|
||||
"accountUnconfirmedUnreceived": "Didn't receive the email?",
|
||||
"accountUnconfirmedResend": "Resend one",
|
||||
"accountUnconfirmedResendSuccessful": "Email has been resent, you can resend it again in 60 minutes.",
|
||||
"stickerPickerEmpty": "Sticker list is empty",
|
||||
"stickerPickerEmptyHint": "To start using stickers, you need to add a sticker pack first.",
|
||||
"goto": "Go to {}",
|
||||
"accountContactMethods": "Contact Methods",
|
||||
"accountContactMethodsDescription": "Manage your contact methods.",
|
||||
"accountContactMethodsNameEmail": "Email address",
|
||||
"accountContactMethodsNamePhone": "Phone number",
|
||||
"accountContactMethodsNameAddress": "Address",
|
||||
"accountContactMethodsPrimary": "Primary",
|
||||
"accountContactMethodsVerified": "Verified",
|
||||
"accountContactMethodsPublic": "Public",
|
||||
"accountContactMethodsAdd": "Add Contact Method",
|
||||
"accountContactMethodsEdit": "Edit Contact Method",
|
||||
"accountContactMethodsAddDescription": "Add a new contact method.",
|
||||
"fieldContactContent": "Contact method",
|
||||
"accountContactMethodsPublicHint": "This contact method will be displayed publicly on your profile.",
|
||||
"accountContactMethodsDelete": "Delete Contact Method",
|
||||
"accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible.",
|
||||
"postCommentAdd": "Write a comment",
|
||||
"translate": "Translate",
|
||||
"translating": "Translating…",
|
||||
"translated": "Translated",
|
||||
"settingsAutoTranslate": "Auto Translate",
|
||||
"settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages.",
|
||||
"trayMenuHide": "Hide",
|
||||
"accountSettingsNotify": "Notify Settings",
|
||||
"accountSettingsNotifyDescription": "Adjust the types of notifications you receive.",
|
||||
"accountSettingsSecurity": "Security Settings",
|
||||
"accountSettingsSecurityDescription": "Adjust your account security settings.",
|
||||
"save": "Save",
|
||||
"notificationTopicPostFeedback": "Post Feedback",
|
||||
"notificationTopicPostReply": "Post Replies",
|
||||
"notificationTopicPostSubscription": "Post Subscriptions",
|
||||
"notificationTopicMessaging": "New Messages",
|
||||
"notificationTopicMessagingCall": "Incoming Calls",
|
||||
"notificationTopicGeneral": "General",
|
||||
"authMaximumAuthSteps": "Maximum Authenticate Steps",
|
||||
"authMaximumAuthStepsDescription": {
|
||||
"one": "Maximum ask for {} step authenticate",
|
||||
"other": "Maximum ask for {} steps authenticate"
|
||||
},
|
||||
"authAlwaysRisky": "Always Risky",
|
||||
"authAlwaysRiskyDescription": "Always ask for the highest steps count of authentication when logging in.",
|
||||
"chatUnjoined": "Unjoined Channel",
|
||||
"chatUnjoinedDescription": "You haven't joined this channel, so you can't send messages either view messages in it.",
|
||||
"chatUnjoinedPublicDescription": "Fortunately, this is a public channel, so you can join it as you want.",
|
||||
"chatJoin": "Join the Channel",
|
||||
"appInitStarting": "Starting",
|
||||
"appInitNetwork": "Initializing Network",
|
||||
"appInitUserdata": "Initializing User Data",
|
||||
"appInitWebsocket": "Establishing Solar Link",
|
||||
"appInitNotification": "Initializing Push Notifications",
|
||||
"appInitKeyPair": "Initializing Key Pairs",
|
||||
"appInitStickers": "Initializing Stickers",
|
||||
"appInitUserDirectory": "Initializing User Directory",
|
||||
"appInitRealm": "Initializing Realms",
|
||||
"appInitChat": "Initializing Chat",
|
||||
"appInitDone": "Completed",
|
||||
"community": "Community",
|
||||
"realmCommunity": "{}'s Community",
|
||||
"postTotalCount": {
|
||||
"one": "Total {} post",
|
||||
"other": "Total {} posts"
|
||||
},
|
||||
"settingsHideBottomNav": "Hide Bottom Navigation",
|
||||
"settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.",
|
||||
"reCaptcha": "reCaptcha"
|
||||
}
|
||||
|
@ -205,6 +205,7 @@
|
||||
"one": "{} 条评论",
|
||||
"other": "{} 条评论"
|
||||
},
|
||||
"postCommentExpand": "展开评论",
|
||||
"settingsAppearance": "外观",
|
||||
"settingsCustomFonts": "自定义字体",
|
||||
"settingsCustomFontsDescription": "设置应用程序使用的字体。",
|
||||
@ -335,6 +336,7 @@
|
||||
"fieldAttachmentRandomId": "访问 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||
"addAttachmentFromFiles": "从文件中添加附件",
|
||||
"addAttachmentFromClipboard": "粘贴附件",
|
||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||
"addAttachmentFromCameraVideo": "拍摄视频",
|
||||
@ -789,5 +791,102 @@
|
||||
"fieldAccountStatusClearAt": "清除时间",
|
||||
"accountStatusNegative": "负面",
|
||||
"accountStatusNeutral": "中性",
|
||||
"accountStatusPositive": "正面"
|
||||
"accountStatusPositive": "正面",
|
||||
"mixedFeed": "混合推荐流",
|
||||
"mixedFeedDescription": "探索页面可能不只会展示用户的帖子,更可能包含其他的内容。但该模式不适用分类和过滤。",
|
||||
"filterFeed": "探索队列调整",
|
||||
"feedUnknownItem": "无法显示该内容,当前版本客户端不支持该类型的内容,请尝试更新应用程序后再试。",
|
||||
"serviceStatusOperational": "所有服务正常",
|
||||
"serviceStatusDowngraded": "部分服务异常",
|
||||
"serviceStatusFailed": "服务状态异常",
|
||||
"serviceStatusFailedDescription": "服务器炸了或者刚刚执行完维护程序。",
|
||||
"serviceNameInsights": "总结、见解与洞察",
|
||||
"serviceNameInteractive": "帖子与互动",
|
||||
"serviceNameReader": "新闻与链接展开",
|
||||
"serviceNameMessaging": "即使聊天",
|
||||
"serviceNameMatrix": "矩阵市场",
|
||||
"serviceNamePaperclip": "附件",
|
||||
"serviceNameWallet": "源点钱包",
|
||||
"serviceNamePassport": "身份验证与授权",
|
||||
"accountActionEvent": "操作日志",
|
||||
"accountActionEventDescription": "查看你的操作日志。",
|
||||
"eventMetadata": "元数据",
|
||||
"accountAuthTickets": "授权会话",
|
||||
"accountAuthTicketsDescription": "查看和管理你的授权会话。",
|
||||
"authTicketCreatedAt": "签发于 {}",
|
||||
"authTicketExpiredAt": "到期于 {}",
|
||||
"authTicketLastGrantAt": "上次刷新于 {}",
|
||||
"authTicketCurrent": "当前会话",
|
||||
"accountUnconfirmedTitle": "尚未未确认账户",
|
||||
"accountUnconfirmedSubtitle": "您的账户尚未确认,这会导致大部分功能不可用,并且您的帐户将在 24 小时后自毁。您绑定的邮箱的收件箱应该会有一封邮件指引您确认您的帐户。",
|
||||
"accountUnconfirmedUnreceived": "未收到邮件?",
|
||||
"accountUnconfirmedResend": "重新发送一封",
|
||||
"accountUnconfirmedResendSuccessful": "邮件已重新发送,你可以在 60 分钟后再重发一封。",
|
||||
"stickerPickerEmpty": "贴图列表为空",
|
||||
"stickerPickerEmptyHint": "想要开始使用贴图,请先添加贴图包。",
|
||||
"goto": "跳转到 {}",
|
||||
"accountContactMethods": "联系方式",
|
||||
"accountContactMethodsDescription": "管理你的联系方式。",
|
||||
"accountContactMethodsNameEmail": "电子邮箱",
|
||||
"accountContactMethodsNamePhone": "电话",
|
||||
"accountContactMethodsNameAddress": "地址",
|
||||
"accountContactMethodsPrimary": "主要的",
|
||||
"accountContactMethodsVerified": "已验证",
|
||||
"accountContactMethodsPublic": "公开的",
|
||||
"accountContactMethodsAdd": "添加联系方式",
|
||||
"accountContactMethodsEdit": "编辑联系方式",
|
||||
"accountContactMethodsAddDescription": "添加新的联系方式。",
|
||||
"fieldContactContent": "联系方式",
|
||||
"accountContactMethodsPublicHint": "这个联系方式公开地显示在个人资料中。",
|
||||
"accountContactMethodsDelete": "删除联系方式",
|
||||
"accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。",
|
||||
"postCommentAdd": "撰写一条评论",
|
||||
"translate": "翻译",
|
||||
"translating": "正在翻译……",
|
||||
"translated": "已翻译",
|
||||
"settingsAutoTranslate": "自动翻译",
|
||||
"settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。",
|
||||
"trayMenuHide": "隐藏",
|
||||
"accountSettingsNotify": "通知设置",
|
||||
"accountSettingsNotifyDescription": "调整你所收到的通知种类。",
|
||||
"accountSettingsSecurity": "安全设置",
|
||||
"accountSettingsSecurityDescription": "调整你的帐户安全设置。",
|
||||
"save": "保存",
|
||||
"notificationTopicPostFeedback": "帖子数据反馈",
|
||||
"notificationTopicPostReply": "帖子回复",
|
||||
"notificationTopicPostSubscription": "帖子订阅",
|
||||
"notificationTopicMessaging": "消息",
|
||||
"notificationTopicMessagingCall": "通话",
|
||||
"notificationTopicGeneral": "杂项",
|
||||
"authMaximumAuthSteps": "最大验证步骤",
|
||||
"authMaximumAuthStepsDescription": {
|
||||
"one": "登入时最多要求 {} 步验证",
|
||||
"other": "登入时最多要求 {} 步验证"
|
||||
},
|
||||
"authAlwaysRisky": "总是风险",
|
||||
"authAlwaysRiskyDescription": "在登入时始终按最高标准要求验证。",
|
||||
"chatUnjoined": "未加入频道",
|
||||
"chatUnjoinedDescription": "你没有加入这个频道,所以你也无法发送消息或者查看这个频道中的消息。",
|
||||
"chatUnjoinedPublicDescription": "但幸运的是,这是一个公开频道,所以你可以主动加入。",
|
||||
"chatJoin": "加入频道",
|
||||
"appInitStarting": "启动中",
|
||||
"appInitNetwork": "正在初始化网络",
|
||||
"appInitUserdata": "正在初始化用户数据",
|
||||
"appInitWebsocket": "正在建立 Solar Link",
|
||||
"appInitNotification": "正在初始化推送通知",
|
||||
"appInitKeyPair": "正在初始化密钥对",
|
||||
"appInitStickers": "正在初始化贴图包",
|
||||
"appInitUserDirectory": "正在初始化用户目录",
|
||||
"appInitRealm": "正在初始化领域信息",
|
||||
"appInitChat": "正在初始化聊天",
|
||||
"appInitDone": "完成",
|
||||
"community": "社区",
|
||||
"realmCommunity": "{}的社区",
|
||||
"postTotalCount": {
|
||||
"zero": "没有帖子",
|
||||
"one": "共 {} 条帖子"
|
||||
},
|
||||
"settingsHideBottomNav": "隐藏底部导航栏",
|
||||
"settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。",
|
||||
"reCaptcha": "人机验证"
|
||||
}
|
||||
|
@ -205,6 +205,7 @@
|
||||
"one": "{} 條評論",
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"postCommentExpand": "展開評論",
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsCustomFonts": "自定義字體",
|
||||
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
|
||||
@ -335,6 +336,7 @@
|
||||
"fieldAttachmentRandomId": "訪問 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||
"addAttachmentFromFiles": "從文件中添加附件",
|
||||
"addAttachmentFromClipboard": "粘貼附件",
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||
@ -789,5 +791,102 @@
|
||||
"fieldAccountStatusClearAt": "清除時間",
|
||||
"accountStatusNegative": "負面",
|
||||
"accountStatusNeutral": "中性",
|
||||
"accountStatusPositive": "正面"
|
||||
"accountStatusPositive": "正面",
|
||||
"mixedFeed": "混合推薦流",
|
||||
"mixedFeedDescription": "探索頁面可能不只會展示用户的帖子,更可能包含其他的內容。但該模式不適用分類和過濾。",
|
||||
"filterFeed": "探索隊列調整",
|
||||
"feedUnknownItem": "無法顯示該內容,當前版本客户端不支持該類型的內容,請嘗試更新應用程序後再試。",
|
||||
"serviceStatusOperational": "所有服務正常",
|
||||
"serviceStatusDowngraded": "部分服務異常",
|
||||
"serviceStatusFailed": "服務狀態異常",
|
||||
"serviceStatusFailedDescription": "服務器炸了或者剛剛執行完維護程序。",
|
||||
"serviceNameInsights": "總結、見解與洞察",
|
||||
"serviceNameInteractive": "帖子與互動",
|
||||
"serviceNameReader": "新聞與鏈接展開",
|
||||
"serviceNameMessaging": "即使聊天",
|
||||
"serviceNameMatrix": "矩陣市場",
|
||||
"serviceNamePaperclip": "附件",
|
||||
"serviceNameWallet": "源點錢包",
|
||||
"serviceNamePassport": "身份驗證與授權",
|
||||
"accountActionEvent": "操作日誌",
|
||||
"accountActionEventDescription": "查看你的操作日誌。",
|
||||
"eventMetadata": "元數據",
|
||||
"accountAuthTickets": "授權會話",
|
||||
"accountAuthTicketsDescription": "查看和管理你的授權會話。",
|
||||
"authTicketCreatedAt": "簽發於 {}",
|
||||
"authTicketExpiredAt": "到期於 {}",
|
||||
"authTicketLastGrantAt": "上次刷新於 {}",
|
||||
"authTicketCurrent": "當前會話",
|
||||
"accountUnconfirmedTitle": "尚未未確認賬户",
|
||||
"accountUnconfirmedSubtitle": "您的賬户尚未確認,這會導致大部分功能不可用,並且您的帳户將在 24 小時後自毀。您綁定的郵箱的收件箱應該會有一封郵件指引您確認您的帳户。",
|
||||
"accountUnconfirmedUnreceived": "未收到郵件?",
|
||||
"accountUnconfirmedResend": "重新發送一封",
|
||||
"accountUnconfirmedResendSuccessful": "郵件已重新發送,你可以在 60 分鐘後再重發一封。",
|
||||
"stickerPickerEmpty": "貼圖列表為空",
|
||||
"stickerPickerEmptyHint": "想要開始使用貼圖,請先添加貼圖包。",
|
||||
"goto": "跳轉到 {}",
|
||||
"accountContactMethods": "聯繫方式",
|
||||
"accountContactMethodsDescription": "管理你的聯繫方式。",
|
||||
"accountContactMethodsNameEmail": "電子郵箱",
|
||||
"accountContactMethodsNamePhone": "電話",
|
||||
"accountContactMethodsNameAddress": "地址",
|
||||
"accountContactMethodsPrimary": "主要的",
|
||||
"accountContactMethodsVerified": "已驗證",
|
||||
"accountContactMethodsPublic": "公開的",
|
||||
"accountContactMethodsAdd": "添加聯繫方式",
|
||||
"accountContactMethodsEdit": "編輯聯繫方式",
|
||||
"accountContactMethodsAddDescription": "添加新的聯繫方式。",
|
||||
"fieldContactContent": "聯繫方式",
|
||||
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
|
||||
"accountContactMethodsDelete": "刪除聯繫方式",
|
||||
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
|
||||
"postCommentAdd": "撰寫一條評論",
|
||||
"translate": "翻譯",
|
||||
"translating": "正在翻譯……",
|
||||
"translated": "已翻譯",
|
||||
"settingsAutoTranslate": "自動翻譯",
|
||||
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。",
|
||||
"trayMenuHide": "隱藏",
|
||||
"accountSettingsNotify": "通知設置",
|
||||
"accountSettingsNotifyDescription": "調整你所收到的通知種類。",
|
||||
"accountSettingsSecurity": "安全設置",
|
||||
"accountSettingsSecurityDescription": "調整你的帳户安全設置。",
|
||||
"save": "保存",
|
||||
"notificationTopicPostFeedback": "帖子數據反饋",
|
||||
"notificationTopicPostReply": "帖子回覆",
|
||||
"notificationTopicPostSubscription": "帖子訂閲",
|
||||
"notificationTopicMessaging": "消息",
|
||||
"notificationTopicMessagingCall": "通話",
|
||||
"notificationTopicGeneral": "雜項",
|
||||
"authMaximumAuthSteps": "最大驗證步驟",
|
||||
"authMaximumAuthStepsDescription": {
|
||||
"one": "登入時最多要求 {} 步驗證",
|
||||
"other": "登入時最多要求 {} 步驗證"
|
||||
},
|
||||
"authAlwaysRisky": "總是風險",
|
||||
"authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。",
|
||||
"chatUnjoined": "未加入頻道",
|
||||
"chatUnjoinedDescription": "你沒有加入這個頻道,所以你也無法發送消息或者查看這個頻道中的消息。",
|
||||
"chatUnjoinedPublicDescription": "但幸運的是,這是一個公開頻道,所以你可以主動加入。",
|
||||
"chatJoin": "加入頻道",
|
||||
"appInitStarting": "啓動中",
|
||||
"appInitNetwork": "正在初始化網絡",
|
||||
"appInitUserdata": "正在初始化用户數據",
|
||||
"appInitWebsocket": "正在建立 Solar Link",
|
||||
"appInitNotification": "正在初始化推送通知",
|
||||
"appInitKeyPair": "正在初始化密鑰對",
|
||||
"appInitStickers": "正在初始化貼圖包",
|
||||
"appInitUserDirectory": "正在初始化用户目錄",
|
||||
"appInitRealm": "正在初始化領域信息",
|
||||
"appInitChat": "正在初始化聊天",
|
||||
"appInitDone": "完成",
|
||||
"community": "社區",
|
||||
"realmCommunity": "{}的社區",
|
||||
"postTotalCount": {
|
||||
"zero": "沒有帖子",
|
||||
"one": "共 {} 條帖子"
|
||||
},
|
||||
"settingsHideBottomNav": "隱藏底部導航欄",
|
||||
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
|
||||
"reCaptcha": "人機驗證"
|
||||
}
|
||||
|
@ -205,6 +205,7 @@
|
||||
"one": "{} 條評論",
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"postCommentExpand": "展開評論",
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsCustomFonts": "自定義字體",
|
||||
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
|
||||
@ -335,6 +336,7 @@
|
||||
"fieldAttachmentRandomId": "訪問 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||
"addAttachmentFromFiles": "從文件中添加附件",
|
||||
"addAttachmentFromClipboard": "粘貼附件",
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||
@ -789,5 +791,102 @@
|
||||
"fieldAccountStatusClearAt": "清除時間",
|
||||
"accountStatusNegative": "負面",
|
||||
"accountStatusNeutral": "中性",
|
||||
"accountStatusPositive": "正面"
|
||||
"accountStatusPositive": "正面",
|
||||
"mixedFeed": "混合推薦流",
|
||||
"mixedFeedDescription": "探索頁面可能不只會展示用戶的帖子,更可能包含其他的內容。但該模式不適用分類和過濾。",
|
||||
"filterFeed": "探索隊列調整",
|
||||
"feedUnknownItem": "無法顯示該內容,當前版本客戶端不支持該類型的內容,請嘗試更新應用程序後再試。",
|
||||
"serviceStatusOperational": "所有服務正常",
|
||||
"serviceStatusDowngraded": "部分服務異常",
|
||||
"serviceStatusFailed": "服務狀態異常",
|
||||
"serviceStatusFailedDescription": "服務器炸了或者剛剛執行完維護程序。",
|
||||
"serviceNameInsights": "總結、見解與洞察",
|
||||
"serviceNameInteractive": "帖子與互動",
|
||||
"serviceNameReader": "新聞與鏈接展開",
|
||||
"serviceNameMessaging": "即使聊天",
|
||||
"serviceNameMatrix": "矩陣市場",
|
||||
"serviceNamePaperclip": "附件",
|
||||
"serviceNameWallet": "源點錢包",
|
||||
"serviceNamePassport": "身份驗證與授權",
|
||||
"accountActionEvent": "操作日誌",
|
||||
"accountActionEventDescription": "查看你的操作日誌。",
|
||||
"eventMetadata": "元數據",
|
||||
"accountAuthTickets": "授權會話",
|
||||
"accountAuthTicketsDescription": "查看和管理你的授權會話。",
|
||||
"authTicketCreatedAt": "簽發於 {}",
|
||||
"authTicketExpiredAt": "到期於 {}",
|
||||
"authTicketLastGrantAt": "上次刷新於 {}",
|
||||
"authTicketCurrent": "當前會話",
|
||||
"accountUnconfirmedTitle": "尚未未確認賬戶",
|
||||
"accountUnconfirmedSubtitle": "您的賬戶尚未確認,這會導致大部分功能不可用,並且您的帳戶將在 24 小時後自毀。您綁定的郵箱的收件箱應該會有一封郵件指引您確認您的帳戶。",
|
||||
"accountUnconfirmedUnreceived": "未收到郵件?",
|
||||
"accountUnconfirmedResend": "重新發送一封",
|
||||
"accountUnconfirmedResendSuccessful": "郵件已重新發送,你可以在 60 分鐘後再重發一封。",
|
||||
"stickerPickerEmpty": "貼圖列表為空",
|
||||
"stickerPickerEmptyHint": "想要開始使用貼圖,請先添加貼圖包。",
|
||||
"goto": "跳轉到 {}",
|
||||
"accountContactMethods": "聯繫方式",
|
||||
"accountContactMethodsDescription": "管理你的聯繫方式。",
|
||||
"accountContactMethodsNameEmail": "電子郵箱",
|
||||
"accountContactMethodsNamePhone": "電話",
|
||||
"accountContactMethodsNameAddress": "地址",
|
||||
"accountContactMethodsPrimary": "主要的",
|
||||
"accountContactMethodsVerified": "已驗證",
|
||||
"accountContactMethodsPublic": "公開的",
|
||||
"accountContactMethodsAdd": "添加聯繫方式",
|
||||
"accountContactMethodsEdit": "編輯聯繫方式",
|
||||
"accountContactMethodsAddDescription": "添加新的聯繫方式。",
|
||||
"fieldContactContent": "聯繫方式",
|
||||
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
|
||||
"accountContactMethodsDelete": "刪除聯繫方式",
|
||||
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
|
||||
"postCommentAdd": "撰寫一條評論",
|
||||
"translate": "翻譯",
|
||||
"translating": "正在翻譯……",
|
||||
"translated": "已翻譯",
|
||||
"settingsAutoTranslate": "自動翻譯",
|
||||
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。",
|
||||
"trayMenuHide": "隱藏",
|
||||
"accountSettingsNotify": "通知設置",
|
||||
"accountSettingsNotifyDescription": "調整你所收到的通知種類。",
|
||||
"accountSettingsSecurity": "安全設置",
|
||||
"accountSettingsSecurityDescription": "調整你的帳戶安全設置。",
|
||||
"save": "保存",
|
||||
"notificationTopicPostFeedback": "帖子數據反饋",
|
||||
"notificationTopicPostReply": "帖子回覆",
|
||||
"notificationTopicPostSubscription": "帖子訂閱",
|
||||
"notificationTopicMessaging": "消息",
|
||||
"notificationTopicMessagingCall": "通話",
|
||||
"notificationTopicGeneral": "雜項",
|
||||
"authMaximumAuthSteps": "最大驗證步驟",
|
||||
"authMaximumAuthStepsDescription": {
|
||||
"one": "登入時最多要求 {} 步驗證",
|
||||
"other": "登入時最多要求 {} 步驗證"
|
||||
},
|
||||
"authAlwaysRisky": "總是風險",
|
||||
"authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。",
|
||||
"chatUnjoined": "未加入頻道",
|
||||
"chatUnjoinedDescription": "你沒有加入這個頻道,所以你也無法發送消息或者查看這個頻道中的消息。",
|
||||
"chatUnjoinedPublicDescription": "但幸運的是,這是一個公開頻道,所以你可以主動加入。",
|
||||
"chatJoin": "加入頻道",
|
||||
"appInitStarting": "啟動中",
|
||||
"appInitNetwork": "正在初始化網絡",
|
||||
"appInitUserdata": "正在初始化用戶數據",
|
||||
"appInitWebsocket": "正在建立 Solar Link",
|
||||
"appInitNotification": "正在初始化推送通知",
|
||||
"appInitKeyPair": "正在初始化密鑰對",
|
||||
"appInitStickers": "正在初始化貼圖包",
|
||||
"appInitUserDirectory": "正在初始化用戶目錄",
|
||||
"appInitRealm": "正在初始化領域信息",
|
||||
"appInitChat": "正在初始化聊天",
|
||||
"appInitDone": "完成",
|
||||
"community": "社區",
|
||||
"realmCommunity": "{}的社區",
|
||||
"postTotalCount": {
|
||||
"zero": "沒有帖子",
|
||||
"one": "共 {} 條帖子"
|
||||
},
|
||||
"settingsHideBottomNav": "隱藏底部導航欄",
|
||||
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
|
||||
"reCaptcha": "人機驗證"
|
||||
}
|
||||
|
1
drift_schemas/my_database/drift_schema_v4.json
Normal file
1
drift_schemas/my_database/drift_schema_v4.json
Normal file
File diff suppressed because one or more lines are too long
@ -126,8 +126,6 @@ PODS:
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- geolocator_apple (1.2.0):
|
||||
- Flutter
|
||||
- GoogleAppMeasurement (11.8.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.8.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -185,7 +183,7 @@ PODS:
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- Kingfisher (8.2.0)
|
||||
- livekit_client (2.4.0):
|
||||
- livekit_client (2.4.1):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@ -234,6 +232,8 @@ PODS:
|
||||
- sqlite3/common
|
||||
- sqlite3/fts5 (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/math (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/perf-threadsafe (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/rtree (3.49.1):
|
||||
@ -244,6 +244,7 @@ PODS:
|
||||
- sqlite3 (~> 3.49.1)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/math
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- SwiftyGif (5.4.5)
|
||||
@ -278,7 +279,6 @@ DEPENDENCIES:
|
||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`)
|
||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
@ -362,8 +362,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||
gal:
|
||||
:path: ".symlinks/plugins/gal/darwin"
|
||||
geolocator_apple:
|
||||
:path: ".symlinks/plugins/geolocator_apple/ios"
|
||||
home_widget:
|
||||
:path: ".symlinks/plugins/home_widget/ios"
|
||||
image_picker_ios:
|
||||
@ -436,7 +434,6 @@ SPEC CHECKSUMS:
|
||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1
|
||||
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
@ -444,7 +441,7 @@ SPEC CHECKSUMS:
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
|
||||
livekit_client: 9819ebc8be8ef00ed0fae7d806bf8938ec689573
|
||||
livekit_client: 170022ce5f7c8c70d7f862ac9c17e11508ad5fbc
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
@ -463,7 +460,7 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
||||
sqlite3_flutter_libs: 487032b9008b28de37c72a3aa66849ef3745f3e6
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
||||
|
@ -79,6 +79,8 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
@ -6,10 +6,12 @@ import 'package:surface/database/attachment.dart';
|
||||
import 'package:surface/database/chat.dart';
|
||||
import 'package:surface/database/database.steps.dart';
|
||||
import 'package:surface/database/keypair.dart';
|
||||
import 'package:surface/database/realm.dart';
|
||||
import 'package:surface/database/sticker.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@ -22,12 +24,13 @@ part 'database.g.dart';
|
||||
SnLocalAttachment,
|
||||
SnLocalSticker,
|
||||
SnLocalStickerPack,
|
||||
SnLocalRealm,
|
||||
])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 3;
|
||||
int get schemaVersion => 4;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
@ -49,6 +52,10 @@ class AppDatabase extends _$AppDatabase {
|
||||
// Nothing else to do here
|
||||
}, from2To3: (m, schema) async {
|
||||
// Nothing else to do here, too
|
||||
}, from3To4: (m, schema) async {
|
||||
m.createTable(schema.snLocalRealm);
|
||||
m.createIndex(schema.idxRealmAccount);
|
||||
m.createIndex(schema.idxRealmAlias);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -2454,6 +2454,351 @@ class SnLocalStickerPackCompanion
|
||||
}
|
||||
}
|
||||
|
||||
class $SnLocalRealmTable extends SnLocalRealm
|
||||
with TableInfo<$SnLocalRealmTable, SnLocalRealmData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$SnLocalRealmTable(this.attachedDatabase, [this._alias]);
|
||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
@override
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
static const VerificationMeta _aliasMeta = const VerificationMeta('alias');
|
||||
@override
|
||||
late final GeneratedColumn<String> alias = GeneratedColumn<String>(
|
||||
'alias', aliasedName, false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'));
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<SnRealm, String> content =
|
||||
GeneratedColumn<String>('content', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true)
|
||||
.withConverter<SnRealm>($SnLocalRealmTable.$convertercontent);
|
||||
static const VerificationMeta _accountIdMeta =
|
||||
const VerificationMeta('accountId');
|
||||
@override
|
||||
late final GeneratedColumn<int> accountId = GeneratedColumn<int>(
|
||||
'account_id', aliasedName, false,
|
||||
type: DriftSqlType.int, requiredDuringInsert: true);
|
||||
static const VerificationMeta _createdAtMeta =
|
||||
const VerificationMeta('createdAt');
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||
'created_at', aliasedName, false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: currentDateAndTime);
|
||||
static const VerificationMeta _cacheExpiredAtMeta =
|
||||
const VerificationMeta('cacheExpiredAt');
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> cacheExpiredAt =
|
||||
GeneratedColumn<DateTime>('cache_expired_at', aliasedName, false,
|
||||
type: DriftSqlType.dateTime, requiredDuringInsert: true);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns =>
|
||||
[id, alias, content, accountId, createdAt, cacheExpiredAt];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'sn_local_realm';
|
||||
@override
|
||||
VerificationContext validateIntegrity(Insertable<SnLocalRealmData> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
if (data.containsKey('alias')) {
|
||||
context.handle(
|
||||
_aliasMeta, alias.isAcceptableOrUnknown(data['alias']!, _aliasMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_aliasMeta);
|
||||
}
|
||||
if (data.containsKey('account_id')) {
|
||||
context.handle(_accountIdMeta,
|
||||
accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_accountIdMeta);
|
||||
}
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||
}
|
||||
if (data.containsKey('cache_expired_at')) {
|
||||
context.handle(
|
||||
_cacheExpiredAtMeta,
|
||||
cacheExpiredAt.isAcceptableOrUnknown(
|
||||
data['cache_expired_at']!, _cacheExpiredAtMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_cacheExpiredAtMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
SnLocalRealmData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return SnLocalRealmData(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
alias: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}alias'])!,
|
||||
content: $SnLocalRealmTable.$convertercontent.fromSql(attachedDatabase
|
||||
.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}content'])!),
|
||||
accountId: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}account_id'])!,
|
||||
createdAt: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||
cacheExpiredAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime, data['${effectivePrefix}cache_expired_at'])!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$SnLocalRealmTable createAlias(String alias) {
|
||||
return $SnLocalRealmTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static JsonTypeConverter2<SnRealm, String, Map<String, Object?>>
|
||||
$convertercontent = const SnRealmConverter();
|
||||
}
|
||||
|
||||
class SnLocalRealmData extends DataClass
|
||||
implements Insertable<SnLocalRealmData> {
|
||||
final int id;
|
||||
final String alias;
|
||||
final SnRealm content;
|
||||
final int accountId;
|
||||
final DateTime createdAt;
|
||||
final DateTime cacheExpiredAt;
|
||||
const SnLocalRealmData(
|
||||
{required this.id,
|
||||
required this.alias,
|
||||
required this.content,
|
||||
required this.accountId,
|
||||
required this.createdAt,
|
||||
required this.cacheExpiredAt});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<int>(id);
|
||||
map['alias'] = Variable<String>(alias);
|
||||
{
|
||||
map['content'] =
|
||||
Variable<String>($SnLocalRealmTable.$convertercontent.toSql(content));
|
||||
}
|
||||
map['account_id'] = Variable<int>(accountId);
|
||||
map['created_at'] = Variable<DateTime>(createdAt);
|
||||
map['cache_expired_at'] = Variable<DateTime>(cacheExpiredAt);
|
||||
return map;
|
||||
}
|
||||
|
||||
SnLocalRealmCompanion toCompanion(bool nullToAbsent) {
|
||||
return SnLocalRealmCompanion(
|
||||
id: Value(id),
|
||||
alias: Value(alias),
|
||||
content: Value(content),
|
||||
accountId: Value(accountId),
|
||||
createdAt: Value(createdAt),
|
||||
cacheExpiredAt: Value(cacheExpiredAt),
|
||||
);
|
||||
}
|
||||
|
||||
factory SnLocalRealmData.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return SnLocalRealmData(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
alias: serializer.fromJson<String>(json['alias']),
|
||||
content: $SnLocalRealmTable.$convertercontent
|
||||
.fromJson(serializer.fromJson<Map<String, Object?>>(json['content'])),
|
||||
accountId: serializer.fromJson<int>(json['accountId']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
cacheExpiredAt: serializer.fromJson<DateTime>(json['cacheExpiredAt']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'alias': serializer.toJson<String>(alias),
|
||||
'content': serializer.toJson<Map<String, Object?>>(
|
||||
$SnLocalRealmTable.$convertercontent.toJson(content)),
|
||||
'accountId': serializer.toJson<int>(accountId),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'cacheExpiredAt': serializer.toJson<DateTime>(cacheExpiredAt),
|
||||
};
|
||||
}
|
||||
|
||||
SnLocalRealmData copyWith(
|
||||
{int? id,
|
||||
String? alias,
|
||||
SnRealm? content,
|
||||
int? accountId,
|
||||
DateTime? createdAt,
|
||||
DateTime? cacheExpiredAt}) =>
|
||||
SnLocalRealmData(
|
||||
id: id ?? this.id,
|
||||
alias: alias ?? this.alias,
|
||||
content: content ?? this.content,
|
||||
accountId: accountId ?? this.accountId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
cacheExpiredAt: cacheExpiredAt ?? this.cacheExpiredAt,
|
||||
);
|
||||
SnLocalRealmData copyWithCompanion(SnLocalRealmCompanion data) {
|
||||
return SnLocalRealmData(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
alias: data.alias.present ? data.alias.value : this.alias,
|
||||
content: data.content.present ? data.content.value : this.content,
|
||||
accountId: data.accountId.present ? data.accountId.value : this.accountId,
|
||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
cacheExpiredAt: data.cacheExpiredAt.present
|
||||
? data.cacheExpiredAt.value
|
||||
: this.cacheExpiredAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('SnLocalRealmData(')
|
||||
..write('id: $id, ')
|
||||
..write('alias: $alias, ')
|
||||
..write('content: $content, ')
|
||||
..write('accountId: $accountId, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('cacheExpiredAt: $cacheExpiredAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(id, alias, content, accountId, createdAt, cacheExpiredAt);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is SnLocalRealmData &&
|
||||
other.id == this.id &&
|
||||
other.alias == this.alias &&
|
||||
other.content == this.content &&
|
||||
other.accountId == this.accountId &&
|
||||
other.createdAt == this.createdAt &&
|
||||
other.cacheExpiredAt == this.cacheExpiredAt);
|
||||
}
|
||||
|
||||
class SnLocalRealmCompanion extends UpdateCompanion<SnLocalRealmData> {
|
||||
final Value<int> id;
|
||||
final Value<String> alias;
|
||||
final Value<SnRealm> content;
|
||||
final Value<int> accountId;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<DateTime> cacheExpiredAt;
|
||||
const SnLocalRealmCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.alias = const Value.absent(),
|
||||
this.content = const Value.absent(),
|
||||
this.accountId = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.cacheExpiredAt = const Value.absent(),
|
||||
});
|
||||
SnLocalRealmCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
required String alias,
|
||||
required SnRealm content,
|
||||
required int accountId,
|
||||
this.createdAt = const Value.absent(),
|
||||
required DateTime cacheExpiredAt,
|
||||
}) : alias = Value(alias),
|
||||
content = Value(content),
|
||||
accountId = Value(accountId),
|
||||
cacheExpiredAt = Value(cacheExpiredAt);
|
||||
static Insertable<SnLocalRealmData> custom({
|
||||
Expression<int>? id,
|
||||
Expression<String>? alias,
|
||||
Expression<String>? content,
|
||||
Expression<int>? accountId,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<DateTime>? cacheExpiredAt,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (alias != null) 'alias': alias,
|
||||
if (content != null) 'content': content,
|
||||
if (accountId != null) 'account_id': accountId,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
if (cacheExpiredAt != null) 'cache_expired_at': cacheExpiredAt,
|
||||
});
|
||||
}
|
||||
|
||||
SnLocalRealmCompanion copyWith(
|
||||
{Value<int>? id,
|
||||
Value<String>? alias,
|
||||
Value<SnRealm>? content,
|
||||
Value<int>? accountId,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<DateTime>? cacheExpiredAt}) {
|
||||
return SnLocalRealmCompanion(
|
||||
id: id ?? this.id,
|
||||
alias: alias ?? this.alias,
|
||||
content: content ?? this.content,
|
||||
accountId: accountId ?? this.accountId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
cacheExpiredAt: cacheExpiredAt ?? this.cacheExpiredAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<int>(id.value);
|
||||
}
|
||||
if (alias.present) {
|
||||
map['alias'] = Variable<String>(alias.value);
|
||||
}
|
||||
if (content.present) {
|
||||
map['content'] = Variable<String>(
|
||||
$SnLocalRealmTable.$convertercontent.toSql(content.value));
|
||||
}
|
||||
if (accountId.present) {
|
||||
map['account_id'] = Variable<int>(accountId.value);
|
||||
}
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
if (cacheExpiredAt.present) {
|
||||
map['cache_expired_at'] = Variable<DateTime>(cacheExpiredAt.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('SnLocalRealmCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('alias: $alias, ')
|
||||
..write('content: $content, ')
|
||||
..write('accountId: $accountId, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('cacheExpiredAt: $cacheExpiredAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
_$AppDatabase(QueryExecutor e) : super(e);
|
||||
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
||||
@ -2470,6 +2815,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
late final $SnLocalStickerTable snLocalSticker = $SnLocalStickerTable(this);
|
||||
late final $SnLocalStickerPackTable snLocalStickerPack =
|
||||
$SnLocalStickerPackTable(this);
|
||||
late final $SnLocalRealmTable snLocalRealm = $SnLocalRealmTable(this);
|
||||
late final Index idxChannelAlias = Index('idx_channel_alias',
|
||||
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
|
||||
late final Index idxChatChannel = Index('idx_chat_channel',
|
||||
@ -2480,6 +2826,10 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
|
||||
late final Index idxAttachmentAccount = Index('idx_attachment_account',
|
||||
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
|
||||
late final Index idxRealmAlias = Index('idx_realm_alias',
|
||||
'CREATE INDEX idx_realm_alias ON sn_local_realm (alias)');
|
||||
late final Index idxRealmAccount = Index('idx_realm_account',
|
||||
'CREATE INDEX idx_realm_account ON sn_local_realm (account_id)');
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@ -2493,11 +2843,14 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
snLocalAttachment,
|
||||
snLocalSticker,
|
||||
snLocalStickerPack,
|
||||
snLocalRealm,
|
||||
idxChannelAlias,
|
||||
idxChatChannel,
|
||||
idxAccountName,
|
||||
idxAttachmentRid,
|
||||
idxAttachmentAccount
|
||||
idxAttachmentAccount,
|
||||
idxRealmAlias,
|
||||
idxRealmAccount
|
||||
];
|
||||
}
|
||||
|
||||
@ -3888,6 +4241,192 @@ typedef $$SnLocalStickerPackTableProcessedTableManager = ProcessedTableManager<
|
||||
),
|
||||
SnLocalStickerPackData,
|
||||
PrefetchHooks Function()>;
|
||||
typedef $$SnLocalRealmTableCreateCompanionBuilder = SnLocalRealmCompanion
|
||||
Function({
|
||||
Value<int> id,
|
||||
required String alias,
|
||||
required SnRealm content,
|
||||
required int accountId,
|
||||
Value<DateTime> createdAt,
|
||||
required DateTime cacheExpiredAt,
|
||||
});
|
||||
typedef $$SnLocalRealmTableUpdateCompanionBuilder = SnLocalRealmCompanion
|
||||
Function({
|
||||
Value<int> id,
|
||||
Value<String> alias,
|
||||
Value<SnRealm> content,
|
||||
Value<int> accountId,
|
||||
Value<DateTime> createdAt,
|
||||
Value<DateTime> cacheExpiredAt,
|
||||
});
|
||||
|
||||
class $$SnLocalRealmTableFilterComposer
|
||||
extends Composer<_$AppDatabase, $SnLocalRealmTable> {
|
||||
$$SnLocalRealmTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnFilters<int> get id => $composableBuilder(
|
||||
column: $table.id, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<String> get alias => $composableBuilder(
|
||||
column: $table.alias, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnWithTypeConverterFilters<SnRealm, SnRealm, String> get content =>
|
||||
$composableBuilder(
|
||||
column: $table.content,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||
|
||||
ColumnFilters<int> get accountId => $composableBuilder(
|
||||
column: $table.accountId, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<DateTime> get cacheExpiredAt => $composableBuilder(
|
||||
column: $table.cacheExpiredAt,
|
||||
builder: (column) => ColumnFilters(column));
|
||||
}
|
||||
|
||||
class $$SnLocalRealmTableOrderingComposer
|
||||
extends Composer<_$AppDatabase, $SnLocalRealmTable> {
|
||||
$$SnLocalRealmTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnOrderings<int> get id => $composableBuilder(
|
||||
column: $table.id, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get alias => $composableBuilder(
|
||||
column: $table.alias, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get content => $composableBuilder(
|
||||
column: $table.content, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<int> get accountId => $composableBuilder(
|
||||
column: $table.accountId, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<DateTime> get cacheExpiredAt => $composableBuilder(
|
||||
column: $table.cacheExpiredAt,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
}
|
||||
|
||||
class $$SnLocalRealmTableAnnotationComposer
|
||||
extends Composer<_$AppDatabase, $SnLocalRealmTable> {
|
||||
$$SnLocalRealmTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
GeneratedColumn<int> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get alias =>
|
||||
$composableBuilder(column: $table.alias, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<SnRealm, String> get content =>
|
||||
$composableBuilder(column: $table.content, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get accountId =>
|
||||
$composableBuilder(column: $table.accountId, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get cacheExpiredAt => $composableBuilder(
|
||||
column: $table.cacheExpiredAt, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$SnLocalRealmTableTableManager extends RootTableManager<
|
||||
_$AppDatabase,
|
||||
$SnLocalRealmTable,
|
||||
SnLocalRealmData,
|
||||
$$SnLocalRealmTableFilterComposer,
|
||||
$$SnLocalRealmTableOrderingComposer,
|
||||
$$SnLocalRealmTableAnnotationComposer,
|
||||
$$SnLocalRealmTableCreateCompanionBuilder,
|
||||
$$SnLocalRealmTableUpdateCompanionBuilder,
|
||||
(
|
||||
SnLocalRealmData,
|
||||
BaseReferences<_$AppDatabase, $SnLocalRealmTable, SnLocalRealmData>
|
||||
),
|
||||
SnLocalRealmData,
|
||||
PrefetchHooks Function()> {
|
||||
$$SnLocalRealmTableTableManager(_$AppDatabase db, $SnLocalRealmTable table)
|
||||
: super(TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
$$SnLocalRealmTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
$$SnLocalRealmTableOrderingComposer($db: db, $table: table),
|
||||
createComputedFieldComposer: () =>
|
||||
$$SnLocalRealmTableAnnotationComposer($db: db, $table: table),
|
||||
updateCompanionCallback: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
Value<String> alias = const Value.absent(),
|
||||
Value<SnRealm> content = const Value.absent(),
|
||||
Value<int> accountId = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<DateTime> cacheExpiredAt = const Value.absent(),
|
||||
}) =>
|
||||
SnLocalRealmCompanion(
|
||||
id: id,
|
||||
alias: alias,
|
||||
content: content,
|
||||
accountId: accountId,
|
||||
createdAt: createdAt,
|
||||
cacheExpiredAt: cacheExpiredAt,
|
||||
),
|
||||
createCompanionCallback: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
required String alias,
|
||||
required SnRealm content,
|
||||
required int accountId,
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
required DateTime cacheExpiredAt,
|
||||
}) =>
|
||||
SnLocalRealmCompanion.insert(
|
||||
id: id,
|
||||
alias: alias,
|
||||
content: content,
|
||||
accountId: accountId,
|
||||
createdAt: createdAt,
|
||||
cacheExpiredAt: cacheExpiredAt,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: null,
|
||||
));
|
||||
}
|
||||
|
||||
typedef $$SnLocalRealmTableProcessedTableManager = ProcessedTableManager<
|
||||
_$AppDatabase,
|
||||
$SnLocalRealmTable,
|
||||
SnLocalRealmData,
|
||||
$$SnLocalRealmTableFilterComposer,
|
||||
$$SnLocalRealmTableOrderingComposer,
|
||||
$$SnLocalRealmTableAnnotationComposer,
|
||||
$$SnLocalRealmTableCreateCompanionBuilder,
|
||||
$$SnLocalRealmTableUpdateCompanionBuilder,
|
||||
(
|
||||
SnLocalRealmData,
|
||||
BaseReferences<_$AppDatabase, $SnLocalRealmTable, SnLocalRealmData>
|
||||
),
|
||||
SnLocalRealmData,
|
||||
PrefetchHooks Function()>;
|
||||
|
||||
class $AppDatabaseManager {
|
||||
final _$AppDatabase _db;
|
||||
@ -3908,4 +4447,6 @@ class $AppDatabaseManager {
|
||||
$$SnLocalStickerTableTableManager(_db, _db.snLocalSticker);
|
||||
$$SnLocalStickerPackTableTableManager get snLocalStickerPack =>
|
||||
$$SnLocalStickerPackTableTableManager(_db, _db.snLocalStickerPack);
|
||||
$$SnLocalRealmTableTableManager get snLocalRealm =>
|
||||
$$SnLocalRealmTableTableManager(_db, _db.snLocalRealm);
|
||||
}
|
||||
|
@ -412,9 +412,214 @@ class Shape8 extends i0.VersionedTable {
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
final class Schema4 extends i0.VersionedSchema {
|
||||
Schema4({required super.database}) : super(version: 4);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
snLocalChatChannel,
|
||||
snLocalChatMessage,
|
||||
snLocalChannelMember,
|
||||
snLocalKeyPair,
|
||||
snLocalAccount,
|
||||
snLocalAttachment,
|
||||
snLocalSticker,
|
||||
snLocalStickerPack,
|
||||
snLocalRealm,
|
||||
idxChannelAlias,
|
||||
idxChatChannel,
|
||||
idxAccountName,
|
||||
idxAttachmentRid,
|
||||
idxAttachmentAccount,
|
||||
idxRealmAlias,
|
||||
idxRealmAccount,
|
||||
];
|
||||
late final Shape0 snLocalChatChannel = Shape0(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_chat_channel',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape3 snLocalChatMessage = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_chat_message',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_4,
|
||||
_column_10,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape4 snLocalChannelMember = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_channel_member',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_4,
|
||||
_column_6,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape2 snLocalKeyPair = Shape2(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_key_pair',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(id)',
|
||||
],
|
||||
columns: [
|
||||
_column_5,
|
||||
_column_6,
|
||||
_column_7,
|
||||
_column_8,
|
||||
_column_9,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape5 snLocalAccount = Shape5(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_account',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_12,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape6 snLocalAttachment = Shape6(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_attachment',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_2,
|
||||
_column_6,
|
||||
_column_3,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape7 snLocalSticker = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_sticker',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_15,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape8 snLocalStickerPack = Shape8(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_sticker_pack',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape9 snLocalRealm = Shape9(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_realm',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_16,
|
||||
_column_2,
|
||||
_column_6,
|
||||
_column_3,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
final i1.Index idxChannelAlias = i1.Index('idx_channel_alias',
|
||||
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
|
||||
final i1.Index idxChatChannel = i1.Index('idx_chat_channel',
|
||||
'CREATE INDEX idx_chat_channel ON sn_local_chat_message (channel_id)');
|
||||
final i1.Index idxAccountName = i1.Index('idx_account_name',
|
||||
'CREATE INDEX idx_account_name ON sn_local_account (name)');
|
||||
final i1.Index idxAttachmentRid = i1.Index('idx_attachment_rid',
|
||||
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
|
||||
final i1.Index idxAttachmentAccount = i1.Index('idx_attachment_account',
|
||||
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
|
||||
final i1.Index idxRealmAlias = i1.Index('idx_realm_alias',
|
||||
'CREATE INDEX idx_realm_alias ON sn_local_realm (alias)');
|
||||
final i1.Index idxRealmAccount = i1.Index('idx_realm_account',
|
||||
'CREATE INDEX idx_realm_account ON sn_local_realm (account_id)');
|
||||
}
|
||||
|
||||
class Shape9 extends i0.VersionedTable {
|
||||
Shape9({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get alias =>
|
||||
columnsByName['alias']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get accountId =>
|
||||
columnsByName['account_id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
|
||||
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_16(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('alias', aliasedName, false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@ -428,6 +633,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from2To3(migrator, schema);
|
||||
return 3;
|
||||
case 3:
|
||||
final schema = Schema4(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from3To4(migrator, schema);
|
||||
return 4;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@ -437,9 +647,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||
}) =>
|
||||
i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
from2To3: from2To3,
|
||||
from3To4: from3To4,
|
||||
));
|
||||
|
45
lib/database/realm.dart
Normal file
45
lib/database/realm.dart
Normal file
@ -0,0 +1,45 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
|
||||
class SnRealmConverter extends TypeConverter<SnRealm, String>
|
||||
with JsonTypeConverter2<SnRealm, String, Map<String, Object?>> {
|
||||
const SnRealmConverter();
|
||||
|
||||
@override
|
||||
SnRealm fromSql(String fromDb) {
|
||||
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(SnRealm value) {
|
||||
return jsonEncode(toJson(value));
|
||||
}
|
||||
|
||||
@override
|
||||
SnRealm fromJson(Map<String, Object?> json) {
|
||||
return SnRealm.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson(SnRealm value) {
|
||||
return value.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
@TableIndex(name: 'idx_realm_alias', columns: {#alias})
|
||||
@TableIndex(name: 'idx_realm_account', columns: {#accountId})
|
||||
class SnLocalRealm extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
TextColumn get alias => text().unique()();
|
||||
|
||||
TextColumn get content => text().map(const SnRealmConverter())();
|
||||
|
||||
IntColumn get accountId => integer()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get cacheExpiredAt => dateTime()();
|
||||
}
|
187
lib/main.dart
187
lib/main.dart
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:math' hide log;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
@ -12,6 +13,7 @@ import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.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:package_info_plus/package_info_plus.dart';
|
||||
@ -19,6 +21,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/firebase_options.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
@ -37,6 +40,7 @@ import 'package:surface/providers/sn_realm.dart';
|
||||
import 'package:surface/providers/sn_sticker.dart';
|
||||
import 'package:surface/providers/special_day.dart';
|
||||
import 'package:surface/providers/theme.dart';
|
||||
import 'package:surface/providers/translation.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
@ -44,6 +48,8 @@ import 'package:surface/providers/widget.dart';
|
||||
import 'package:surface/router.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/menu_bar.dart';
|
||||
import 'package:surface/widgets/version_label.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
@ -166,6 +172,7 @@ class SolianApp extends StatelessWidget {
|
||||
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
|
||||
Provider(create: (ctx) => SnTranslator()),
|
||||
|
||||
// Additional helper layer
|
||||
Provider(create: (ctx) => SpecialDayProvider(ctx)),
|
||||
@ -225,6 +232,9 @@ class _AppSplashScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
bool _isBusy = false;
|
||||
String _phaseText = 'appInitStarting';
|
||||
|
||||
void _tryRequestRating() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.containsKey('first_boot_time')) {
|
||||
@ -273,7 +283,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
mounted) {
|
||||
final config = context.read<ConfigProvider>();
|
||||
config.setUpdate(
|
||||
remoteVersionString, resp.data?['body'] ?? 'No changelog');
|
||||
remoteVersionString,
|
||||
resp.data?['body'] ?? 'No changelog',
|
||||
);
|
||||
logging.info("[Update] Update available: $remoteVersionString");
|
||||
}
|
||||
} catch (e) {
|
||||
@ -282,6 +294,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
}
|
||||
}
|
||||
|
||||
void _setPhaseText(String text) {
|
||||
_phaseText = 'appInit${text.capitalize()}'.tr();
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
try {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
@ -294,31 +311,45 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
// The Network initialization must be done after the HomeWidget initialization
|
||||
// The Network initialization will save the server url to the HomeWidget
|
||||
// The Network initialization will also save initialize the Config, so it not need to be initialized again
|
||||
_setPhaseText('network');
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.initializeUserAgent();
|
||||
await sn.setConfigWithNative();
|
||||
if (!mounted) return;
|
||||
_setPhaseText('userdata');
|
||||
final ua = context.read<UserProvider>();
|
||||
await ua.initialize();
|
||||
if (!mounted) return;
|
||||
_setPhaseText('websocket');
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
await ws.tryConnect();
|
||||
if (!mounted) return;
|
||||
_setPhaseText('notification');
|
||||
final notify = context.read<NotificationProvider>();
|
||||
notify.listen();
|
||||
await notify.registerPushNotifications();
|
||||
if (!mounted) return;
|
||||
_setPhaseText('keyPair');
|
||||
final kp = context.read<KeyPairProvider>();
|
||||
await kp.reloadActive();
|
||||
kp.listen();
|
||||
if (!mounted) return;
|
||||
_setPhaseText('stickers');
|
||||
final sticker = context.read<SnStickerProvider>();
|
||||
await sticker.listSticker();
|
||||
if (!mounted) return;
|
||||
_setPhaseText('userDirectory');
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final userCacheSize = await ud.loadAccountCache();
|
||||
logging.info('[Users] Loaded local user cache, size: $userCacheSize');
|
||||
logging.info('[Bootstrap] Everything initialized!');
|
||||
await ud.loadAccountCache();
|
||||
if (!mounted) return;
|
||||
_setPhaseText('realm');
|
||||
final rm = context.read<SnRealmProvider>();
|
||||
await rm.refreshAvailableRealms();
|
||||
if (!mounted) return;
|
||||
_setPhaseText('chat');
|
||||
final ct = context.read<ChatChannelProvider>();
|
||||
await ct.refreshAvailableChannels();
|
||||
_setPhaseText('done');
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
await context.showErrorDialog(err);
|
||||
@ -331,18 +362,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
// The quit key has been removed, and the logic of the quit key is moved to system menu bar activator.
|
||||
}
|
||||
|
||||
final Menu _appTrayMenu = Menu(
|
||||
@ -405,6 +425,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_isBusy = true;
|
||||
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
||||
_appLifecycleListener = AppLifecycleListener(
|
||||
onExitRequested: _onExitRequested,
|
||||
@ -418,6 +439,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
_postInitialization();
|
||||
_tryRequestRating();
|
||||
_checkForUpdate();
|
||||
setState(() => _isBusy = false);
|
||||
});
|
||||
}
|
||||
|
||||
@ -426,6 +448,15 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
return AppExitResponse.cancel;
|
||||
}
|
||||
|
||||
void _quitApp() {
|
||||
_appLifecycleListener?.dispose();
|
||||
if (Platform.isWindows) {
|
||||
appWindow.close();
|
||||
} else {
|
||||
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconMouseDown() {
|
||||
if (Platform.isWindows) {
|
||||
@ -460,12 +491,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
Timer(const Duration(milliseconds: 100), () => appWindow.show());
|
||||
break;
|
||||
case 'exit':
|
||||
_appLifecycleListener?.dispose();
|
||||
if (Platform.isWindows) {
|
||||
appWindow.close();
|
||||
} else {
|
||||
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
||||
}
|
||||
_quitApp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -482,29 +508,110 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
return NotificationListener<SizeChangedLayoutNotification>(
|
||||
onNotification: (notification) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
cfg.calcDrawerSize(context);
|
||||
});
|
||||
return false;
|
||||
},
|
||||
child: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
return AppSystemMenuBar(
|
||||
onQuit: _quitApp,
|
||||
child: NotificationListener<SizeChangedLayoutNotification>(
|
||||
onNotification: (notification) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
cfg.calcDrawerSize(context);
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (context.mounted) {
|
||||
cfg.calcDrawerSize(context);
|
||||
}
|
||||
});
|
||||
return SizeChangedLayoutNotifier(
|
||||
child: widget.child,
|
||||
);
|
||||
return false;
|
||||
},
|
||||
child: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
cfg.calcDrawerSize(context);
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (context.mounted) {
|
||||
cfg.calcDrawerSize(context);
|
||||
}
|
||||
});
|
||||
return SizeChangedLayoutNotifier(
|
||||
child: _isBusy
|
||||
? Material(
|
||||
key: Key('app-splash-screen-$_isBusy'),
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomPaint(painter: GraphPainter()),
|
||||
Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 240,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/icon/icon.png',
|
||||
width: 64,
|
||||
height: 64,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
Text('Solar Network').bold(),
|
||||
AppVersionLabel(),
|
||||
Gap(8),
|
||||
Text(
|
||||
_phaseText,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Gap(16),
|
||||
const LinearProgressIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: widget.child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GraphPainter extends CustomPainter {
|
||||
final Random random = Random();
|
||||
final int numNodes = 20;
|
||||
final double maxDistance = 100; // Max distance to draw a line
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paintNode = Paint()..color = Colors.white;
|
||||
final paintEdge = Paint()
|
||||
..color = Colors.white.withOpacity(0.3)
|
||||
..strokeWidth = 1;
|
||||
|
||||
// Generate random points
|
||||
List<Offset> nodes = List.generate(
|
||||
numNodes,
|
||||
(_) => Offset(
|
||||
random.nextDouble() * size.width,
|
||||
random.nextDouble() * size.height,
|
||||
),
|
||||
);
|
||||
|
||||
// Draw edges between close nodes
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
for (var j = i + 1; j < nodes.length; j++) {
|
||||
double distance = (nodes[i] - nodes[j]).distance;
|
||||
if (distance < maxDistance) {
|
||||
canvas.drawLine(nodes[i], nodes[j], paintEdge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw nodes
|
||||
for (var node in nodes) {
|
||||
canvas.drawCircle(node, 4, paintNode);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
@ -28,6 +28,19 @@ class ChatChannelProvider extends ChangeNotifier {
|
||||
_rels = context.read<SnRealmProvider>();
|
||||
}
|
||||
|
||||
final List<SnChannel> _availableChannels = List.empty(growable: true);
|
||||
|
||||
List<SnChannel> get availableChannels => _availableChannels;
|
||||
|
||||
Future<void> refreshAvailableChannels() async {
|
||||
final stream = fetchChannels();
|
||||
stream.listen((ele) {
|
||||
_availableChannels.clear();
|
||||
_availableChannels.addAll(ele);
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
|
||||
await Future.wait(
|
||||
channels.map(
|
||||
|
@ -19,6 +19,9 @@ const kAppExpandPostLink = 'app_expand_post_link';
|
||||
const kAppExpandChatLink = 'app_expand_chat_link';
|
||||
const kAppRealmCompactView = 'app_realm_compact_view';
|
||||
const kAppCustomFonts = 'app_custom_fonts';
|
||||
const kAppMixedFeed = 'app_mixed_feed';
|
||||
const kAppAutoTranslate = 'app_auto_translate';
|
||||
const kAppHideBottomNav = 'app_hide_bottom_nav';
|
||||
|
||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||
'settingsImageQualityLowest': FilterQuality.none,
|
||||
@ -81,8 +84,36 @@ class ConfigProvider extends ChangeNotifier {
|
||||
return prefs.getBool(kAppRealmCompactView) ?? false;
|
||||
}
|
||||
|
||||
bool get mixedFeed {
|
||||
return prefs.getBool(kAppMixedFeed) ?? true;
|
||||
}
|
||||
|
||||
bool get autoTranslate {
|
||||
return prefs.getBool(kAppAutoTranslate) ?? false;
|
||||
}
|
||||
|
||||
bool get hideBottomNav {
|
||||
return prefs.getBool(kAppHideBottomNav) ?? false;
|
||||
}
|
||||
|
||||
set hideBottomNav(bool value) {
|
||||
prefs.setBool(kAppHideBottomNav, value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
set autoTranslate(bool value) {
|
||||
prefs.setBool(kAppAutoTranslate, value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
set mixedFeed(bool value) {
|
||||
prefs.setBool(kAppMixedFeed, value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
set realmCompactView(bool value) {
|
||||
prefs.setBool(kAppRealmCompactView, value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
set serverUrl(String url) {
|
||||
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
|
||||
class AppNavDestination {
|
||||
final String label;
|
||||
@ -24,13 +25,10 @@ class NavigationProvider extends ChangeNotifier {
|
||||
|
||||
int? get currentIndex => _currentIndex;
|
||||
|
||||
static const List<String> kShowBottomNavScreen = [
|
||||
'home',
|
||||
'explore',
|
||||
'account',
|
||||
'album',
|
||||
'chat',
|
||||
];
|
||||
List<String> get showBottomNavScreen => destinations
|
||||
.where((ele) => ele.isPinned)
|
||||
.map((ele) => ele.screen)
|
||||
.toList();
|
||||
|
||||
static const List<AppNavDestination> kAllDestination = [
|
||||
AppNavDestination(
|
||||
@ -88,7 +86,7 @@ class NavigationProvider extends ChangeNotifier {
|
||||
'home',
|
||||
'explore',
|
||||
'chat',
|
||||
'account',
|
||||
'realm',
|
||||
];
|
||||
|
||||
List<AppNavDestination> destinations = [];
|
||||
@ -143,4 +141,11 @@ class NavigationProvider extends ChangeNotifier {
|
||||
_currentIndex = idx;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
SnRealm? focusedRealm;
|
||||
|
||||
void setFocusedRealm(SnRealm? realm) {
|
||||
focusedRealm = realm;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -145,6 +145,36 @@ class SnPostContentProvider {
|
||||
return out;
|
||||
}
|
||||
|
||||
Future<List<SnFeedEntry>> getFeed({int take = 20, DateTime? cursor}) async {
|
||||
final resp =
|
||||
await _sn.client.get('/cgi/co/recommendations/feed', queryParameters: {
|
||||
'take': take,
|
||||
if (cursor != null) 'cursor': cursor.toUtc().millisecondsSinceEpoch,
|
||||
});
|
||||
final List<SnFeedEntry> out =
|
||||
List.from(resp.data.map((ele) => SnFeedEntry.fromJson(ele)));
|
||||
|
||||
List<SnPost> posts = List.empty(growable: true);
|
||||
for (var idx = 0; idx < out.length; idx++) {
|
||||
final ele = out[idx];
|
||||
if (ele.type == 'interactive.post') {
|
||||
posts.add(SnPost.fromJson(ele.data));
|
||||
}
|
||||
}
|
||||
posts = await _preloadRelatedDataInBatch(posts);
|
||||
|
||||
var postsIdx = 0;
|
||||
for (var idx = 0; idx < out.length; idx++) {
|
||||
final ele = out[idx];
|
||||
if (ele.type == 'interactive.post') {
|
||||
out[idx] = ele.copyWith(data: posts[postsIdx].toJson());
|
||||
postsIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Future<(List<SnPost>, int)> listPosts({
|
||||
int take = 10,
|
||||
int offset = 0,
|
||||
|
@ -321,13 +321,13 @@ class SnAttachmentProvider {
|
||||
uuid: ele.uuid,
|
||||
content: ele,
|
||||
accountId: ele.accountId,
|
||||
cacheExpiredAt: DateTime.now().add(const Duration(days: 7)),
|
||||
cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
|
||||
),
|
||||
onConflict: DoUpdate(
|
||||
(_) => SnLocalAttachmentCompanion.custom(
|
||||
content: Constant(jsonEncode(ele.toJson())),
|
||||
cacheExpiredAt:
|
||||
Constant(DateTime.now().add(const Duration(days: 7))),
|
||||
Constant(DateTime.now().add(const Duration(hours: 1))),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -17,6 +17,20 @@ import 'package:synchronized/synchronized.dart';
|
||||
import 'package:talker_dio_logger/talker_dio_logger_interceptor.dart';
|
||||
import 'package:talker_dio_logger/talker_dio_logger_settings.dart';
|
||||
|
||||
enum ServiceStatus { operational, downgraded, failed }
|
||||
|
||||
const Map<String, String> kServicesName = {
|
||||
'ai': 'Insights',
|
||||
'co': 'Interactive',
|
||||
're': 'Reader',
|
||||
'im': 'Messaging',
|
||||
'ma': 'Matrix',
|
||||
'uc': 'Paperclip',
|
||||
'wa': 'Wallet',
|
||||
'id': 'Passport',
|
||||
'pusher': 'Pusher',
|
||||
};
|
||||
|
||||
const kNetworkServerDirectory = [
|
||||
('Solar Network', 'https://api.sn.solsynth.dev'),
|
||||
('Local', 'http://localhost:8001'),
|
||||
|
@ -1,16 +1,30 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/database/database.dart';
|
||||
import 'package:surface/providers/database.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
|
||||
class SnRealmProvider {
|
||||
late final SnNetworkProvider _sn;
|
||||
late final DatabaseProvider _dt;
|
||||
|
||||
SnRealmProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
_dt = context.read<DatabaseProvider>();
|
||||
}
|
||||
|
||||
final Map<String, SnRealm> _cache = {};
|
||||
List<SnRealm> _availableRealms = List.empty(growable: true);
|
||||
|
||||
Future<void> refreshAvailableRealms() async {
|
||||
_availableRealms = await listAvailableRealms();
|
||||
}
|
||||
|
||||
List<SnRealm> get availableRealms => _availableRealms;
|
||||
|
||||
Future<List<SnRealm>> listAvailableRealms() async {
|
||||
final resp = await _sn.client.get('/cgi/id/realms/me/available');
|
||||
@ -21,6 +35,7 @@ class SnRealmProvider {
|
||||
_cache[realm.alias] = realm;
|
||||
_cache[realm.id.toString()] = realm;
|
||||
}
|
||||
_saveToLocal(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
@ -28,10 +43,43 @@ class SnRealmProvider {
|
||||
if (_cache.containsKey(aliasOrId.toString())) {
|
||||
return _cache[aliasOrId.toString()]!;
|
||||
}
|
||||
final localResp = await (_dt.db.snLocalRealm.select()
|
||||
..where((e) =>
|
||||
e.id.equals(aliasOrId is int ? aliasOrId : 0) |
|
||||
e.alias.equals(aliasOrId.toString()))
|
||||
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())))
|
||||
.getSingleOrNull();
|
||||
if (localResp != null) {
|
||||
_cache[localResp.content.id.toString()] = localResp.content;
|
||||
_cache[localResp.content.alias] = localResp.content;
|
||||
return localResp.content;
|
||||
}
|
||||
final resp = await _sn.client.get('/cgi/id/realms/$aliasOrId');
|
||||
final out = SnRealm.fromJson(resp.data);
|
||||
_cache[out.alias] = out;
|
||||
_cache[out.id.toString()] = out;
|
||||
_saveToLocal([out]);
|
||||
return out;
|
||||
}
|
||||
|
||||
Future<void> _saveToLocal(Iterable<SnRealm> out) async {
|
||||
for (final ele in out) {
|
||||
await _dt.db.snLocalRealm.insertOne(
|
||||
SnLocalRealmCompanion.insert(
|
||||
id: Value(ele.id),
|
||||
alias: ele.alias,
|
||||
content: ele,
|
||||
accountId: ele.accountId,
|
||||
cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
|
||||
),
|
||||
onConflict: DoUpdate(
|
||||
(_) => SnLocalRealmCompanion.custom(
|
||||
content: Constant(jsonEncode(ele.toJson())),
|
||||
cacheExpiredAt:
|
||||
Constant(DateTime.now().add(const Duration(hours: 1))),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
56
lib/providers/translation.dart
Normal file
56
lib/providers/translation.dart
Normal file
@ -0,0 +1,56 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
|
||||
// TODO self host translate api
|
||||
const kTranslateApiBaseUrl = 'https://translate.disroot.org';
|
||||
|
||||
class SnTranslator {
|
||||
final Dio client = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: kTranslateApiBaseUrl,
|
||||
connectTimeout: Duration(seconds: 3),
|
||||
sendTimeout: Duration(seconds: 3),
|
||||
receiveTimeout: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
|
||||
final Map<String, String> _cache = {};
|
||||
|
||||
Future<String> translate(
|
||||
String text, {
|
||||
required String to,
|
||||
String from = 'auto',
|
||||
bool skipCache = false,
|
||||
}) async {
|
||||
if (text.isEmpty) return text;
|
||||
|
||||
final cacheKey = md5.convert(utf8.encode('$text$from$to')).toString();
|
||||
if (!skipCache && _cache.containsKey(cacheKey)) {
|
||||
return _cache[cacheKey]!;
|
||||
}
|
||||
|
||||
logging.info('[Translator] Translate $text from $from to $to');
|
||||
|
||||
final resp = await client.post(
|
||||
'/translate',
|
||||
data: {
|
||||
'q': text,
|
||||
'source': from,
|
||||
'target': to,
|
||||
'format': 'text',
|
||||
},
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
final out = resp.data['translatedText'];
|
||||
if (out.isNotEmpty) {
|
||||
logging.info('[Translator] Translated $text from $from to $to');
|
||||
_cache[cacheKey] = out;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
throw Exception('translate failed: $resp');
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ class UserDirectoryProvider {
|
||||
|
||||
Future<List<SnAccount?>> listAccount(Iterable<dynamic> id) async {
|
||||
// In-memory cache
|
||||
if (_cacheExpiredAt != null && _cacheExpiredAt!.isAfter(DateTime.now())) {
|
||||
if (_cacheExpiredAt != null && _cacheExpiredAt!.isBefore(DateTime.now())) {
|
||||
_cache.clear();
|
||||
_cacheExpiredAt = DateTime.now().add(const Duration(hours: 1));
|
||||
} else {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -35,6 +37,32 @@ class UserProvider extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> get atkClaims async {
|
||||
final tk = (await atk);
|
||||
if (tk == null) return null;
|
||||
final atkParts = tk.split('.');
|
||||
if (atkParts.length != 3) {
|
||||
throw Exception('invalid format of access token');
|
||||
}
|
||||
|
||||
var rawPayload = atkParts[1].replaceAll('-', '+').replaceAll('_', '/');
|
||||
switch (rawPayload.length % 4) {
|
||||
case 0:
|
||||
break;
|
||||
case 2:
|
||||
rawPayload += '==';
|
||||
break;
|
||||
case 3:
|
||||
rawPayload += '=';
|
||||
break;
|
||||
default:
|
||||
throw Exception('illegal format of access token payload');
|
||||
}
|
||||
|
||||
final b64 = utf8.fuse(base64Url);
|
||||
return jsonDecode(b64.decode(rawPayload));
|
||||
}
|
||||
|
||||
Future<SnAccount?> refreshUser() async {
|
||||
final resp = await _sn.client.get('/cgi/id/users/me');
|
||||
final out = SnAccount.fromJson(resp.data);
|
||||
@ -47,7 +75,13 @@ class UserProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void logoutUser() async {
|
||||
_sn.clearTokenPair();
|
||||
atkClaims.then((value) async {
|
||||
if (value != null) {
|
||||
await _sn.client.delete('/cgi/id/users/me/tickets/${value['sed']}');
|
||||
logging.info('[Auth] Current session has been destroyed.');
|
||||
}
|
||||
_sn.clearTokenPair();
|
||||
});
|
||||
isAuthorized = false;
|
||||
user = null;
|
||||
notifyListeners();
|
||||
|
@ -4,14 +4,19 @@ 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/action_events.dart';
|
||||
import 'package:surface/screens/account/badges.dart';
|
||||
import 'package:surface/screens/account/contact_methods.dart';
|
||||
import 'package:surface/screens/account/factor_settings.dart';
|
||||
import 'package:surface/screens/account/keypairs.dart';
|
||||
import 'package:surface/screens/account/prefs/notify.dart';
|
||||
import 'package:surface/screens/account/prefs/security.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
import 'package:surface/screens/account/profile_edit.dart';
|
||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||
import 'package:surface/screens/account/publishers/publisher_new.dart';
|
||||
import 'package:surface/screens/account/publishers/publishers.dart';
|
||||
import 'package:surface/screens/account/auth_tickets.dart';
|
||||
import 'package:surface/screens/album.dart';
|
||||
import 'package:surface/screens/auth/login.dart';
|
||||
import 'package:surface/screens/auth/register.dart';
|
||||
@ -34,6 +39,7 @@ import 'package:surface/screens/post/post_shuffle.dart';
|
||||
import 'package:surface/screens/post/publisher_page.dart';
|
||||
import 'package:surface/screens/post/post_search.dart';
|
||||
import 'package:surface/screens/realm.dart';
|
||||
import 'package:surface/screens/realm/community.dart';
|
||||
import 'package:surface/screens/realm/manage.dart';
|
||||
import 'package:surface/screens/realm/realm_detail.dart';
|
||||
import 'package:surface/screens/realm/realm_discovery.dart';
|
||||
@ -124,6 +130,21 @@ final _appRoutes = [
|
||||
name: 'account',
|
||||
builder: (context, state) => const AccountScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/contacts',
|
||||
name: 'accountContactMethods',
|
||||
builder: (context, state) => const AccountContactMethod(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/events',
|
||||
name: 'accountActionEvents',
|
||||
builder: (context, state) => const ActionEventScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/tickets',
|
||||
name: 'accountAuthTickets',
|
||||
builder: (context, state) => const AccountAuthTicket(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/badges',
|
||||
name: 'accountBadges',
|
||||
@ -143,6 +164,18 @@ final _appRoutes = [
|
||||
path: '/settings',
|
||||
name: 'accountSettings',
|
||||
builder: (context, state) => AccountSettingsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/notify',
|
||||
name: 'accountSettingsNotify',
|
||||
builder: (context, state) => const AccountNotifyPrefsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth',
|
||||
name: 'accountSettingsSecurity',
|
||||
builder: (context, state) => const AccountSecurityPrefsScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings/factors',
|
||||
@ -172,7 +205,7 @@ final _appRoutes = [
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:name',
|
||||
path: '/profile/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
@ -227,6 +260,13 @@ final _appRoutes = [
|
||||
child: const RealmScreen(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:alias/community',
|
||||
name: 'realmCommunity',
|
||||
builder: (context, state) => RealmCommunityScreen(
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manage',
|
||||
name: 'realmManage',
|
||||
|
@ -207,6 +207,26 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
GoRouter.of(context).pushNamed('accountKeyPairs');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountActionEvent').tr(),
|
||||
subtitle: Text('accountActionEventDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.history),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountActionEvents');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountAuthTickets').tr(),
|
||||
subtitle: Text('accountAuthTicketsDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.confirmation_number),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountAuthTickets');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountSettings').tr(),
|
||||
subtitle: Text('accountSettingsSubtitle').tr(),
|
||||
|
@ -87,6 +87,36 @@ class AccountSettingsScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountContactMethods').tr(),
|
||||
subtitle: Text('accountContactMethodsDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.contacts),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountContactMethods');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountSettingsNotify').tr(),
|
||||
subtitle: Text('accountSettingsNotifyDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.notifications),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountSettingsNotify');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountSettingsSecurity').tr(),
|
||||
subtitle: Text('accountSettingsSecurityDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.shield),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountSettingsSecurity');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountProfileEdit').tr(),
|
||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||
|
160
lib/screens/account/action_events.dart
Normal file
160
lib/screens/account/action_events.dart
Normal file
@ -0,0 +1,160 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:timelines_plus/timelines_plus.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class ActionEventScreen extends StatefulWidget {
|
||||
const ActionEventScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ActionEventScreen> createState() => _ActionEventScreenState();
|
||||
}
|
||||
|
||||
class _ActionEventScreenState extends State<ActionEventScreen> {
|
||||
bool _isBusy = false;
|
||||
int? _totalCount;
|
||||
final List<SnActionEvent> _actionEvents = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchActionEvents() async {
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get(
|
||||
'/cgi/id/users/me/events',
|
||||
queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _actionEvents.length,
|
||||
},
|
||||
);
|
||||
_totalCount = resp.data['count'];
|
||||
_actionEvents.addAll(
|
||||
(resp.data['data'] as List<dynamic>)
|
||||
.map((e) => SnActionEvent.fromJson(e)),
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchActionEvents();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountActionEvent').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_totalCount = null;
|
||||
return _fetchActionEvents();
|
||||
},
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.only(left: 20, right: 8),
|
||||
itemCount: _actionEvents.length,
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax:
|
||||
_totalCount != null && _actionEvents.length >= _totalCount!,
|
||||
onFetchData: _fetchActionEvents,
|
||||
itemBuilder: (context, idx) {
|
||||
final event = _actionEvents[idx];
|
||||
return TimelineTile(
|
||||
nodeAlign: TimelineNodeAlign.start,
|
||||
contents: Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
event.type,
|
||||
maxLines: 1,
|
||||
style: GoogleFonts.robotoMono(),
|
||||
),
|
||||
if (event.ipAddress.isNotEmpty)
|
||||
Text(
|
||||
event.ipAddress,
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
if (event.location?.isNotEmpty ?? false)
|
||||
Text(event.location!),
|
||||
Row(
|
||||
children: [
|
||||
Text(DateFormat()
|
||||
.format(event.createdAt.toLocal()))
|
||||
.fontSize(12),
|
||||
Text(' · ')
|
||||
.fontSize(12)
|
||||
.padding(horizontal: 4),
|
||||
Text(RelativeTime(context)
|
||||
.format(event.createdAt.toLocal()))
|
||||
.fontSize(12),
|
||||
],
|
||||
).opacity(0.75).padding(top: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (event.metadata != null)
|
||||
ExpansionTile(
|
||||
minTileHeight: 40,
|
||||
tilePadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
title: Text('eventMetadata').tr(),
|
||||
expandedAlignment: Alignment.topLeft,
|
||||
expandedCrossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
JsonEncoder.withIndent('\t')
|
||||
.convert(event.metadata),
|
||||
style: GoogleFonts.robotoMono(),
|
||||
).padding(vertical: 8, horizontal: 16),
|
||||
],
|
||||
).padding(bottom: 6),
|
||||
],
|
||||
),
|
||||
),
|
||||
node: TimelineNode(
|
||||
indicator: DotIndicator(),
|
||||
startConnector: SolidLineConnector(),
|
||||
endConnector: SolidLineConnector(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
186
lib/screens/account/auth_tickets.dart
Normal file
186
lib/screens/account/auth_tickets.dart
Normal file
@ -0,0 +1,186 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/auth.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';
|
||||
|
||||
const Map<String, IconData> kAuthTicketIcon = {
|
||||
'ios': Symbols.ios,
|
||||
'android': Symbols.android,
|
||||
'macos': Symbols.computer,
|
||||
'windows nt': Symbols.laptop_windows,
|
||||
'linux': Symbols.laptop,
|
||||
};
|
||||
|
||||
class AccountAuthTicket extends StatefulWidget {
|
||||
const AccountAuthTicket({super.key});
|
||||
|
||||
@override
|
||||
State<AccountAuthTicket> createState() => _AccountAuthTicketState();
|
||||
}
|
||||
|
||||
class _AccountAuthTicketState extends State<AccountAuthTicket> {
|
||||
bool _isBusy = false;
|
||||
int? _totalCount;
|
||||
final List<SnAuthTicket> _authTickets = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchAuthTickets() async {
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get(
|
||||
'/cgi/id/users/me/tickets',
|
||||
queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _authTickets.length,
|
||||
},
|
||||
);
|
||||
_totalCount = resp.data['count'];
|
||||
_authTickets.addAll(
|
||||
(resp.data['data'] as List<dynamic>)
|
||||
.map((e) => SnAuthTicket.fromJson(e)),
|
||||
);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteAuthTicket(SnAuthTicket ticket) async {
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete(
|
||||
'/cgi/id/users/me/tickets/${ticket.id}',
|
||||
);
|
||||
setState(() {
|
||||
_authTickets.remove(ticket);
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
int? _currentTicketId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchAuthTickets();
|
||||
|
||||
final ua = context.read<UserProvider>();
|
||||
ua.atkClaims.then((value) {
|
||||
if (value == null) return;
|
||||
_currentTicketId = int.parse(value['sed']);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountAuthTickets').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_totalCount = null;
|
||||
return _fetchAuthTickets();
|
||||
},
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.zero,
|
||||
onFetchData: _fetchAuthTickets,
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax:
|
||||
_totalCount != null && _authTickets.length >= _totalCount!,
|
||||
itemCount: _authTickets.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final ticket = _authTickets[idx];
|
||||
final platform = RegExp(r'\(([^;]+);')
|
||||
.firstMatch(ticket.userAgent)
|
||||
?.group(1);
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
kAuthTicketIcon[platform!.toLowerCase()] ?? Symbols.web,
|
||||
),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
ticket.ipAddress,
|
||||
style: TextStyle(fontSize: 15),
|
||||
),
|
||||
Text(ticket.userAgent).opacity(0.8),
|
||||
if (ticket.location?.isNotEmpty ?? false)
|
||||
const Gap(4),
|
||||
if (ticket.location?.isNotEmpty ?? false)
|
||||
Text(ticket.location!).opacity(0.8),
|
||||
const Gap(4),
|
||||
Text('authTicketCreatedAt'.tr(args: [
|
||||
(DateFormat().format(ticket.createdAt.toLocal()))
|
||||
])).fontSize(12).opacity(0.75),
|
||||
if (ticket.expiredAt != null)
|
||||
Text('authTicketExpiredAt'.tr(args: [
|
||||
(DateFormat()
|
||||
.format(ticket.expiredAt!.toLocal()))
|
||||
])).fontSize(12).opacity(0.75),
|
||||
if (ticket.lastGrantAt != null)
|
||||
Text('authTicketLastGrantAt'.tr(args: [
|
||||
(DateFormat()
|
||||
.format(ticket.lastGrantAt!.toLocal()))
|
||||
])).fontSize(12).opacity(0.75),
|
||||
const Gap(4),
|
||||
if (_currentTicketId == ticket.id)
|
||||
Text('authTicketCurrent'.tr())
|
||||
.fontSize(11)
|
||||
.bold()
|
||||
.opacity(0.75),
|
||||
Text('#${ticket.id}').fontSize(11).opacity(0.75),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
iconSize: 20,
|
||||
visualDensity:
|
||||
VisualDensity(horizontal: -4, vertical: -4),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Symbols.logout),
|
||||
onPressed: _currentTicketId == ticket.id
|
||||
? null
|
||||
: () {
|
||||
_deleteAuthTicket(ticket);
|
||||
},
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
322
lib/screens/account/contact_methods.dart
Normal file
322
lib/screens/account/contact_methods.dart
Normal file
@ -0,0 +1,322 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
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:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
const kContactMethodsIcons = [Symbols.email, Symbols.phone, Symbols.map];
|
||||
const kContactMethodsName = ['Email', 'Phone', 'Address'];
|
||||
|
||||
class AccountContactMethod extends StatefulWidget {
|
||||
const AccountContactMethod({super.key});
|
||||
|
||||
@override
|
||||
State<AccountContactMethod> createState() => _AccountContactMethodState();
|
||||
}
|
||||
|
||||
class _AccountContactMethodState extends State<AccountContactMethod> {
|
||||
bool _isBusy = false;
|
||||
List<SnAccountContact> _contactMethods = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchContactMethods() async {
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/me/contacts');
|
||||
_contactMethods = List.from((resp.data as List<dynamic>)
|
||||
.map((e) => SnAccountContact.fromJson(e)));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteContactMethod(SnAccountContact contact) async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'accountContactMethodsDelete'.tr(),
|
||||
'accountContactMethodsDeleteDescription'.tr(args: [contact.content]),
|
||||
);
|
||||
if (!confirm || !mounted) return;
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/id/users/me/contacts/${contact.id}');
|
||||
if (!mounted) return;
|
||||
await _fetchContactMethods();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchContactMethods();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountContactMethods').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
ListTile(
|
||||
title: Text('accountContactMethodsAdd').tr(),
|
||||
subtitle: Text('accountContactMethodsAddDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.add),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _ContactMethodEditor(),
|
||||
).then((value) {
|
||||
if (value) {
|
||||
_fetchContactMethods();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
Divider(height: 1),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchContactMethods,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: _contactMethods.length,
|
||||
itemBuilder: (context, index) {
|
||||
final method = _contactMethods[index];
|
||||
return ListTile(
|
||||
title: Text(method.content),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'accountContactMethodsName${kContactMethodsName[method.type]}',
|
||||
).tr().bold(),
|
||||
if (method.isPrimary ||
|
||||
method.isPublic ||
|
||||
method.verifiedAt != null)
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
if (method.isPrimary)
|
||||
Text('accountContactMethodsPrimary').tr(),
|
||||
if (method.isPublic)
|
||||
Text('accountContactMethodsPublic').tr(),
|
||||
if (method.verifiedAt != null)
|
||||
Text('accountContactMethodsVerified').tr(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: Icon(
|
||||
kContactMethodsIcons[method.type],
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const Gap(16),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _ContactMethodEditor(
|
||||
contact: method,
|
||||
),
|
||||
).then((value) {
|
||||
if (value) {
|
||||
_fetchContactMethods();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete),
|
||||
const Gap(16),
|
||||
Text('delete'.tr()),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
_deleteContactMethod(method);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ContactMethodEditor extends StatefulWidget {
|
||||
final SnAccountContact? contact;
|
||||
const _ContactMethodEditor({this.contact});
|
||||
|
||||
@override
|
||||
State<_ContactMethodEditor> createState() => _ContactMethodEditorState();
|
||||
}
|
||||
|
||||
class _ContactMethodEditorState extends State<_ContactMethodEditor> {
|
||||
int _type = 0;
|
||||
bool _isPublic = false;
|
||||
final TextEditingController _contentController = TextEditingController();
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _saveContactMethod() async {
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.request(
|
||||
widget.contact == null
|
||||
? '/cgi/id/users/me/contacts'
|
||||
: '/cgi/id/users/me/contacts/${widget.contact!.id}',
|
||||
data: {
|
||||
'content': _contentController.text,
|
||||
'type': _type,
|
||||
'is_public': _isPublic,
|
||||
},
|
||||
options: Options(
|
||||
method: widget.contact == null ? 'POST' : 'PUT',
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.contact != null) {
|
||||
_type = widget.contact!.type;
|
||||
_isPublic = widget.contact!.isPublic;
|
||||
_contentController.text = widget.contact!.content;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: widget.contact == null
|
||||
? Text('accountContactMethodsAdd').tr()
|
||||
: Text('accountContactMethodsEdit').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<int>(
|
||||
value: _type,
|
||||
items: kContactMethodsName
|
||||
.mapIndexed((idx, ele) => DropdownMenuItem<int>(
|
||||
value: idx,
|
||||
child: Text('accountContactMethodsName$ele').tr(),
|
||||
))
|
||||
.toList(),
|
||||
buttonStyleData: ButtonStyleData(
|
||||
height: 48,
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 48,
|
||||
padding: EdgeInsets.only(left: 14, right: 14),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() => _type = value ?? 0);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: _contentController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'fieldContactContent'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(8),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: CheckboxListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
title: Text('accountContactMethodsPublic').tr(),
|
||||
subtitle: Text('accountContactMethodsPublicHint').tr(),
|
||||
secondary: const Icon(Symbols.globe),
|
||||
value: _isPublic,
|
||||
onChanged: (value) {
|
||||
setState(() => _isPublic = value ?? false);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text('dialogDismiss').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () {
|
||||
_saveContactMethod();
|
||||
},
|
||||
child: Text('dialogConfirm').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
122
lib/screens/account/prefs/notify.dart
Normal file
122
lib/screens/account/prefs/notify.dart
Normal file
@ -0,0 +1,122 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
final Map<String, String> kNotifyTopicMap = {
|
||||
'interactive.reply': 'notificationTopicPostReply'.tr(),
|
||||
'interactive.feedback': 'notificationTopicPostFeedback'.tr(),
|
||||
'interactive.subscription': 'notificationTopicPostSubscription'.tr(),
|
||||
'messaging.message': 'notificationTopicMessaging'.tr(),
|
||||
'messaging.call': 'notificationTopicMessagingCall'.tr(),
|
||||
'general': 'notificationTopicGeneral'.tr(),
|
||||
};
|
||||
|
||||
class AccountNotifyPrefsScreen extends StatefulWidget {
|
||||
const AccountNotifyPrefsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AccountNotifyPrefsScreen> createState() =>
|
||||
_AccountNotifyPrefsScreenState();
|
||||
}
|
||||
|
||||
class _AccountNotifyPrefsScreenState extends State<AccountNotifyPrefsScreen> {
|
||||
bool _isBusy = true;
|
||||
|
||||
Map<String, bool> _config = {};
|
||||
|
||||
Future<void> _getPreferences() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
try {
|
||||
final resp = await sn.client.get('/cgi/id/preferences/notifications');
|
||||
_config = resp.data['config']
|
||||
.map((k, v) => MapEntry(k, v as bool))
|
||||
.cast<String, bool>();
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _savePreferences() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
try {
|
||||
await sn.client.put(
|
||||
'/cgi/id/preferences/notifications',
|
||||
data: {
|
||||
'config': _config,
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('accountSettingsApplied'.tr());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getPreferences();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountSettingsNotify').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.save),
|
||||
title: Text('save').tr(),
|
||||
enabled: !_isBusy,
|
||||
onTap: () {
|
||||
_savePreferences();
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: kNotifyTopicMap.length,
|
||||
itemBuilder: (context, index) {
|
||||
final element = kNotifyTopicMap.entries.elementAt(index);
|
||||
return CheckboxListTile(
|
||||
title: Text(element.value),
|
||||
subtitle: Text(
|
||||
element.key,
|
||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||
),
|
||||
value: _config[element.key] ?? true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_config[element.key] = value ?? false;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
147
lib/screens/account/prefs/security.dart
Normal file
147
lib/screens/account/prefs/security.dart
Normal file
@ -0,0 +1,147 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class AccountSecurityPrefsScreen extends StatefulWidget {
|
||||
const AccountSecurityPrefsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AccountSecurityPrefsScreen> createState() =>
|
||||
_AccountSecurityPrefsScreenState();
|
||||
}
|
||||
|
||||
class _AccountSecurityPrefsScreenState
|
||||
extends State<AccountSecurityPrefsScreen> {
|
||||
bool _isBusy = true;
|
||||
|
||||
Map<String, dynamic> _config = {
|
||||
'maximum_auth_steps': 2,
|
||||
'always_risky': false,
|
||||
};
|
||||
|
||||
Future<void> _getPreferences() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
try {
|
||||
final resp = await sn.client.get('/cgi/id/preferences/auth');
|
||||
_config = resp.data['config']
|
||||
.map((k, v) => MapEntry(k, v as bool))
|
||||
.cast<String, bool>();
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _savePreferences() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
try {
|
||||
await sn.client.put(
|
||||
'/cgi/id/preferences/auth',
|
||||
data: {
|
||||
'config': _config,
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('accountSettingsApplied'.tr());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getPreferences();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountSettingsSecurity').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.save),
|
||||
title: Text('save').tr(),
|
||||
enabled: !_isBusy,
|
||||
onTap: () {
|
||||
_savePreferences();
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text('authMaximumAuthSteps').tr(),
|
||||
subtitle: Text('authMaximumAuthStepsDescription')
|
||||
.plural(_config['maximum_auth_steps'] ?? 2),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
icon: const Icon(Symbols.remove),
|
||||
onPressed: () {
|
||||
if (_config['maximum_auth_steps'] > 1) {
|
||||
setState(() => _config['maximum_auth_steps']--);
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
icon: const Icon(Symbols.add),
|
||||
onPressed: () {
|
||||
if (_config['maximum_auth_steps'] < 99) {
|
||||
setState(() => _config['maximum_auth_steps']++);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: Text('authAlwaysRisky').tr(),
|
||||
subtitle: Text('authAlwaysRiskyDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
value: _config['always_risky'] ?? false,
|
||||
onChanged: (value) {
|
||||
setState(() => _config['always_risky'] = value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ 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/screens/captcha.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
@ -33,10 +34,20 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
final username = _usernameController.value.text;
|
||||
final nickname = _nicknameController.value.text;
|
||||
final password = _passwordController.value.text;
|
||||
if (email.isEmpty || username.isEmpty || nickname.isEmpty || password.isEmpty) {
|
||||
if (email.isEmpty ||
|
||||
username.isEmpty ||
|
||||
nickname.isEmpty ||
|
||||
password.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TurnstileScreen(),
|
||||
),
|
||||
);
|
||||
if (captchaTk == null) return;
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/id/users', data: {
|
||||
@ -45,6 +56,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'language': EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
'captcha_token': captchaTk,
|
||||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
@ -91,8 +103,11 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
children: [
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 4 || value.length > 32) {
|
||||
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
||||
if (value == null ||
|
||||
value.length < 4 ||
|
||||
value.length > 32) {
|
||||
return 'fieldUsernameLengthLimit'
|
||||
.tr(args: [4.toString(), 32.toString()]);
|
||||
}
|
||||
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
||||
return 'fieldUsernameAlphanumOnly'.tr();
|
||||
@ -108,13 +123,17 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldUsername'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 4 || value.length > 32) {
|
||||
return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
||||
if (value == null ||
|
||||
value.length < 4 ||
|
||||
value.length > 32) {
|
||||
return 'fieldNicknameLengthLimit'
|
||||
.tr(args: [4.toString(), 32.toString()]);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@ -127,7 +146,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldNickname'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextFormField(
|
||||
@ -149,7 +169,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldEmail'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextFormField(
|
||||
@ -169,7 +190,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldPassword'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 7),
|
||||
@ -186,9 +208,13 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
Text(
|
||||
'termAcceptNextWithAgree'.tr(),
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
||||
),
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha((255 * 0.75).round()),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
|
38
lib/screens/captcha.dart
Normal file
38
lib/screens/captcha.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class TurnstileScreen extends StatefulWidget {
|
||||
const TurnstileScreen({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TurnstileScreen> createState() => _TurnstileScreenState();
|
||||
}
|
||||
|
||||
class _TurnstileScreenState extends State<TurnstileScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: Text("reCaptcha").tr()),
|
||||
body: InAppWebView(
|
||||
initialUrlRequest: URLRequest(
|
||||
url: WebUri('${cfg.serverUrl}/captcha?redirect_uri=solink://captcha'),
|
||||
),
|
||||
shouldOverrideUrlLoading: (controller, navigationAction) async {
|
||||
Uri? url = navigationAction.request.url;
|
||||
if (url != null && url.queryParameters.containsKey('captcha_tk')) {
|
||||
Navigator.pop(context, url.queryParameters['captcha_tk']!);
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
}
|
||||
return NavigationActionPolicy.ALLOW;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -52,8 +52,10 @@ class ChatRoomScreen extends StatefulWidget {
|
||||
class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
bool _isBusy = false;
|
||||
bool _isCalling = false;
|
||||
bool _isJoining = false;
|
||||
|
||||
SnChannel? _channel;
|
||||
SnChannelMember? _currentMember;
|
||||
SnChannelMember? _otherMember;
|
||||
SnChatCall? _ongoingCall;
|
||||
|
||||
@ -67,7 +69,24 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
|
||||
StreamSubscription? _wsSubscription;
|
||||
|
||||
// TODO fetch user identity and ask them to join the channel or not
|
||||
Future<void> _joinChannel() async {
|
||||
try {
|
||||
setState(() => _isJoining = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client
|
||||
.post('/cgi/im/channels/${_channel!.keyPath}/members', data: {
|
||||
'related': ua.user?.name,
|
||||
});
|
||||
_initializeChat();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isJoining = true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchChannel() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
@ -76,6 +95,12 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
_channel = await chan.getChannel('${widget.scope}:${widget.alias}');
|
||||
|
||||
if (!mounted || _channel == null) return;
|
||||
final ct = context.read<ChatChannelProvider>();
|
||||
try {
|
||||
_currentMember = await ct.getChannelProfile(_channel!);
|
||||
} catch (_) {}
|
||||
|
||||
if (!mounted || _currentMember == null) return;
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
if (_channel!.type == 1) {
|
||||
@ -204,11 +229,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_messageController = ChatMessageController(context);
|
||||
Future<void> _initializeChat() async {
|
||||
_fetchChannel().then((_) async {
|
||||
if (_currentMember == null) return;
|
||||
await _messageController.initialize(_channel!);
|
||||
|
||||
if (widget.extra != null) {
|
||||
@ -230,6 +253,13 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
_fetchOngoingCall(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_messageController = ChatMessageController(context);
|
||||
_initializeChat();
|
||||
|
||||
_wsSubscription = _ws.pk.stream.listen((event) {
|
||||
switch (event.method) {
|
||||
@ -281,25 +311,27 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
: _channel?.name ?? 'loading'.tr(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() => _isEncrypted = !_isEncrypted);
|
||||
_inputGlobalKey.currentState?.setEncrypted(_isEncrypted);
|
||||
},
|
||||
icon: _isEncrypted
|
||||
? const Icon(Symbols.lock)
|
||||
: const Icon(Symbols.no_encryption),
|
||||
),
|
||||
IconButton(
|
||||
icon: _ongoingCall == null
|
||||
? const Icon(Symbols.call)
|
||||
: const Icon(Symbols.call_end),
|
||||
onPressed: _isCalling
|
||||
? null
|
||||
: _ongoingCall == null
|
||||
? _makeCall
|
||||
: _endCall,
|
||||
),
|
||||
if (_currentMember != null)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() => _isEncrypted = !_isEncrypted);
|
||||
_inputGlobalKey.currentState?.setEncrypted(_isEncrypted);
|
||||
},
|
||||
icon: _isEncrypted
|
||||
? const Icon(Symbols.lock)
|
||||
: const Icon(Symbols.no_encryption),
|
||||
),
|
||||
if (_currentMember != null)
|
||||
IconButton(
|
||||
icon: _ongoingCall == null
|
||||
? const Icon(Symbols.call)
|
||||
: const Icon(Symbols.call_end),
|
||||
onPressed: _isCalling
|
||||
? null
|
||||
: _ongoingCall == null
|
||||
? _makeCall
|
||||
: _endCall,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.more_vert),
|
||||
onPressed: () {
|
||||
@ -348,7 +380,41 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
).height(_ongoingCall != null ? 54 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.fastLinearToSlowEaseIn),
|
||||
if (_messageController.isPending)
|
||||
if (_currentMember == null && !_isBusy)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Symbols.person_remove, size: 40, fill: 1),
|
||||
const Gap(8),
|
||||
Text('chatUnjoined'.tr(), textAlign: TextAlign.center)
|
||||
.fontSize(16)
|
||||
.bold(),
|
||||
Text('chatUnjoinedDescription'.tr(),
|
||||
textAlign: TextAlign.center)
|
||||
.fontSize(13),
|
||||
if (_channel!.isPublic)
|
||||
Text('chatUnjoinedPublicDescription'.tr(),
|
||||
textAlign: TextAlign.center)
|
||||
.fontSize(13)
|
||||
.padding(top: 8),
|
||||
if (_channel!.isPublic)
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
onPressed: _isJoining ? null : _joinChannel,
|
||||
child: Text('chatJoin').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (_messageController.isPending)
|
||||
Expanded(
|
||||
child: const CircularProgressIndicator().center(),
|
||||
)
|
||||
@ -403,7 +469,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!_messageController.isPending)
|
||||
if (!_messageController.isPending && _currentMember != null)
|
||||
Material(
|
||||
elevation: 2,
|
||||
child: Column(
|
||||
|
@ -5,16 +5,22 @@ import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_realm.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/feed/feed_news.dart';
|
||||
import 'package:surface/widgets/feed/feed_unknown.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/fediverse_post_item.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
@ -75,6 +81,8 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
|
||||
Future<void> _fetchRealms() async {
|
||||
try {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) return;
|
||||
final rels = context.read<SnRealmProvider>();
|
||||
final out = await rels.listAvailableRealms();
|
||||
setState(() {
|
||||
@ -147,6 +155,7 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
return AppScaffold(
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: ExpandableFab(
|
||||
@ -219,10 +228,15 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
SliverOverlapAbsorber(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: SliverAppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
leading:
|
||||
ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
||||
? AutoAppBarLeading()
|
||||
: null,
|
||||
titleSpacing: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
|
||||
const Gap(8),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.shuffle),
|
||||
onPressed: () {
|
||||
@ -263,6 +277,14 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
}
|
||||
});
|
||||
},
|
||||
onMixedFeedChanged: (flag) {
|
||||
_listKey.currentState?.setRealm(null);
|
||||
_listKey.currentState?.setCategory(null);
|
||||
if (_showCategories && flag) {
|
||||
_toggleShowCategories();
|
||||
}
|
||||
_listKey.currentState?.refreshPosts();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -286,9 +308,11 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onPressed: () {
|
||||
_toggleShowCategories();
|
||||
},
|
||||
onPressed: cfg.mixedFeed
|
||||
? null
|
||||
: () {
|
||||
_toggleShowCategories();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.search),
|
||||
@ -298,74 +322,78 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
bottom: TabBar(
|
||||
isScrollable: _showCategories,
|
||||
controller: _tabController,
|
||||
tabs: _showCategories
|
||||
? [
|
||||
for (final category in _categories)
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
kCategoryIcons[category.alias] ??
|
||||
Symbols.question_mark,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor!,
|
||||
),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'postCategory${category.alias.capitalize()}'
|
||||
.trExists()
|
||||
? 'postCategory${category.alias.capitalize()}'
|
||||
.tr()
|
||||
: category.name,
|
||||
maxLines: 1,
|
||||
).textColor(
|
||||
Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor!,
|
||||
bottom: cfg.mixedFeed
|
||||
? null
|
||||
: TabBar(
|
||||
isScrollable: _showCategories,
|
||||
controller: _tabController,
|
||||
tabs: _showCategories
|
||||
? [
|
||||
for (final category in _categories)
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
kCategoryIcons[category.alias] ??
|
||||
Symbols.question_mark,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor!,
|
||||
),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'postCategory${category.alias.capitalize()}'
|
||||
.trExists()
|
||||
? 'postCategory${category.alias.capitalize()}'
|
||||
.tr()
|
||||
: category.name,
|
||||
maxLines: 1,
|
||||
).textColor(
|
||||
Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
: [
|
||||
for (final channel in kPostChannels)
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
kPostChannelIcons[
|
||||
kPostChannels.indexOf(channel)],
|
||||
size: 20,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor,
|
||||
),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'postChannel$channel',
|
||||
maxLines: 1,
|
||||
).tr().textColor(
|
||||
Theme.of(context)
|
||||
]
|
||||
: [
|
||||
for (final channel in kPostChannels)
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
kPostChannelIcons[
|
||||
kPostChannels.indexOf(channel)],
|
||||
size: 20,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor,
|
||||
),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'postChannel$channel',
|
||||
maxLines: 1,
|
||||
).tr().textColor(
|
||||
Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
@ -390,21 +418,22 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
|
||||
SnRealm? get realm => _selectedRealm;
|
||||
|
||||
final List<SnPost> _posts = List.empty(growable: true);
|
||||
final List<SnFeedEntry> _feed = List.empty(growable: true);
|
||||
SnRealm? _selectedRealm;
|
||||
String? _selectedChannel;
|
||||
SnPostCategory? _selectedCategory;
|
||||
int? _postCount;
|
||||
bool _hasLoadedAll = false;
|
||||
|
||||
// Called when using regular feed
|
||||
Future<void> _fetchPosts() async {
|
||||
if (_postCount != null && _posts.length >= _postCount!) return;
|
||||
if (_hasLoadedAll) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final pt = context.read<SnPostContentProvider>();
|
||||
final result = await pt.listPosts(
|
||||
take: 10,
|
||||
offset: _posts.length,
|
||||
offset: _feed.length,
|
||||
categories: _selectedCategory != null ? [_selectedCategory!.alias] : null,
|
||||
channel: _selectedChannel,
|
||||
realm: _selectedRealm?.alias,
|
||||
@ -413,8 +442,36 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
_postCount = result.$2;
|
||||
_posts.addAll(out);
|
||||
final postCount = result.$2;
|
||||
_feed.addAll(
|
||||
out.map((ele) => SnFeedEntry(
|
||||
type: 'interactive.post',
|
||||
data: ele.toJson(),
|
||||
createdAt: ele.createdAt)),
|
||||
);
|
||||
_hasLoadedAll = _feed.length >= postCount;
|
||||
|
||||
if (mounted) setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
// Called when mixed feed is enabled
|
||||
Future<void> _fetchFeed() async {
|
||||
if (_hasLoadedAll) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final pt = context.read<SnPostContentProvider>();
|
||||
final result = await pt.getFeed(
|
||||
cursor: _feed
|
||||
.where((ele) => !['reader.news'].contains(ele.type))
|
||||
.lastOrNull
|
||||
?.createdAt,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
_feed.addAll(result);
|
||||
_hasLoadedAll = result.isEmpty;
|
||||
|
||||
if (mounted) setState(() => _isBusy = false);
|
||||
}
|
||||
@ -435,77 +492,81 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
}
|
||||
|
||||
Future<void> refreshPosts() {
|
||||
_postCount = null;
|
||||
_posts.clear();
|
||||
return _fetchPosts();
|
||||
_hasLoadedAll = false;
|
||||
_feed.clear();
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
if (cfg.mixedFeed) {
|
||||
return _fetchFeed();
|
||||
} else {
|
||||
return _fetchPosts();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchPosts();
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
if (cfg.mixedFeed) {
|
||||
_fetchFeed();
|
||||
} else {
|
||||
_fetchPosts();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
if (_selectedCategory != null)
|
||||
MaterialBanner(
|
||||
content: Text(
|
||||
'postFilterWithCategory'.tr(args: [
|
||||
'postCategory${_selectedCategory!.alias.capitalize()}'.trExists()
|
||||
? 'postCategory${_selectedCategory!.alias.capitalize()}'
|
||||
.tr()
|
||||
: _selectedCategory!.name,
|
||||
]),
|
||||
),
|
||||
leading: Icon(kCategoryIcons[_selectedCategory!.alias] ??
|
||||
Symbols.question_mark),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.clear),
|
||||
onPressed: () {
|
||||
setState(() => _selectedCategory = null);
|
||||
refreshPosts();
|
||||
},
|
||||
),
|
||||
],
|
||||
padding: const EdgeInsets.only(left: 20, right: 4),
|
||||
),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
displacement: 40 + MediaQuery.of(context).padding.top,
|
||||
onRefresh: () => refreshPosts(),
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
itemCount: _posts.length,
|
||||
isLoading: _isBusy,
|
||||
centerLoading: true,
|
||||
hasReachedMax:
|
||||
_postCount != null && _posts.length >= _postCount!,
|
||||
onFetchData: _fetchPosts,
|
||||
itemBuilder: (context, idx) {
|
||||
return OpenablePostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
refreshPosts();
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
displacement: 40 + MediaQuery.of(context).padding.top,
|
||||
onRefresh: () => refreshPosts(),
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
itemCount: _feed.length,
|
||||
isLoading: _isBusy,
|
||||
centerLoading: true,
|
||||
hasReachedMax: _hasLoadedAll,
|
||||
onFetchData: cfg.mixedFeed ? _fetchFeed : _fetchPosts,
|
||||
itemBuilder: (context, idx) {
|
||||
final ele = _feed[idx];
|
||||
switch (ele.type) {
|
||||
case 'interactive.post':
|
||||
return OpenablePostItem(
|
||||
data: SnPost.fromJson(ele.data),
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() {
|
||||
_feed[idx] = _feed[idx].copyWith(data: data.toJson());
|
||||
});
|
||||
},
|
||||
onDeleted: () {
|
||||
refreshPosts();
|
||||
},
|
||||
);
|
||||
case 'fediverse.post':
|
||||
return FediversePostWidget(
|
||||
data: SnFediversePost.fromJson(ele.data),
|
||||
maxWidth: 640,
|
||||
);
|
||||
case 'reader.news':
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: NewsFeedEntry(data: ele),
|
||||
),
|
||||
);
|
||||
default:
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: FeedUnknownEntry(data: ele),
|
||||
);
|
||||
}
|
||||
},
|
||||
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -513,54 +574,71 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
class _PostListRealmPopup extends StatelessWidget {
|
||||
final List<SnRealm>? realms;
|
||||
final Function(SnRealm?) onUpdate;
|
||||
final Function(bool) onMixedFeedChanged;
|
||||
|
||||
const _PostListRealmPopup({
|
||||
required this.realms,
|
||||
required this.onUpdate,
|
||||
required this.onMixedFeedChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.face, size: 24),
|
||||
const Icon(Symbols.tune, size: 24),
|
||||
const Gap(16),
|
||||
Text('accountRealms', style: Theme.of(context).textTheme.titleLarge)
|
||||
Text('filterFeed', style: Theme.of(context).textTheme.titleLarge)
|
||||
.tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.close),
|
||||
title: Text('postInGlobal').tr(),
|
||||
subtitle: Text('postViewInGlobalDescription').tr(),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Symbols.merge_type),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
onUpdate.call(null);
|
||||
Navigator.pop(context);
|
||||
title: Text('mixedFeed').tr(),
|
||||
subtitle: Text('mixedFeedDescription').tr(),
|
||||
value: cfg.mixedFeed,
|
||||
onChanged: (value) {
|
||||
cfg.mixedFeed = value;
|
||||
onMixedFeedChanged.call(value);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: realms?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final realm = realms![idx];
|
||||
return ListTile(
|
||||
title: Text(realm.name),
|
||||
subtitle: Text('@${realm.alias}'),
|
||||
leading: AccountImage(content: realm.avatar, radius: 18),
|
||||
onTap: () {
|
||||
onUpdate.call(realm);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
if (!cfg.mixedFeed)
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.close),
|
||||
title: Text('postInGlobal').tr(),
|
||||
subtitle: Text('postViewInGlobalDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
onUpdate.call(null);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!cfg.mixedFeed) const Divider(height: 1),
|
||||
if (!cfg.mixedFeed)
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: realms?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final realm = realms![idx];
|
||||
return ListTile(
|
||||
title: Text(realm.name),
|
||||
subtitle: Text('@${realm.alias}'),
|
||||
leading: AccountImage(content: realm.avatar, radius: 18),
|
||||
onTap: () {
|
||||
onUpdate.call(realm);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
@ -19,14 +18,16 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/special_day.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/widget.dart';
|
||||
import 'package:surface/screens/captcha.dart';
|
||||
import 'package:surface/types/check_in.dart';
|
||||
import 'package:surface/types/news.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/updater.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class HomeScreenDashEntry {
|
||||
final String name;
|
||||
@ -66,7 +67,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
),
|
||||
HomeScreenDashEntry(
|
||||
name: 'dashEntryTodayNews',
|
||||
child: _HomeDashTodayNews(),
|
||||
child: _HomeDashServiceStatus(),
|
||||
cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2,
|
||||
),
|
||||
];
|
||||
@ -99,6 +100,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
right: 8,
|
||||
),
|
||||
),
|
||||
_HomeDashUnconfirmedWidget().padding(horizontal: 8),
|
||||
_HomeDashSpecialDayWidget().padding(horizontal: 8),
|
||||
StaggeredGrid.extent(
|
||||
maxCrossAxisExtent: 280,
|
||||
@ -123,6 +125,64 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeDashUnconfirmedWidget extends StatelessWidget {
|
||||
const _HomeDashUnconfirmedWidget();
|
||||
|
||||
Future<void> _resendConfirmationEmail(BuildContext context) async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.patch('/cgi/id/users/me/confirm');
|
||||
if (!context.mounted) return;
|
||||
context.showSnackbar('accountUnconfirmedResendSuccessful'.tr());
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
if (ua.user == null || ua.user?.confirmedAt != null) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
leading: const Icon(Symbols.shield),
|
||||
title: Text('accountUnconfirmedTitle').tr(),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('accountUnconfirmedSubtitle').tr(),
|
||||
const Gap(4),
|
||||
Row(
|
||||
children: [
|
||||
Text('accountUnconfirmedUnreceived').tr(),
|
||||
const Gap(4),
|
||||
InkWell(
|
||||
child: Text(
|
||||
'accountUnconfirmedResend',
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
).tr(),
|
||||
onTap: () {
|
||||
_resendConfirmationEmail(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
),
|
||||
).padding(bottom: 8);
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeDashUpdateWidget extends StatelessWidget {
|
||||
final EdgeInsets? padding;
|
||||
|
||||
@ -131,7 +191,6 @@ class _HomeDashUpdateWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = context.watch<ConfigProvider>();
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: config,
|
||||
builder: (context, _) {
|
||||
@ -245,21 +304,31 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeDashTodayNews extends StatefulWidget {
|
||||
const _HomeDashTodayNews();
|
||||
class _HomeDashServiceStatus extends StatefulWidget {
|
||||
const _HomeDashServiceStatus();
|
||||
|
||||
@override
|
||||
State<_HomeDashTodayNews> createState() => _HomeDashTodayNewsState();
|
||||
State<_HomeDashServiceStatus> createState() => _HomeDashServiceStatusState();
|
||||
}
|
||||
|
||||
class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
||||
SnNewsArticle? _article;
|
||||
class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
|
||||
Map<String, dynamic>? _statuses;
|
||||
ServiceStatus? _serviceStatus;
|
||||
|
||||
Future<void> _fetchArticle() async {
|
||||
Future<void> _fetchStatuses() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/re/news/today');
|
||||
_article = SnNewsArticle.fromJson(resp.data['data']);
|
||||
final resp = await sn.client.get('/directory/status');
|
||||
_statuses = resp.data;
|
||||
if (_statuses!.values.contains(false)) {
|
||||
if (_statuses!.values.contains(true)) {
|
||||
_serviceStatus = ServiceStatus.downgraded;
|
||||
} else {
|
||||
_serviceStatus = ServiceStatus.failed;
|
||||
}
|
||||
} else {
|
||||
_serviceStatus = ServiceStatus.operational;
|
||||
}
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -272,7 +341,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_fetchArticle();
|
||||
_fetchStatuses();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -284,73 +353,127 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Symbols.newspaper),
|
||||
const Icon(Symbols.flare),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'newsToday',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr()
|
||||
],
|
||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||
if (_article != null)
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
_article!.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 18),
|
||||
maxLines:
|
||||
MediaQuery.of(context).size.width >= 640 ? 2 : 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
parse(_article!.description)
|
||||
.children
|
||||
.map((e) => e.text.trim())
|
||||
.join(),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(DateFormat().format(date)).textStyle(
|
||||
Theme.of(context).textTheme.bodySmall!),
|
||||
Text(' · ')
|
||||
.textStyle(Theme.of(context).textTheme.bodySmall!)
|
||||
.bold(),
|
||||
Text(RelativeTime(context).format(date)).textStyle(
|
||||
Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).opacity(0.75);
|
||||
}),
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'newsDetail',
|
||||
pathParameters: {'hash': _article!.hash},
|
||||
);
|
||||
Expanded(
|
||||
child: Text(
|
||||
'serviceStatus',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.launch, size: 20),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
launchUrlString('https://status.solsynth.dev');
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
],
|
||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 6),
|
||||
width: double.infinity,
|
||||
color: _serviceStatus == null
|
||||
? Theme.of(context).colorScheme.surfaceContainerHigh
|
||||
: switch (_serviceStatus) {
|
||||
ServiceStatus.operational => Colors.green[300],
|
||||
ServiceStatus.failed => Colors.red[300],
|
||||
_ => Colors.orange[300],
|
||||
},
|
||||
child: _serviceStatus == null
|
||||
? Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.more_horiz,
|
||||
size: 20,
|
||||
),
|
||||
const Gap(10),
|
||||
Text('loading').tr(),
|
||||
],
|
||||
)
|
||||
: switch (_serviceStatus) {
|
||||
ServiceStatus.operational => Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.check,
|
||||
size: 20,
|
||||
),
|
||||
const Gap(10),
|
||||
Text('serviceStatusOperational').tr(),
|
||||
],
|
||||
),
|
||||
ServiceStatus.failed => Tooltip(
|
||||
message: 'serviceStatusFailedDescription'.tr(),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.dangerous,
|
||||
size: 20,
|
||||
),
|
||||
const Gap(10),
|
||||
Text('serviceStatusFailed').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ => Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.error,
|
||||
size: 20,
|
||||
),
|
||||
const Gap(10),
|
||||
Text('serviceStatusDowngraded').tr(),
|
||||
],
|
||||
),
|
||||
},
|
||||
),
|
||||
if (_statuses != null)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(top: 6),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final entry in _statuses!.entries)
|
||||
Tooltip(
|
||||
message: kServicesName[entry.key] != null
|
||||
? 'serviceName${kServicesName[entry.key]}'.tr()
|
||||
: 'unknown'.tr(),
|
||||
child: Chip(
|
||||
visualDensity:
|
||||
VisualDensity(horizontal: -4, vertical: -4),
|
||||
avatar: entry.value
|
||||
? const Icon(
|
||||
Symbols.circle,
|
||||
color: Colors.green,
|
||||
fill: 1,
|
||||
size: 16,
|
||||
)
|
||||
: AnimateWidgetExtensions(const Icon(
|
||||
Symbols.error,
|
||||
color: Colors.red,
|
||||
fill: 1,
|
||||
size: 16,
|
||||
))
|
||||
.animate(onPlay: (e) => e.repeat())
|
||||
.fadeIn(
|
||||
duration: 500.ms, curve: Curves.easeOut)
|
||||
.then()
|
||||
.fadeOut(
|
||||
duration: 500.ms,
|
||||
delay: 1000.ms,
|
||||
curve: Curves.easeIn,
|
||||
),
|
||||
label: Text(kServicesName[entry.key] ?? entry.key),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -386,11 +509,20 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
}
|
||||
|
||||
Future<void> _doCheckIn() async {
|
||||
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TurnstileScreen(),
|
||||
),
|
||||
);
|
||||
if (captchaTk == null) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final home = context.read<HomeWidgetProvider>();
|
||||
final resp = await sn.client.post('/cgi/id/check-in');
|
||||
final resp = await sn.client.post('/cgi/id/check-in', data: {
|
||||
'captcha_token': captchaTk,
|
||||
});
|
||||
_todayRecord = SnCheckInRecord.fromJson(resp.data);
|
||||
await home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
|
||||
} catch (err) {
|
||||
@ -758,8 +890,10 @@ class _HomeDashRecommendationPostWidgetState
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
Text('${_currentPage + 1}/${_posts?.length ?? 0}',
|
||||
style: GoogleFonts.robotoMono())
|
||||
Text(
|
||||
'${_currentPage + 1}/${_posts?.length ?? 0}',
|
||||
style: GoogleFonts.robotoMono(),
|
||||
)
|
||||
],
|
||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||
Expanded(
|
||||
@ -777,6 +911,7 @@ class _HomeDashRecommendationPostWidgetState
|
||||
child: PostItem(
|
||||
data: _posts![index],
|
||||
showMenu: false,
|
||||
showFullPost: true,
|
||||
).padding(bottom: 8),
|
||||
onTap: () {
|
||||
GoRouter.of(context)
|
||||
|
@ -1,18 +1,17 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/news.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class NewsDetailScreen extends StatefulWidget {
|
||||
@ -26,14 +25,12 @@ class NewsDetailScreen extends StatefulWidget {
|
||||
|
||||
class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
SnNewsArticle? _article;
|
||||
dom.Document? _articleFragment;
|
||||
|
||||
Future<void> _fetchArticle() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
|
||||
_article = SnNewsArticle.fromJson(resp.data);
|
||||
_articleFragment = parse(_article!.content);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err).then((_) {
|
||||
@ -45,104 +42,6 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _parseHtmlToWidgets(Iterable<dom.Element>? elements) {
|
||||
if (elements == null) return [];
|
||||
|
||||
final List<Widget> widgets = [];
|
||||
|
||||
for (final node in elements) {
|
||||
switch (node.localName) {
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
widgets.add(Text(node.text.trim(), style: Theme.of(context).textTheme.titleMedium));
|
||||
break;
|
||||
case 'p':
|
||||
if (node.text.trim().isEmpty) continue;
|
||||
widgets.add(
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: node.text.trim(),
|
||||
children: [
|
||||
for (final child in node.children)
|
||||
switch (child.localName) {
|
||||
'a' => TextSpan(
|
||||
text: child.text.trim(),
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlString(child.attributes['href']!);
|
||||
},
|
||||
),
|
||||
_ => TextSpan(text: child.text.trim()),
|
||||
},
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'a':
|
||||
// drop single link
|
||||
break;
|
||||
case 'div':
|
||||
// ignore div text, normally it is not meaningful
|
||||
widgets.addAll(_parseHtmlToWidgets(node.children));
|
||||
break;
|
||||
case 'hr':
|
||||
widgets.add(const Divider());
|
||||
break;
|
||||
case 'img':
|
||||
var src = node.attributes['src'];
|
||||
if (src == null) break;
|
||||
final width = double.tryParse(node.attributes['width'] ?? 'null');
|
||||
final height = double.tryParse(node.attributes['height'] ?? 'null');
|
||||
final ratio = width != null && height != null ? width / height : 1.0;
|
||||
if (src.startsWith('//')) {
|
||||
src = 'https:$src';
|
||||
} else if (!src.startsWith('http')) {
|
||||
final baseUri = Uri.parse(_article!.url);
|
||||
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||
src = '$baseUrl/$src';
|
||||
}
|
||||
widgets.add(
|
||||
AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
height: height ?? double.infinity,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AutoResizeUniversalImage(
|
||||
src,
|
||||
fit: width != null && height != null ? BoxFit.cover : BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
widgets.addAll(_parseHtmlToWidgets(node.children));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -163,7 +62,9 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
MaterialBanner(
|
||||
dividerColor: Colors.transparent,
|
||||
leading: const Icon(Icons.info),
|
||||
content: Text(_isReadingFromReader ? 'newsReadingFromReader'.tr() : 'newsReadingFromOriginal'.tr()),
|
||||
content: Text(_isReadingFromReader
|
||||
? 'newsReadingFromReader'.tr()
|
||||
: 'newsReadingFromOriginal'.tr()),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('newsReadingProviderSwap').tr(),
|
||||
@ -173,7 +74,7 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_articleFragment != null && _isReadingFromReader)
|
||||
if (_article != null && _isReadingFromReader)
|
||||
Expanded(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
@ -182,28 +83,43 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
|
||||
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(),
|
||||
htmlDescription.children
|
||||
.map((ele) => ele.text.trim())
|
||||
.join(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
);
|
||||
}),
|
||||
Builder(builder: (context) {
|
||||
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||
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!),
|
||||
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),
|
||||
Text('newsDisclaimer')
|
||||
.tr()
|
||||
.textStyle(Theme.of(context).textTheme.bodySmall!)
|
||||
.opacity(0.75),
|
||||
const Divider(),
|
||||
..._parseHtmlToWidgets(_articleFragment!.children),
|
||||
MarkdownTextContent(
|
||||
textScaler: TextScaler.linear(1.2),
|
||||
content: html2md.convert(_article!.content),
|
||||
),
|
||||
const Divider(),
|
||||
InkWell(
|
||||
child: Row(
|
||||
@ -211,7 +127,8 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
children: [
|
||||
Text(
|
||||
'Reference from original website',
|
||||
style: TextStyle(decoration: TextDecoration.underline),
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline),
|
||||
),
|
||||
const Gap(4),
|
||||
Icon(Icons.launch, size: 16),
|
||||
|
@ -63,7 +63,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
queryParameters: {'take': 10, 'offset': _notifications.length},
|
||||
);
|
||||
_totalCount = resp.data['count'];
|
||||
_notifications.addAll(resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? []);
|
||||
_notifications.addAll(resp.data['data']
|
||||
?.map((e) => SnNotification.fromJson(e))
|
||||
.cast<SnNotification>() ??
|
||||
[]);
|
||||
nty.updateTray();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
@ -98,7 +101,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
nty.clear();
|
||||
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('notificationMarkAllReadPrompt'.plural(resp.data['count']));
|
||||
context.showSnackbar(
|
||||
'notificationMarkAllReadPrompt'.plural(resp.data['count']));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -122,7 +126,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
_fetchNotifications();
|
||||
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']));
|
||||
context.showSnackbar(
|
||||
'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -143,7 +148,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(leading: AutoAppBarLeading(), title: Text('screenNotification').tr()),
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNotification').tr()),
|
||||
body: Center(child: UnauthorizedHint()),
|
||||
);
|
||||
}
|
||||
@ -153,7 +160,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNotification').tr(),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Symbols.checklist), onPressed: _isSubmitting ? null : _markAllAsRead),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.checklist),
|
||||
onPressed: _isSubmitting ? null : _markAllAsRead),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
@ -167,13 +176,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
return _fetchNotifications();
|
||||
},
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.only(top: 16, bottom: math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom:
|
||||
math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
itemCount: _notifications.length,
|
||||
onFetchData: () {
|
||||
_fetchNotifications();
|
||||
},
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!,
|
||||
hasReachedMax: _totalCount != null &&
|
||||
_notifications.length >= _totalCount!,
|
||||
itemBuilder: (context, idx) {
|
||||
final nty = _notifications[idx];
|
||||
return Row(
|
||||
@ -186,12 +199,19 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (nty.readAt == null)
|
||||
StyledWidget(Badge(label: Text('notificationUnread').tr())).padding(bottom: 4),
|
||||
Text(nty.title, style: Theme.of(context).textTheme.titleMedium),
|
||||
StyledWidget(Badge(
|
||||
label: Text('notificationUnread').tr()))
|
||||
.padding(bottom: 4),
|
||||
Text(nty.title,
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
if (nty.subtitle != null)
|
||||
Text(nty.subtitle!, style: Theme.of(context).textTheme.titleSmall),
|
||||
Text(nty.subtitle!,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleSmall),
|
||||
if (nty.subtitle != null) const Gap(4),
|
||||
SelectionArea(child: MarkdownTextContent(content: nty.body, isAutoWarp: true)),
|
||||
SelectionArea(
|
||||
child: MarkdownTextContent(
|
||||
content: nty.body, isAutoWarp: true)),
|
||||
if ([
|
||||
'interactive.reply',
|
||||
'interactive.feedback',
|
||||
@ -201,31 +221,43 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(color: Theme.of(context).dividerColor, width: 1),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1),
|
||||
),
|
||||
child: PostItem(
|
||||
data: SnPost.fromJson(nty.metadata['related_post']!),
|
||||
data: SnPost.fromJson(
|
||||
nty.metadata['related_post']!),
|
||||
showComments: false,
|
||||
showReactions: false,
|
||||
showMenu: false,
|
||||
),
|
||||
).padding(vertical: 4),
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {'slug': nty.metadata['related_post']!['id'].toString()},
|
||||
pathParameters: {
|
||||
'slug': nty
|
||||
.metadata['related_post']!['id']
|
||||
.toString()
|
||||
},
|
||||
);
|
||||
},
|
||||
).padding(top: 8),
|
||||
const Gap(8),
|
||||
Row(
|
||||
children: [
|
||||
Text(DateFormat('yy/MM/dd').format(nty.createdAt)).fontSize(12),
|
||||
Text(DateFormat('yy/MM/dd')
|
||||
.format(nty.createdAt))
|
||||
.fontSize(12),
|
||||
const Gap(4),
|
||||
Text('·', style: TextStyle(fontSize: 12)),
|
||||
const Gap(4),
|
||||
Text(RelativeTime(context).format(nty.createdAt)).fontSize(12),
|
||||
Text(RelativeTime(context)
|
||||
.format(nty.createdAt))
|
||||
.fontSize(12),
|
||||
],
|
||||
).opacity(0.75),
|
||||
],
|
||||
@ -235,8 +267,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.check),
|
||||
padding: EdgeInsets.all(0),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed:
|
||||
_isSubmitting ? null : () => _markOneAsRead(nty),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16);
|
||||
|
@ -22,7 +22,8 @@ class PostDetailScreen extends StatefulWidget {
|
||||
final SnPost? preload;
|
||||
final Function? onBack;
|
||||
|
||||
const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
|
||||
const PostDetailScreen(
|
||||
{super.key, required this.slug, this.preload, this.onBack});
|
||||
|
||||
@override
|
||||
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
||||
@ -88,14 +89,16 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
TextSpan(
|
||||
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
color:
|
||||
Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: 'postDetail'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
color:
|
||||
Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
]),
|
||||
@ -124,8 +127,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_data != null && _data!.type != 'video') const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||
if (_data != null && _data!.type != 'video')
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Divider(height: 1).padding(top: 8),
|
||||
),
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
@ -141,7 +147,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
).padding(horizontal: 20, vertical: 12).center(),
|
||||
),
|
||||
),
|
||||
if (_data != null && ua.isAuthorized && _data!.type != 'video')
|
||||
if (_data != null && ua.isAuthorized)
|
||||
SliverToBoxAdapter(
|
||||
child: PostCommentQuickAction(
|
||||
parentPost: _data!,
|
||||
@ -158,13 +164,15 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_data != null && _data!.type != 'video')
|
||||
if (_data != null) SliverGap(8),
|
||||
if (_data != null)
|
||||
PostCommentSliverList(
|
||||
key: _childListKey,
|
||||
parentPost: _data!,
|
||||
maxWidth: maxWidth,
|
||||
),
|
||||
if (_data != null && _data!.type == 'video') SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
if (_data != null)
|
||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -77,7 +77,8 @@ class _PostDraftBoxState extends State<PostDraftBox> {
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
separatorBuilder: (_, __) =>
|
||||
const Divider().padding(vertical: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -45,12 +45,14 @@ class PostEditorExtra {
|
||||
final String? title;
|
||||
final String? description;
|
||||
final List<PostWriteMedia>? attachments;
|
||||
final SnRealm? realm;
|
||||
|
||||
const PostEditorExtra({
|
||||
this.text,
|
||||
this.title,
|
||||
this.description,
|
||||
this.attachments,
|
||||
this.realm,
|
||||
});
|
||||
}
|
||||
|
||||
@ -164,7 +166,7 @@ class _PostEditorScreenState extends State<PostEditorScreen>
|
||||
});
|
||||
hotKeyManager.register(_saveDraftHotKey, keyDownHandler: (_) async {
|
||||
if (mounted) {
|
||||
_writeController.sendPost(context);
|
||||
_writeController.sendPost(context, saveAsDraft: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -263,6 +265,7 @@ class _PostEditorScreenState extends State<PostEditorScreen>
|
||||
_writeController.descriptionController.text =
|
||||
widget.extraProps!.description ?? '';
|
||||
_writeController.addAttachments(widget.extraProps!.attachments ?? []);
|
||||
_writeController.setRealm(widget.extraProps!.realm);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
}
|
||||
|
||||
Future<void> _fetchPosts() async {
|
||||
if (_searchTerm.isEmpty && _searchCategories.isEmpty && _searchTags.isEmpty) return;
|
||||
if (_searchTerm.isEmpty && _searchCategories.isEmpty && _searchTags.isEmpty)
|
||||
return;
|
||||
if (_postCount != null && _posts.length >= _postCount!) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
@ -152,7 +153,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||
),
|
||||
Positioned(
|
||||
top: 16,
|
||||
@ -166,7 +167,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
padding: const WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(horizontal: 24),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onChanged: (value) {
|
||||
_searchTerm = value;
|
||||
},
|
||||
|
@ -34,9 +34,11 @@ class PostPublisherScreen extends StatefulWidget {
|
||||
State<PostPublisherScreen> createState() => _PostPublisherScreenState();
|
||||
}
|
||||
|
||||
class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTickerProviderStateMixin {
|
||||
class _PostPublisherScreenState extends State<PostPublisherScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final ScrollController _scrollController = ScrollController();
|
||||
late final TabController _tabController = TabController(length: 3, vsync: this);
|
||||
late final TabController _tabController =
|
||||
TabController(length: 3, vsync: this);
|
||||
|
||||
SnPublisher? _publisher;
|
||||
SnAccount? _account;
|
||||
@ -66,7 +68,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
_account = await ud.getAccount(_publisher?.accountId);
|
||||
_accountRelationship = await rel.getRelationship(_account!.id);
|
||||
if (_publisher?.realmId != null && _publisher!.realmId != 0) {
|
||||
final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
|
||||
final resp =
|
||||
await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
|
||||
_realm = SnRealm.fromJson(resp.data);
|
||||
}
|
||||
} catch (_) {
|
||||
@ -133,12 +136,14 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
double _appBarBlur = 0.0;
|
||||
|
||||
late final _appBarWidth = MediaQuery.of(context).size.width;
|
||||
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||
late final _appBarHeight =
|
||||
(_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||
|
||||
void _updateAppBarBlur() {
|
||||
if (_scrollController.offset > _appBarHeight) return;
|
||||
setState(() {
|
||||
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
||||
_appBarBlur =
|
||||
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
||||
});
|
||||
}
|
||||
|
||||
@ -193,7 +198,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
'related': _account!.name,
|
||||
});
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
||||
context.showSnackbar(
|
||||
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -209,9 +215,11 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
|
||||
try {
|
||||
final rel = context.read<SnRelationshipProvider>();
|
||||
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
||||
await rel.updateRelationship(
|
||||
_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
||||
context.showSnackbar(
|
||||
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -299,7 +307,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: _publisher!.nick,
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge!
|
||||
.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: labelShadows,
|
||||
),
|
||||
@ -307,7 +318,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: '@${_publisher!.name}',
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: labelShadows,
|
||||
),
|
||||
@ -330,13 +344,16 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
)
|
||||
else
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer,
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 56 + MediaQuery.of(context).padding.top,
|
||||
height:
|
||||
56 + MediaQuery.of(context).padding.top,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
@ -345,7 +362,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(
|
||||
clampDouble(_appBarBlur * 0.1, 0, 0.5),
|
||||
clampDouble(
|
||||
_appBarBlur * 0.1, 0, 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -372,11 +390,14 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_publisher!.nick,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
).bold(),
|
||||
Text('@${_publisher!.name}').fontSize(13),
|
||||
],
|
||||
@ -387,7 +408,9 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
style: ButtonStyle(
|
||||
elevation: WidgetStatePropertyAll(0),
|
||||
),
|
||||
onPressed: _isSubscribing ? null : _toggleSubscription,
|
||||
onPressed: _isSubscribing
|
||||
? null
|
||||
: _toggleSubscription,
|
||||
label: Text('subscribe').tr(),
|
||||
icon: const Icon(Symbols.add),
|
||||
)
|
||||
@ -396,14 +419,17 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
style: ButtonStyle(
|
||||
elevation: WidgetStatePropertyAll(0),
|
||||
),
|
||||
onPressed: _isSubscribing ? null : _toggleSubscription,
|
||||
onPressed: _isSubscribing
|
||||
? null
|
||||
: _toggleSubscription,
|
||||
label: Text('unsubscribe').tr(),
|
||||
icon: const Icon(Symbols.remove),
|
||||
),
|
||||
PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
style: ButtonStyle(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -4, vertical: -4),
|
||||
),
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(
|
||||
@ -443,7 +469,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Text(_publisher!.description).padding(horizontal: 8),
|
||||
Text(_publisher!.description)
|
||||
.padding(horizontal: 8),
|
||||
const Gap(12),
|
||||
Column(
|
||||
children: [
|
||||
@ -451,8 +478,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
children: [
|
||||
const Icon(Symbols.calendar_add_on),
|
||||
const Gap(8),
|
||||
Text('publisherJoinedAt')
|
||||
.tr(args: [DateFormat('y/M/d').format(_publisher!.createdAt)]),
|
||||
Text('publisherJoinedAt').tr(args: [
|
||||
DateFormat('y/M/d')
|
||||
.format(_publisher!.createdAt)
|
||||
]),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
@ -460,7 +489,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
const Icon(Symbols.trending_up),
|
||||
const Gap(8),
|
||||
Text('publisherSocialPointTotal').plural(
|
||||
_publisher!.totalUpvote - _publisher!.totalDownvote,
|
||||
_publisher!.totalUpvote -
|
||||
_publisher!.totalDownvote,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -470,18 +500,22 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
const Icon(Symbols.group_work),
|
||||
const Gap(8),
|
||||
InkWell(
|
||||
child: Text('publisherAffiliatedBy').tr(args: [
|
||||
child: Text('publisherAffiliatedBy')
|
||||
.tr(args: [
|
||||
'@${_realm?.alias ?? 'unknown'}',
|
||||
]),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmDetail',
|
||||
pathParameters: {'alias': _realm!.alias},
|
||||
pathParameters: {
|
||||
'alias': _realm!.alias
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
AccountImage(content: _realm?.avatar, radius: 8),
|
||||
AccountImage(
|
||||
content: _realm?.avatar, radius: 8),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
@ -502,7 +536,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
AccountImage(content: _account?.avatar, radius: 8),
|
||||
AccountImage(
|
||||
content: _account?.avatar, radius: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -606,7 +641,7 @@ class _PublisherPostList extends StatelessWidget {
|
||||
onDeleted: onDeleted,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
149
lib/screens/realm/community.dart
Normal file
149
lib/screens/realm/community.dart
Normal file
@ -0,0 +1,149 @@
|
||||
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/post.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/post/post_editor.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class RealmCommunityScreen extends StatefulWidget {
|
||||
final String alias;
|
||||
const RealmCommunityScreen({super.key, required this.alias});
|
||||
|
||||
@override
|
||||
State<RealmCommunityScreen> createState() => _RealmCommunityScreenState();
|
||||
}
|
||||
|
||||
class _RealmCommunityScreenState extends State<RealmCommunityScreen> {
|
||||
SnRealm? _realm;
|
||||
|
||||
Future<void> _fetchRealm() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/realms/${widget.alias}');
|
||||
_realm = SnRealm.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
rethrow;
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
bool _isBusy = false;
|
||||
int? _totalCount;
|
||||
final List<SnPost> _posts = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchPosts() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final pt = context.read<SnPostContentProvider>();
|
||||
final out = await pt.listPosts(
|
||||
take: 10,
|
||||
offset: _posts.length,
|
||||
realm: _realm?.id.toString(),
|
||||
);
|
||||
_totalCount = out.$2;
|
||||
_posts.addAll(out.$1);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchRealm();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||
),
|
||||
floatingActionButton: _realm != null
|
||||
? FloatingActionButton(
|
||||
child: const Icon(Symbols.edit),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
extra: PostEditorExtra(realm: _realm!),
|
||||
);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_realm == null)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator().center(),
|
||||
),
|
||||
),
|
||||
if (_realm != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('realmCommunity'.tr(args: [_realm!.name]))
|
||||
.fontSize(17)
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
Text('postTotalCount'.plural(_totalCount ?? 0))
|
||||
.fontSize(13)
|
||||
.opacity(0.8)
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 16),
|
||||
const Divider(height: 1),
|
||||
if (_realm != null)
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchPosts,
|
||||
child: InfiniteList(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
itemCount: _posts.length,
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax:
|
||||
_totalCount != null && _posts.length >= _totalCount!,
|
||||
onFetchData: _fetchPosts,
|
||||
itemBuilder: (context, idx) {
|
||||
final post = _posts[idx];
|
||||
return OpenablePostItem(
|
||||
data: post,
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
setState(() => _posts.removeAt(idx));
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) =>
|
||||
const Divider().padding(vertical: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -318,7 +318,7 @@ class _RealmPostListWidgetState extends State<_RealmPostListWidget> {
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||
),
|
||||
),
|
||||
).padding(top: 8);
|
||||
|
@ -336,6 +336,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.hide),
|
||||
title: Text('settingsHideBottomNav').tr(),
|
||||
subtitle: Text('settingsHideBottomNavDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
value: _prefs.getBool(kAppHideBottomNav) ?? false,
|
||||
onChanged: (value) {
|
||||
_prefs.setBool(kAppHideBottomNav, value ?? false);
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
cfg.calcDrawerSize(context);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.font_download),
|
||||
title: Text('settingsCustomFonts').tr(),
|
||||
@ -387,6 +400,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
.fontSize(17)
|
||||
.tr()
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.translate),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
title: Text('settingsAutoTranslate').tr(),
|
||||
subtitle: Text('settingsAutoTranslateDescription').tr(),
|
||||
value: _prefs.getBool(kAppAutoTranslate) ?? false,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_prefs.setBool(kAppAutoTranslate, value ?? false);
|
||||
});
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.vibration),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
|
@ -88,6 +88,8 @@ Future<ThemeData> createAppTheme(
|
||||
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false),
|
||||
sliderTheme: SliderThemeData(year2023: false),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -162,3 +162,25 @@ abstract class SnAbuseReport with _$SnAbuseReport {
|
||||
factory SnAbuseReport.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAbuseReportFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SnActionEvent with _$SnActionEvent {
|
||||
const factory SnActionEvent({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required String type,
|
||||
required Map<String, dynamic>? metadata,
|
||||
required String? location,
|
||||
required double? coordinateX,
|
||||
required double? coordinateY,
|
||||
required String ipAddress,
|
||||
required String userAgent,
|
||||
required SnAccount account,
|
||||
required int accountId,
|
||||
}) = _SnActionEvent;
|
||||
|
||||
factory SnActionEvent.fromJson(Map<String, Object?> json) =>
|
||||
_$SnActionEventFromJson(json);
|
||||
}
|
||||
|
@ -3027,4 +3027,447 @@ class __$SnAbuseReportCopyWithImpl<$Res>
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnActionEvent {
|
||||
int get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
DateTime? get deletedAt;
|
||||
String get type;
|
||||
Map<String, dynamic>? get metadata;
|
||||
String? get location;
|
||||
double? get coordinateX;
|
||||
double? get coordinateY;
|
||||
String get ipAddress;
|
||||
String get userAgent;
|
||||
SnAccount get account;
|
||||
int get accountId;
|
||||
|
||||
/// Create a copy of SnActionEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnActionEventCopyWith<SnActionEvent> get copyWith =>
|
||||
_$SnActionEventCopyWithImpl<SnActionEvent>(
|
||||
this as SnActionEvent, _$identity);
|
||||
|
||||
/// Serializes this SnActionEvent to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnActionEvent &&
|
||||
(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.type, type) || other.type == type) &&
|
||||
const DeepCollectionEquality().equals(other.metadata, metadata) &&
|
||||
(identical(other.location, location) ||
|
||||
other.location == location) &&
|
||||
(identical(other.coordinateX, coordinateX) ||
|
||||
other.coordinateX == coordinateX) &&
|
||||
(identical(other.coordinateY, coordinateY) ||
|
||||
other.coordinateY == coordinateY) &&
|
||||
(identical(other.ipAddress, ipAddress) ||
|
||||
other.ipAddress == ipAddress) &&
|
||||
(identical(other.userAgent, userAgent) ||
|
||||
other.userAgent == userAgent) &&
|
||||
(identical(other.account, account) || other.account == account) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
type,
|
||||
const DeepCollectionEquality().hash(metadata),
|
||||
location,
|
||||
coordinateX,
|
||||
coordinateY,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
account,
|
||||
accountId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnActionEvent(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, metadata: $metadata, location: $location, coordinateX: $coordinateX, coordinateY: $coordinateY, ipAddress: $ipAddress, userAgent: $userAgent, account: $account, accountId: $accountId)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnActionEventCopyWith<$Res> {
|
||||
factory $SnActionEventCopyWith(
|
||||
SnActionEvent value, $Res Function(SnActionEvent) _then) =
|
||||
_$SnActionEventCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String type,
|
||||
Map<String, dynamic>? metadata,
|
||||
String? location,
|
||||
double? coordinateX,
|
||||
double? coordinateY,
|
||||
String ipAddress,
|
||||
String userAgent,
|
||||
SnAccount account,
|
||||
int accountId});
|
||||
|
||||
$SnAccountCopyWith<$Res> get account;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnActionEventCopyWithImpl<$Res>
|
||||
implements $SnActionEventCopyWith<$Res> {
|
||||
_$SnActionEventCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnActionEvent _self;
|
||||
final $Res Function(SnActionEvent) _then;
|
||||
|
||||
/// Create a copy of SnActionEvent
|
||||
/// 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? type = null,
|
||||
Object? metadata = freezed,
|
||||
Object? location = freezed,
|
||||
Object? coordinateX = freezed,
|
||||
Object? coordinateY = freezed,
|
||||
Object? ipAddress = null,
|
||||
Object? userAgent = null,
|
||||
Object? account = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
type: null == type
|
||||
? _self.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
metadata: freezed == metadata
|
||||
? _self.metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,
|
||||
location: freezed == location
|
||||
? _self.location
|
||||
: location // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
coordinateX: freezed == coordinateX
|
||||
? _self.coordinateX
|
||||
: coordinateX // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
coordinateY: freezed == coordinateY
|
||||
? _self.coordinateY
|
||||
: coordinateY // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
ipAddress: null == ipAddress
|
||||
? _self.ipAddress
|
||||
: ipAddress // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
userAgent: null == userAgent
|
||||
? _self.userAgent
|
||||
: userAgent // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
account: null == account
|
||||
? _self.account
|
||||
: account // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccount,
|
||||
accountId: null == accountId
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnActionEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountCopyWith<$Res> get account {
|
||||
return $SnAccountCopyWith<$Res>(_self.account, (value) {
|
||||
return _then(_self.copyWith(account: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _SnActionEvent implements SnActionEvent {
|
||||
const _SnActionEvent(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.type,
|
||||
required final Map<String, dynamic>? metadata,
|
||||
required this.location,
|
||||
required this.coordinateX,
|
||||
required this.coordinateY,
|
||||
required this.ipAddress,
|
||||
required this.userAgent,
|
||||
required this.account,
|
||||
required this.accountId})
|
||||
: _metadata = metadata;
|
||||
factory _SnActionEvent.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnActionEventFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final String type;
|
||||
final Map<String, dynamic>? _metadata;
|
||||
@override
|
||||
Map<String, dynamic>? get metadata {
|
||||
final value = _metadata;
|
||||
if (value == null) return null;
|
||||
if (_metadata is EqualUnmodifiableMapView) return _metadata;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
final String? location;
|
||||
@override
|
||||
final double? coordinateX;
|
||||
@override
|
||||
final double? coordinateY;
|
||||
@override
|
||||
final String ipAddress;
|
||||
@override
|
||||
final String userAgent;
|
||||
@override
|
||||
final SnAccount account;
|
||||
@override
|
||||
final int accountId;
|
||||
|
||||
/// Create a copy of SnActionEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnActionEventCopyWith<_SnActionEvent> get copyWith =>
|
||||
__$SnActionEventCopyWithImpl<_SnActionEvent>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnActionEventToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnActionEvent &&
|
||||
(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.type, type) || other.type == type) &&
|
||||
const DeepCollectionEquality().equals(other._metadata, _metadata) &&
|
||||
(identical(other.location, location) ||
|
||||
other.location == location) &&
|
||||
(identical(other.coordinateX, coordinateX) ||
|
||||
other.coordinateX == coordinateX) &&
|
||||
(identical(other.coordinateY, coordinateY) ||
|
||||
other.coordinateY == coordinateY) &&
|
||||
(identical(other.ipAddress, ipAddress) ||
|
||||
other.ipAddress == ipAddress) &&
|
||||
(identical(other.userAgent, userAgent) ||
|
||||
other.userAgent == userAgent) &&
|
||||
(identical(other.account, account) || other.account == account) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
type,
|
||||
const DeepCollectionEquality().hash(_metadata),
|
||||
location,
|
||||
coordinateX,
|
||||
coordinateY,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
account,
|
||||
accountId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnActionEvent(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, metadata: $metadata, location: $location, coordinateX: $coordinateX, coordinateY: $coordinateY, ipAddress: $ipAddress, userAgent: $userAgent, account: $account, accountId: $accountId)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnActionEventCopyWith<$Res>
|
||||
implements $SnActionEventCopyWith<$Res> {
|
||||
factory _$SnActionEventCopyWith(
|
||||
_SnActionEvent value, $Res Function(_SnActionEvent) _then) =
|
||||
__$SnActionEventCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String type,
|
||||
Map<String, dynamic>? metadata,
|
||||
String? location,
|
||||
double? coordinateX,
|
||||
double? coordinateY,
|
||||
String ipAddress,
|
||||
String userAgent,
|
||||
SnAccount account,
|
||||
int accountId});
|
||||
|
||||
@override
|
||||
$SnAccountCopyWith<$Res> get account;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnActionEventCopyWithImpl<$Res>
|
||||
implements _$SnActionEventCopyWith<$Res> {
|
||||
__$SnActionEventCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnActionEvent _self;
|
||||
final $Res Function(_SnActionEvent) _then;
|
||||
|
||||
/// Create a copy of SnActionEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? type = null,
|
||||
Object? metadata = freezed,
|
||||
Object? location = freezed,
|
||||
Object? coordinateX = freezed,
|
||||
Object? coordinateY = freezed,
|
||||
Object? ipAddress = null,
|
||||
Object? userAgent = null,
|
||||
Object? account = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
return _then(_SnActionEvent(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
type: null == type
|
||||
? _self.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
metadata: freezed == metadata
|
||||
? _self._metadata
|
||||
: metadata // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,
|
||||
location: freezed == location
|
||||
? _self.location
|
||||
: location // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
coordinateX: freezed == coordinateX
|
||||
? _self.coordinateX
|
||||
: coordinateX // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
coordinateY: freezed == coordinateY
|
||||
? _self.coordinateY
|
||||
: coordinateY // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
ipAddress: null == ipAddress
|
||||
? _self.ipAddress
|
||||
: ipAddress // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
userAgent: null == userAgent
|
||||
? _self.userAgent
|
||||
: userAgent // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
account: null == account
|
||||
? _self.account
|
||||
: account // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccount,
|
||||
accountId: null == accountId
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnActionEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountCopyWith<$Res> get account {
|
||||
return $SnAccountCopyWith<$Res>(_self.account, (value) {
|
||||
return _then(_self.copyWith(account: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
@ -283,3 +283,39 @@ Map<String, dynamic> _$SnAbuseReportToJson(_SnAbuseReport instance) =>
|
||||
'status': instance.status,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
_SnActionEvent _$SnActionEventFromJson(Map<String, dynamic> json) =>
|
||||
_SnActionEvent(
|
||||
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),
|
||||
type: json['type'] as String,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
location: json['location'] as String?,
|
||||
coordinateX: (json['coordinate_x'] as num?)?.toDouble(),
|
||||
coordinateY: (json['coordinate_y'] as num?)?.toDouble(),
|
||||
ipAddress: json['ip_address'] as String,
|
||||
userAgent: json['user_agent'] as String,
|
||||
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnActionEventToJson(_SnActionEvent instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'type': instance.type,
|
||||
'metadata': instance.metadata,
|
||||
'location': instance.location,
|
||||
'coordinate_x': instance.coordinateX,
|
||||
'coordinate_y': instance.coordinateY,
|
||||
'ip_address': instance.ipAddress,
|
||||
'user_agent': instance.userAgent,
|
||||
'account': instance.account.toJson(),
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
@ -26,7 +26,9 @@ abstract class SnAuthTicket with _$SnAuthTicket {
|
||||
required String? accessToken,
|
||||
required String? refreshToken,
|
||||
required String ipAddress,
|
||||
required String location,
|
||||
required String? location,
|
||||
required double? coordinateX,
|
||||
required double? coordinateY,
|
||||
required String userAgent,
|
||||
required DateTime? expiredAt,
|
||||
required DateTime? lastGrantAt,
|
||||
|
@ -217,7 +217,9 @@ mixin _$SnAuthTicket {
|
||||
String? get accessToken;
|
||||
String? get refreshToken;
|
||||
String get ipAddress;
|
||||
String get location;
|
||||
String? get location;
|
||||
double? get coordinateX;
|
||||
double? get coordinateY;
|
||||
String get userAgent;
|
||||
DateTime? get expiredAt;
|
||||
DateTime? get lastGrantAt;
|
||||
@ -261,6 +263,10 @@ mixin _$SnAuthTicket {
|
||||
other.ipAddress == ipAddress) &&
|
||||
(identical(other.location, location) ||
|
||||
other.location == location) &&
|
||||
(identical(other.coordinateX, coordinateX) ||
|
||||
other.coordinateX == coordinateX) &&
|
||||
(identical(other.coordinateY, coordinateY) ||
|
||||
other.coordinateY == coordinateY) &&
|
||||
(identical(other.userAgent, userAgent) ||
|
||||
other.userAgent == userAgent) &&
|
||||
(identical(other.expiredAt, expiredAt) ||
|
||||
@ -278,29 +284,32 @@ mixin _$SnAuthTicket {
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
stepRemain,
|
||||
grantToken,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
ipAddress,
|
||||
location,
|
||||
userAgent,
|
||||
expiredAt,
|
||||
lastGrantAt,
|
||||
availableAt,
|
||||
nonce,
|
||||
accountId,
|
||||
const DeepCollectionEquality().hash(factorTrail));
|
||||
int get hashCode => Object.hashAll([
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
stepRemain,
|
||||
grantToken,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
ipAddress,
|
||||
location,
|
||||
coordinateX,
|
||||
coordinateY,
|
||||
userAgent,
|
||||
expiredAt,
|
||||
lastGrantAt,
|
||||
availableAt,
|
||||
nonce,
|
||||
accountId,
|
||||
const DeepCollectionEquality().hash(factorTrail)
|
||||
]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAuthTicket(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stepRemain: $stepRemain, grantToken: $grantToken, accessToken: $accessToken, refreshToken: $refreshToken, ipAddress: $ipAddress, location: $location, userAgent: $userAgent, expiredAt: $expiredAt, lastGrantAt: $lastGrantAt, availableAt: $availableAt, nonce: $nonce, accountId: $accountId, factorTrail: $factorTrail)';
|
||||
return 'SnAuthTicket(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stepRemain: $stepRemain, grantToken: $grantToken, accessToken: $accessToken, refreshToken: $refreshToken, ipAddress: $ipAddress, location: $location, coordinateX: $coordinateX, coordinateY: $coordinateY, userAgent: $userAgent, expiredAt: $expiredAt, lastGrantAt: $lastGrantAt, availableAt: $availableAt, nonce: $nonce, accountId: $accountId, factorTrail: $factorTrail)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,7 +329,9 @@ abstract mixin class $SnAuthTicketCopyWith<$Res> {
|
||||
String? accessToken,
|
||||
String? refreshToken,
|
||||
String ipAddress,
|
||||
String location,
|
||||
String? location,
|
||||
double? coordinateX,
|
||||
double? coordinateY,
|
||||
String userAgent,
|
||||
DateTime? expiredAt,
|
||||
DateTime? lastGrantAt,
|
||||
@ -351,7 +362,9 @@ class _$SnAuthTicketCopyWithImpl<$Res> implements $SnAuthTicketCopyWith<$Res> {
|
||||
Object? accessToken = freezed,
|
||||
Object? refreshToken = freezed,
|
||||
Object? ipAddress = null,
|
||||
Object? location = null,
|
||||
Object? location = freezed,
|
||||
Object? coordinateX = freezed,
|
||||
Object? coordinateY = freezed,
|
||||
Object? userAgent = null,
|
||||
Object? expiredAt = freezed,
|
||||
Object? lastGrantAt = freezed,
|
||||
@ -397,10 +410,18 @@ class _$SnAuthTicketCopyWithImpl<$Res> implements $SnAuthTicketCopyWith<$Res> {
|
||||
? _self.ipAddress
|
||||
: ipAddress // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
location: null == location
|
||||
location: freezed == location
|
||||
? _self.location
|
||||
: location // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as String?,
|
||||
coordinateX: freezed == coordinateX
|
||||
? _self.coordinateX
|
||||
: coordinateX // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
coordinateY: freezed == coordinateY
|
||||
? _self.coordinateY
|
||||
: coordinateY // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
userAgent: null == userAgent
|
||||
? _self.userAgent
|
||||
: userAgent // ignore: cast_nullable_to_non_nullable
|
||||
@ -447,6 +468,8 @@ class _SnAuthTicket implements SnAuthTicket {
|
||||
required this.refreshToken,
|
||||
required this.ipAddress,
|
||||
required this.location,
|
||||
required this.coordinateX,
|
||||
required this.coordinateY,
|
||||
required this.userAgent,
|
||||
required this.expiredAt,
|
||||
required this.lastGrantAt,
|
||||
@ -477,7 +500,11 @@ class _SnAuthTicket implements SnAuthTicket {
|
||||
@override
|
||||
final String ipAddress;
|
||||
@override
|
||||
final String location;
|
||||
final String? location;
|
||||
@override
|
||||
final double? coordinateX;
|
||||
@override
|
||||
final double? coordinateY;
|
||||
@override
|
||||
final String userAgent;
|
||||
@override
|
||||
@ -538,6 +565,10 @@ class _SnAuthTicket implements SnAuthTicket {
|
||||
other.ipAddress == ipAddress) &&
|
||||
(identical(other.location, location) ||
|
||||
other.location == location) &&
|
||||
(identical(other.coordinateX, coordinateX) ||
|
||||
other.coordinateX == coordinateX) &&
|
||||
(identical(other.coordinateY, coordinateY) ||
|
||||
other.coordinateY == coordinateY) &&
|
||||
(identical(other.userAgent, userAgent) ||
|
||||
other.userAgent == userAgent) &&
|
||||
(identical(other.expiredAt, expiredAt) ||
|
||||
@ -555,29 +586,32 @@ class _SnAuthTicket implements SnAuthTicket {
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
stepRemain,
|
||||
grantToken,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
ipAddress,
|
||||
location,
|
||||
userAgent,
|
||||
expiredAt,
|
||||
lastGrantAt,
|
||||
availableAt,
|
||||
nonce,
|
||||
accountId,
|
||||
const DeepCollectionEquality().hash(_factorTrail));
|
||||
int get hashCode => Object.hashAll([
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
stepRemain,
|
||||
grantToken,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
ipAddress,
|
||||
location,
|
||||
coordinateX,
|
||||
coordinateY,
|
||||
userAgent,
|
||||
expiredAt,
|
||||
lastGrantAt,
|
||||
availableAt,
|
||||
nonce,
|
||||
accountId,
|
||||
const DeepCollectionEquality().hash(_factorTrail)
|
||||
]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAuthTicket(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stepRemain: $stepRemain, grantToken: $grantToken, accessToken: $accessToken, refreshToken: $refreshToken, ipAddress: $ipAddress, location: $location, userAgent: $userAgent, expiredAt: $expiredAt, lastGrantAt: $lastGrantAt, availableAt: $availableAt, nonce: $nonce, accountId: $accountId, factorTrail: $factorTrail)';
|
||||
return 'SnAuthTicket(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stepRemain: $stepRemain, grantToken: $grantToken, accessToken: $accessToken, refreshToken: $refreshToken, ipAddress: $ipAddress, location: $location, coordinateX: $coordinateX, coordinateY: $coordinateY, userAgent: $userAgent, expiredAt: $expiredAt, lastGrantAt: $lastGrantAt, availableAt: $availableAt, nonce: $nonce, accountId: $accountId, factorTrail: $factorTrail)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -599,7 +633,9 @@ abstract mixin class _$SnAuthTicketCopyWith<$Res>
|
||||
String? accessToken,
|
||||
String? refreshToken,
|
||||
String ipAddress,
|
||||
String location,
|
||||
String? location,
|
||||
double? coordinateX,
|
||||
double? coordinateY,
|
||||
String userAgent,
|
||||
DateTime? expiredAt,
|
||||
DateTime? lastGrantAt,
|
||||
@ -631,7 +667,9 @@ class __$SnAuthTicketCopyWithImpl<$Res>
|
||||
Object? accessToken = freezed,
|
||||
Object? refreshToken = freezed,
|
||||
Object? ipAddress = null,
|
||||
Object? location = null,
|
||||
Object? location = freezed,
|
||||
Object? coordinateX = freezed,
|
||||
Object? coordinateY = freezed,
|
||||
Object? userAgent = null,
|
||||
Object? expiredAt = freezed,
|
||||
Object? lastGrantAt = freezed,
|
||||
@ -677,10 +715,18 @@ class __$SnAuthTicketCopyWithImpl<$Res>
|
||||
? _self.ipAddress
|
||||
: ipAddress // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
location: null == location
|
||||
location: freezed == location
|
||||
? _self.location
|
||||
: location // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as String?,
|
||||
coordinateX: freezed == coordinateX
|
||||
? _self.coordinateX
|
||||
: coordinateX // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
coordinateY: freezed == coordinateY
|
||||
? _self.coordinateY
|
||||
: coordinateY // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
userAgent: null == userAgent
|
||||
? _self.userAgent
|
||||
: userAgent // ignore: cast_nullable_to_non_nullable
|
||||
|
@ -33,7 +33,9 @@ _SnAuthTicket _$SnAuthTicketFromJson(Map<String, dynamic> json) =>
|
||||
accessToken: json['access_token'] as String?,
|
||||
refreshToken: json['refresh_token'] as String?,
|
||||
ipAddress: json['ip_address'] as String,
|
||||
location: json['location'] as String,
|
||||
location: json['location'] as String?,
|
||||
coordinateX: (json['coordinate_x'] as num?)?.toDouble(),
|
||||
coordinateY: (json['coordinate_y'] as num?)?.toDouble(),
|
||||
userAgent: json['user_agent'] as String,
|
||||
expiredAt: json['expired_at'] == null
|
||||
? null
|
||||
@ -64,6 +66,8 @@ Map<String, dynamic> _$SnAuthTicketToJson(_SnAuthTicket instance) =>
|
||||
'refresh_token': instance.refreshToken,
|
||||
'ip_address': instance.ipAddress,
|
||||
'location': instance.location,
|
||||
'coordinate_x': instance.coordinateX,
|
||||
'coordinate_y': instance.coordinateY,
|
||||
'user_agent': instance.userAgent,
|
||||
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||
'last_grant_at': instance.lastGrantAt?.toIso8601String(),
|
||||
|
@ -166,3 +166,53 @@ abstract class SnSubscription with _$SnSubscription {
|
||||
factory SnSubscription.fromJson(Map<String, Object?> json) =>
|
||||
_$SnSubscriptionFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SnFeedEntry with _$SnFeedEntry {
|
||||
const factory SnFeedEntry({
|
||||
required String type,
|
||||
required dynamic data,
|
||||
required DateTime createdAt,
|
||||
}) = _SnFeedEntry;
|
||||
|
||||
factory SnFeedEntry.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnFeedEntryFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SnFediversePost with _$SnFediversePost {
|
||||
const factory SnFediversePost({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required String identifier,
|
||||
required String origin,
|
||||
required String content,
|
||||
required String language,
|
||||
required List<String> images,
|
||||
required SnFediverseUser user,
|
||||
required int userId,
|
||||
}) = _SnFediversePost;
|
||||
|
||||
factory SnFediversePost.fromJson(Map<String, Object?> json) =>
|
||||
_$SnFediversePostFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SnFediverseUser with _$SnFediverseUser {
|
||||
const factory SnFediverseUser({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required String identifier,
|
||||
required String origin,
|
||||
required String avatar,
|
||||
required String name,
|
||||
required String nick,
|
||||
}) = _SnFediverseUser;
|
||||
|
||||
factory SnFediverseUser.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnFediverseUserFromJson(json);
|
||||
}
|
||||
|
@ -3120,4 +3120,874 @@ class __$SnSubscriptionCopyWithImpl<$Res>
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnFeedEntry {
|
||||
String get type;
|
||||
dynamic get data;
|
||||
DateTime get createdAt;
|
||||
|
||||
/// Create a copy of SnFeedEntry
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFeedEntryCopyWith<SnFeedEntry> get copyWith =>
|
||||
_$SnFeedEntryCopyWithImpl<SnFeedEntry>(this as SnFeedEntry, _$identity);
|
||||
|
||||
/// Serializes this SnFeedEntry to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnFeedEntry &&
|
||||
(identical(other.type, type) || other.type == type) &&
|
||||
const DeepCollectionEquality().equals(other.data, data) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, type, const DeepCollectionEquality().hash(data), createdAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFeedEntry(type: $type, data: $data, createdAt: $createdAt)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnFeedEntryCopyWith<$Res> {
|
||||
factory $SnFeedEntryCopyWith(
|
||||
SnFeedEntry value, $Res Function(SnFeedEntry) _then) =
|
||||
_$SnFeedEntryCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({String type, dynamic data, DateTime createdAt});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnFeedEntryCopyWithImpl<$Res> implements $SnFeedEntryCopyWith<$Res> {
|
||||
_$SnFeedEntryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnFeedEntry _self;
|
||||
final $Res Function(SnFeedEntry) _then;
|
||||
|
||||
/// Create a copy of SnFeedEntry
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? type = null,
|
||||
Object? data = freezed,
|
||||
Object? createdAt = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
type: null == type
|
||||
? _self.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
data: freezed == data
|
||||
? _self.data
|
||||
: data // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _SnFeedEntry implements SnFeedEntry {
|
||||
const _SnFeedEntry(
|
||||
{required this.type, required this.data, required this.createdAt});
|
||||
factory _SnFeedEntry.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnFeedEntryFromJson(json);
|
||||
|
||||
@override
|
||||
final String type;
|
||||
@override
|
||||
final dynamic data;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
/// Create a copy of SnFeedEntry
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnFeedEntryCopyWith<_SnFeedEntry> get copyWith =>
|
||||
__$SnFeedEntryCopyWithImpl<_SnFeedEntry>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnFeedEntryToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnFeedEntry &&
|
||||
(identical(other.type, type) || other.type == type) &&
|
||||
const DeepCollectionEquality().equals(other.data, data) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, type, const DeepCollectionEquality().hash(data), createdAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFeedEntry(type: $type, data: $data, createdAt: $createdAt)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnFeedEntryCopyWith<$Res>
|
||||
implements $SnFeedEntryCopyWith<$Res> {
|
||||
factory _$SnFeedEntryCopyWith(
|
||||
_SnFeedEntry value, $Res Function(_SnFeedEntry) _then) =
|
||||
__$SnFeedEntryCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String type, dynamic data, DateTime createdAt});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnFeedEntryCopyWithImpl<$Res> implements _$SnFeedEntryCopyWith<$Res> {
|
||||
__$SnFeedEntryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnFeedEntry _self;
|
||||
final $Res Function(_SnFeedEntry) _then;
|
||||
|
||||
/// Create a copy of SnFeedEntry
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? type = null,
|
||||
Object? data = freezed,
|
||||
Object? createdAt = null,
|
||||
}) {
|
||||
return _then(_SnFeedEntry(
|
||||
type: null == type
|
||||
? _self.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
data: freezed == data
|
||||
? _self.data
|
||||
: data // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnFediversePost {
|
||||
int get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
DateTime? get deletedAt;
|
||||
String get identifier;
|
||||
String get origin;
|
||||
String get content;
|
||||
String get language;
|
||||
List<String> get images;
|
||||
SnFediverseUser get user;
|
||||
int get userId;
|
||||
|
||||
/// Create a copy of SnFediversePost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFediversePostCopyWith<SnFediversePost> get copyWith =>
|
||||
_$SnFediversePostCopyWithImpl<SnFediversePost>(
|
||||
this as SnFediversePost, _$identity);
|
||||
|
||||
/// Serializes this SnFediversePost to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnFediversePost &&
|
||||
(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.identifier, identifier) ||
|
||||
other.identifier == identifier) &&
|
||||
(identical(other.origin, origin) || other.origin == origin) &&
|
||||
(identical(other.content, content) || other.content == content) &&
|
||||
(identical(other.language, language) ||
|
||||
other.language == language) &&
|
||||
const DeepCollectionEquality().equals(other.images, images) &&
|
||||
(identical(other.user, user) || other.user == user) &&
|
||||
(identical(other.userId, userId) || other.userId == userId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
identifier,
|
||||
origin,
|
||||
content,
|
||||
language,
|
||||
const DeepCollectionEquality().hash(images),
|
||||
user,
|
||||
userId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFediversePost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, identifier: $identifier, origin: $origin, content: $content, language: $language, images: $images, user: $user, userId: $userId)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnFediversePostCopyWith<$Res> {
|
||||
factory $SnFediversePostCopyWith(
|
||||
SnFediversePost value, $Res Function(SnFediversePost) _then) =
|
||||
_$SnFediversePostCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String identifier,
|
||||
String origin,
|
||||
String content,
|
||||
String language,
|
||||
List<String> images,
|
||||
SnFediverseUser user,
|
||||
int userId});
|
||||
|
||||
$SnFediverseUserCopyWith<$Res> get user;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnFediversePostCopyWithImpl<$Res>
|
||||
implements $SnFediversePostCopyWith<$Res> {
|
||||
_$SnFediversePostCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnFediversePost _self;
|
||||
final $Res Function(SnFediversePost) _then;
|
||||
|
||||
/// Create a copy of SnFediversePost
|
||||
/// 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? identifier = null,
|
||||
Object? origin = null,
|
||||
Object? content = null,
|
||||
Object? language = null,
|
||||
Object? images = null,
|
||||
Object? user = null,
|
||||
Object? userId = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
identifier: null == identifier
|
||||
? _self.identifier
|
||||
: identifier // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
origin: null == origin
|
||||
? _self.origin
|
||||
: origin // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
content: null == content
|
||||
? _self.content
|
||||
: content // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
language: null == language
|
||||
? _self.language
|
||||
: language // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
images: null == images
|
||||
? _self.images
|
||||
: images // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
user: null == user
|
||||
? _self.user
|
||||
: user // ignore: cast_nullable_to_non_nullable
|
||||
as SnFediverseUser,
|
||||
userId: null == userId
|
||||
? _self.userId
|
||||
: userId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnFediversePost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFediverseUserCopyWith<$Res> get user {
|
||||
return $SnFediverseUserCopyWith<$Res>(_self.user, (value) {
|
||||
return _then(_self.copyWith(user: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _SnFediversePost implements SnFediversePost {
|
||||
const _SnFediversePost(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.identifier,
|
||||
required this.origin,
|
||||
required this.content,
|
||||
required this.language,
|
||||
required final List<String> images,
|
||||
required this.user,
|
||||
required this.userId})
|
||||
: _images = images;
|
||||
factory _SnFediversePost.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnFediversePostFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final String identifier;
|
||||
@override
|
||||
final String origin;
|
||||
@override
|
||||
final String content;
|
||||
@override
|
||||
final String language;
|
||||
final List<String> _images;
|
||||
@override
|
||||
List<String> get images {
|
||||
if (_images is EqualUnmodifiableListView) return _images;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_images);
|
||||
}
|
||||
|
||||
@override
|
||||
final SnFediverseUser user;
|
||||
@override
|
||||
final int userId;
|
||||
|
||||
/// Create a copy of SnFediversePost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnFediversePostCopyWith<_SnFediversePost> get copyWith =>
|
||||
__$SnFediversePostCopyWithImpl<_SnFediversePost>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnFediversePostToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnFediversePost &&
|
||||
(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.identifier, identifier) ||
|
||||
other.identifier == identifier) &&
|
||||
(identical(other.origin, origin) || other.origin == origin) &&
|
||||
(identical(other.content, content) || other.content == content) &&
|
||||
(identical(other.language, language) ||
|
||||
other.language == language) &&
|
||||
const DeepCollectionEquality().equals(other._images, _images) &&
|
||||
(identical(other.user, user) || other.user == user) &&
|
||||
(identical(other.userId, userId) || other.userId == userId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
identifier,
|
||||
origin,
|
||||
content,
|
||||
language,
|
||||
const DeepCollectionEquality().hash(_images),
|
||||
user,
|
||||
userId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFediversePost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, identifier: $identifier, origin: $origin, content: $content, language: $language, images: $images, user: $user, userId: $userId)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnFediversePostCopyWith<$Res>
|
||||
implements $SnFediversePostCopyWith<$Res> {
|
||||
factory _$SnFediversePostCopyWith(
|
||||
_SnFediversePost value, $Res Function(_SnFediversePost) _then) =
|
||||
__$SnFediversePostCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String identifier,
|
||||
String origin,
|
||||
String content,
|
||||
String language,
|
||||
List<String> images,
|
||||
SnFediverseUser user,
|
||||
int userId});
|
||||
|
||||
@override
|
||||
$SnFediverseUserCopyWith<$Res> get user;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnFediversePostCopyWithImpl<$Res>
|
||||
implements _$SnFediversePostCopyWith<$Res> {
|
||||
__$SnFediversePostCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnFediversePost _self;
|
||||
final $Res Function(_SnFediversePost) _then;
|
||||
|
||||
/// Create a copy of SnFediversePost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? identifier = null,
|
||||
Object? origin = null,
|
||||
Object? content = null,
|
||||
Object? language = null,
|
||||
Object? images = null,
|
||||
Object? user = null,
|
||||
Object? userId = null,
|
||||
}) {
|
||||
return _then(_SnFediversePost(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
identifier: null == identifier
|
||||
? _self.identifier
|
||||
: identifier // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
origin: null == origin
|
||||
? _self.origin
|
||||
: origin // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
content: null == content
|
||||
? _self.content
|
||||
: content // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
language: null == language
|
||||
? _self.language
|
||||
: language // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
images: null == images
|
||||
? _self._images
|
||||
: images // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
user: null == user
|
||||
? _self.user
|
||||
: user // ignore: cast_nullable_to_non_nullable
|
||||
as SnFediverseUser,
|
||||
userId: null == userId
|
||||
? _self.userId
|
||||
: userId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnFediversePost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFediverseUserCopyWith<$Res> get user {
|
||||
return $SnFediverseUserCopyWith<$Res>(_self.user, (value) {
|
||||
return _then(_self.copyWith(user: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnFediverseUser {
|
||||
int get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
DateTime? get deletedAt;
|
||||
String get identifier;
|
||||
String get origin;
|
||||
String get avatar;
|
||||
String get name;
|
||||
String get nick;
|
||||
|
||||
/// Create a copy of SnFediverseUser
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFediverseUserCopyWith<SnFediverseUser> get copyWith =>
|
||||
_$SnFediverseUserCopyWithImpl<SnFediverseUser>(
|
||||
this as SnFediverseUser, _$identity);
|
||||
|
||||
/// Serializes this SnFediverseUser to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnFediverseUser &&
|
||||
(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.identifier, identifier) ||
|
||||
other.identifier == identifier) &&
|
||||
(identical(other.origin, origin) || other.origin == origin) &&
|
||||
(identical(other.avatar, avatar) || other.avatar == avatar) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.nick, nick) || other.nick == nick));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||
deletedAt, identifier, origin, avatar, name, nick);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFediverseUser(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, identifier: $identifier, origin: $origin, avatar: $avatar, name: $name, nick: $nick)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnFediverseUserCopyWith<$Res> {
|
||||
factory $SnFediverseUserCopyWith(
|
||||
SnFediverseUser value, $Res Function(SnFediverseUser) _then) =
|
||||
_$SnFediverseUserCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String identifier,
|
||||
String origin,
|
||||
String avatar,
|
||||
String name,
|
||||
String nick});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnFediverseUserCopyWithImpl<$Res>
|
||||
implements $SnFediverseUserCopyWith<$Res> {
|
||||
_$SnFediverseUserCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnFediverseUser _self;
|
||||
final $Res Function(SnFediverseUser) _then;
|
||||
|
||||
/// Create a copy of SnFediverseUser
|
||||
/// 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? identifier = null,
|
||||
Object? origin = null,
|
||||
Object? avatar = null,
|
||||
Object? name = null,
|
||||
Object? nick = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
identifier: null == identifier
|
||||
? _self.identifier
|
||||
: identifier // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
origin: null == origin
|
||||
? _self.origin
|
||||
: origin // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
avatar: null == avatar
|
||||
? _self.avatar
|
||||
: avatar // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _self.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
nick: null == nick
|
||||
? _self.nick
|
||||
: nick // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _SnFediverseUser implements SnFediverseUser {
|
||||
const _SnFediverseUser(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.identifier,
|
||||
required this.origin,
|
||||
required this.avatar,
|
||||
required this.name,
|
||||
required this.nick});
|
||||
factory _SnFediverseUser.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnFediverseUserFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final String identifier;
|
||||
@override
|
||||
final String origin;
|
||||
@override
|
||||
final String avatar;
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final String nick;
|
||||
|
||||
/// Create a copy of SnFediverseUser
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnFediverseUserCopyWith<_SnFediverseUser> get copyWith =>
|
||||
__$SnFediverseUserCopyWithImpl<_SnFediverseUser>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnFediverseUserToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnFediverseUser &&
|
||||
(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.identifier, identifier) ||
|
||||
other.identifier == identifier) &&
|
||||
(identical(other.origin, origin) || other.origin == origin) &&
|
||||
(identical(other.avatar, avatar) || other.avatar == avatar) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.nick, nick) || other.nick == nick));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||
deletedAt, identifier, origin, avatar, name, nick);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFediverseUser(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, identifier: $identifier, origin: $origin, avatar: $avatar, name: $name, nick: $nick)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnFediverseUserCopyWith<$Res>
|
||||
implements $SnFediverseUserCopyWith<$Res> {
|
||||
factory _$SnFediverseUserCopyWith(
|
||||
_SnFediverseUser value, $Res Function(_SnFediverseUser) _then) =
|
||||
__$SnFediverseUserCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String identifier,
|
||||
String origin,
|
||||
String avatar,
|
||||
String name,
|
||||
String nick});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnFediverseUserCopyWithImpl<$Res>
|
||||
implements _$SnFediverseUserCopyWith<$Res> {
|
||||
__$SnFediverseUserCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnFediverseUser _self;
|
||||
final $Res Function(_SnFediverseUser) _then;
|
||||
|
||||
/// Create a copy of SnFediverseUser
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? identifier = null,
|
||||
Object? origin = null,
|
||||
Object? avatar = null,
|
||||
Object? name = null,
|
||||
Object? nick = null,
|
||||
}) {
|
||||
return _then(_SnFediverseUser(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
identifier: null == identifier
|
||||
? _self.identifier
|
||||
: identifier // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
origin: null == origin
|
||||
? _self.origin
|
||||
: origin // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
avatar: null == avatar
|
||||
? _self.avatar
|
||||
: avatar // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _self.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
nick: null == nick
|
||||
? _self.nick
|
||||
: nick // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
@ -282,3 +282,77 @@ Map<String, dynamic> _$SnSubscriptionToJson(_SnSubscription instance) =>
|
||||
'follower_id': instance.followerId,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
_SnFeedEntry _$SnFeedEntryFromJson(Map<String, dynamic> json) => _SnFeedEntry(
|
||||
type: json['type'] as String,
|
||||
data: json['data'],
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnFeedEntryToJson(_SnFeedEntry instance) =>
|
||||
<String, dynamic>{
|
||||
'type': instance.type,
|
||||
'data': instance.data,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
|
||||
_SnFediversePost _$SnFediversePostFromJson(Map<String, dynamic> json) =>
|
||||
_SnFediversePost(
|
||||
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),
|
||||
identifier: json['identifier'] as String,
|
||||
origin: json['origin'] as String,
|
||||
content: json['content'] as String,
|
||||
language: json['language'] as String,
|
||||
images:
|
||||
(json['images'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
user: SnFediverseUser.fromJson(json['user'] as Map<String, dynamic>),
|
||||
userId: (json['user_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnFediversePostToJson(_SnFediversePost instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'identifier': instance.identifier,
|
||||
'origin': instance.origin,
|
||||
'content': instance.content,
|
||||
'language': instance.language,
|
||||
'images': instance.images,
|
||||
'user': instance.user.toJson(),
|
||||
'user_id': instance.userId,
|
||||
};
|
||||
|
||||
_SnFediverseUser _$SnFediverseUserFromJson(Map<String, dynamic> json) =>
|
||||
_SnFediverseUser(
|
||||
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),
|
||||
identifier: json['identifier'] as String,
|
||||
origin: json['origin'] as String,
|
||||
avatar: json['avatar'] as String,
|
||||
name: json['name'] as String,
|
||||
nick: json['nick'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnFediverseUserToJson(_SnFediverseUser instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'identifier': instance.identifier,
|
||||
'origin': instance.origin,
|
||||
'avatar': instance.avatar,
|
||||
'name': instance.name,
|
||||
'nick': instance.nick,
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ class AccountImage extends StatelessWidget {
|
||||
final Widget? fallbackWidget;
|
||||
final Widget? badge;
|
||||
final Offset? badgeOffset;
|
||||
final FilterQuality? filterQuality;
|
||||
|
||||
const AccountImage({
|
||||
super.key,
|
||||
@ -25,6 +26,7 @@ class AccountImage extends StatelessWidget {
|
||||
this.fallbackWidget,
|
||||
this.badge,
|
||||
this.badgeOffset,
|
||||
this.filterQuality,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -54,6 +56,7 @@ class AccountImage extends StatelessWidget {
|
||||
)
|
||||
: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(url),
|
||||
filterQuality: filterQuality,
|
||||
key: Key('attachment-${content.hashCode}'),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
|
@ -45,11 +45,25 @@ class AttachmentItem extends StatelessWidget {
|
||||
case 'image':
|
||||
return Hero(
|
||||
tag: 'attachment-${data!.rid}-$tag',
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(data!.rid),
|
||||
key: Key('attachment-${data!.rid}-$tag'),
|
||||
fit: fit,
|
||||
filterQuality: filterQuality,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(data!.rid),
|
||||
key: Key('attachment-${data!.rid}-$tag-blur-background'),
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: filterQuality,
|
||||
),
|
||||
),
|
||||
AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(data!.rid),
|
||||
key: Key('attachment-${data!.rid}-$tag'),
|
||||
fit: fit,
|
||||
filterQuality: filterQuality,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
case 'video':
|
||||
@ -251,6 +265,7 @@ class _AttachmentItemContentVideoState
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (widget.data.thumbnail != null)
|
||||
AutoResizeUniversalImage(
|
||||
@ -455,6 +470,7 @@ class _AttachmentItemContentAudioState
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (widget.data.thumbnail != null)
|
||||
AspectRatio(
|
||||
|
@ -95,8 +95,9 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.data.firstOrNull?.mediaType != SnMediaType.image)
|
||||
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) {
|
||||
return;
|
||||
}
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data.where((ele) => ele != null).cast(),
|
||||
@ -209,7 +210,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
child: AspectRatio(
|
||||
aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
|
||||
child: ScrollConfiguration(
|
||||
behavior: _AttachmentListScrollBehavior(),
|
||||
behavior: AttachmentListScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
padding: widget.padding,
|
||||
shrinkWrap: true,
|
||||
@ -283,7 +284,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
}
|
||||
}
|
||||
|
||||
class _AttachmentListScrollBehavior extends MaterialScrollBehavior {
|
||||
class AttachmentListScrollBehavior extends MaterialScrollBehavior {
|
||||
@override
|
||||
Set<PointerDeviceKind> get dragDevices =>
|
||||
{PointerDeviceKind.touch, PointerDeviceKind.mouse};
|
||||
|
@ -10,6 +10,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/keypair.dart';
|
||||
import 'package:surface/providers/translation.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
@ -18,6 +19,7 @@ import 'package:surface/widgets/account/account_popover.dart';
|
||||
import 'package:surface/widgets/account/badge.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/context_menu.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/link_preview.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
@ -63,7 +65,7 @@ class ChatMessage extends StatelessWidget {
|
||||
key: Key('chat-message-${data.id}'),
|
||||
iconOnLeftSwipe: Symbols.reply,
|
||||
iconOnRightSwipe: Symbols.edit,
|
||||
swipeSensitivity: 20,
|
||||
swipeSensitivity: 10,
|
||||
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
||||
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
|
||||
child: ContextMenuArea(
|
||||
@ -228,7 +230,7 @@ class ChatMessage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatMessageText extends StatelessWidget {
|
||||
class _ChatMessageText extends StatefulWidget {
|
||||
final SnChatMessage data;
|
||||
final Function(SnChatMessage)? onReply;
|
||||
final Function(SnChatMessage)? onEdit;
|
||||
@ -237,13 +239,56 @@ class _ChatMessageText extends StatelessWidget {
|
||||
const _ChatMessageText(
|
||||
{required this.data, this.onReply, this.onEdit, this.onDelete});
|
||||
|
||||
@override
|
||||
State<_ChatMessageText> createState() => _ChatMessageTextState();
|
||||
}
|
||||
|
||||
class _ChatMessageTextState extends State<_ChatMessageText> {
|
||||
late String _displayText = widget.data.body['text'] ?? '';
|
||||
bool _isTranslated = false;
|
||||
bool _isTranslating = false;
|
||||
|
||||
Future<void> _translateText() async {
|
||||
if (widget.data.body['text'] == null || widget.data.body['text']!.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final ta = context.read<SnTranslator>();
|
||||
setState(() => _isTranslating = true);
|
||||
try {
|
||||
final to = EasyLocalization.of(context)!.locale.languageCode;
|
||||
_displayText = await ta.translate(
|
||||
widget.data.body['text'],
|
||||
to: to,
|
||||
);
|
||||
_isTranslated = true;
|
||||
if (mounted) setState(() {});
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isTranslating = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
if (cfg.autoTranslate) {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_translateText();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;
|
||||
final isOwner =
|
||||
ua.isAuthorized && widget.data.sender.accountId == ua.user?.id;
|
||||
|
||||
if (data.body['text'] != null && data.body['text'].isNotEmpty) {
|
||||
if (widget.data.body['text'] != null &&
|
||||
widget.data.body['text'].isNotEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -252,38 +297,50 @@ class _ChatMessageText extends StatelessWidget {
|
||||
final List<ContextMenuButtonItem> items =
|
||||
editableTextState.contextMenuButtonItems;
|
||||
|
||||
if (onReply != null) {
|
||||
if (widget.onReply != null) {
|
||||
items.insert(
|
||||
0,
|
||||
ContextMenuButtonItem(
|
||||
label: 'reply'.tr(),
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
onReply?.call(data);
|
||||
widget.onReply?.call(widget.data);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isOwner && onEdit != null) {
|
||||
if (isOwner && widget.onEdit != null) {
|
||||
items.insert(
|
||||
1,
|
||||
ContextMenuButtonItem(
|
||||
label: 'edit'.tr(),
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
onEdit?.call(data);
|
||||
widget.onEdit?.call(widget.data);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isOwner && onDelete != null) {
|
||||
if (isOwner && widget.onDelete != null) {
|
||||
items.insert(
|
||||
2,
|
||||
ContextMenuButtonItem(
|
||||
label: 'delete'.tr(),
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
onDelete?.call(data);
|
||||
widget.onDelete?.call(widget.data);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (widget.data.body['algorithm'] == 'plain') {
|
||||
items.insert(
|
||||
3,
|
||||
ContextMenuButtonItem(
|
||||
label: 'translate'.tr(),
|
||||
onPressed: () {
|
||||
ContextMenuController.removeAny();
|
||||
_translateText();
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -294,26 +351,47 @@ class _ChatMessageText extends StatelessWidget {
|
||||
buttonItems: items,
|
||||
);
|
||||
},
|
||||
child: switch (data.body['algorithm']) {
|
||||
'rsa' => _ChatDecryptMessage(message: data),
|
||||
child: switch (widget.data.body['algorithm']) {
|
||||
'rsa' => _ChatDecryptMessage(message: widget.data),
|
||||
_ => MarkdownTextContent(
|
||||
content: data.body['text'],
|
||||
content: _displayText,
|
||||
isAutoWarp: true,
|
||||
isEnlargeSticker:
|
||||
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
|
||||
isEnlargeSticker: RegExp(r"^:([-\w]+):$")
|
||||
.hasMatch(widget.data.body['text'] ?? ''),
|
||||
),
|
||||
},
|
||||
),
|
||||
if (data.updatedAt != data.createdAt)
|
||||
if (widget.data.updatedAt != widget.data.createdAt)
|
||||
Text('messageEditedHint'.tr()).fontSize(13).opacity(0.75),
|
||||
if (_isTranslating)
|
||||
AnimateWidgetExtensions(Text('translating').tr())
|
||||
.animate(onPlay: (e) => e.repeat())
|
||||
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
|
||||
.then()
|
||||
.fadeOut(
|
||||
duration: 500.ms,
|
||||
delay: 1000.ms,
|
||||
curve: Curves.easeIn,
|
||||
),
|
||||
if (_isTranslated)
|
||||
InkWell(
|
||||
child: Text('translated').tr().opacity(0.75),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_displayText = widget.data.body['text'] ?? '';
|
||||
_isTranslated = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (data.body['attachments']?.isNotEmpty) {
|
||||
} else if (widget.data.body['attachments']?.isNotEmpty) {
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Symbols.file_present, size: 20),
|
||||
const Gap(4),
|
||||
Text('messageFileHint'.plural(data.body['attachments']!.length)),
|
||||
Text('messageFileHint'
|
||||
.plural(widget.data.body['attachments']!.length)),
|
||||
],
|
||||
).opacity(0.8);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import 'package:flutter/foundation.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:google_fonts/google_fonts.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@ -445,6 +446,61 @@ class _StickerPicker extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sticker = context.read<SnStickerProvider>();
|
||||
if (sticker.stickersByPack.isEmpty) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
onDismiss?.call();
|
||||
},
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(360, MediaQuery.of(context).size.width - 40),
|
||||
),
|
||||
child: Material(
|
||||
elevation: 8,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.ar_stickers, size: 48),
|
||||
const Gap(8),
|
||||
Text('stickerPickerEmpty').tr().bold(),
|
||||
Text(
|
||||
'stickerPickerEmptyHint',
|
||||
textAlign: TextAlign.center,
|
||||
).tr().opacity(0.75),
|
||||
TextButton(
|
||||
child: Text('goto'.tr(args: ['screenStickers'.tr()])),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).goNamed('stickers');
|
||||
},
|
||||
),
|
||||
InkWell(
|
||||
child: Text(
|
||||
'stickersReload',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
).tr(),
|
||||
onTap: () async {
|
||||
await sticker.listSticker();
|
||||
if (!context.mounted) return;
|
||||
HapticFeedback.heavyImpact();
|
||||
context.showSnackbar('stickersReloaded'.tr());
|
||||
onDismiss?.call();
|
||||
},
|
||||
)
|
||||
],
|
||||
).padding(all: 64),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
onDismiss?.call();
|
||||
|
@ -36,10 +36,12 @@ class ChatTypingIndicator extends StatelessWidget {
|
||||
'messageTyping'
|
||||
.plural(controller.typingMembers.length, args: [
|
||||
controller.typingMembers
|
||||
.map((ele) => (ele.nick?.isNotEmpty ?? false)
|
||||
? ele.nick!
|
||||
: ud.getFromCache(ele.accountId)?.name ??
|
||||
'unknown')
|
||||
.map(
|
||||
(ele) => (ele.nick?.isNotEmpty ?? false)
|
||||
? ele.nick!
|
||||
: ud.getFromCache(ele.accountId)?.nick ??
|
||||
'unknown',
|
||||
)
|
||||
.join(', '),
|
||||
]),
|
||||
),
|
||||
|
105
lib/widgets/feed/feed_news.dart
Normal file
105
lib/widgets/feed/feed_news.dart
Normal file
@ -0,0 +1,105 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/types/news.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
|
||||
class NewsFeedEntry extends StatelessWidget {
|
||||
final SnFeedEntry data;
|
||||
const NewsFeedEntry({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<SnNewsArticle> news = data.data
|
||||
.map((ele) => SnNewsArticle.fromJson(ele))
|
||||
.cast<SnNewsArticle>()
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Symbols.newspaper),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'newsToday',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr()
|
||||
],
|
||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
height: 150,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: news.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
width: 360,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: Material(
|
||||
elevation: 0,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
news[idx].title,
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
).padding(horizontal: 16, top: 12, bottom: 4),
|
||||
Text(
|
||||
news[idx].description,
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
).padding(horizontal: 16, vertical: 4),
|
||||
const Gap(4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('y/M/d HH:mm')
|
||||
.format(news[idx].createdAt.toLocal()),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
RelativeTime(context)
|
||||
.format(news[idx].createdAt.toLocal()),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
).opacity(0.8).padding(horizontal: 16),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'newsDetail',
|
||||
pathParameters: {'hash': news[idx].hash},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(12),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
25
lib/widgets/feed/feed_unknown.dart
Normal file
25
lib/widgets/feed/feed_unknown.dart
Normal file
@ -0,0 +1,25 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
|
||||
class FeedUnknownEntry extends StatelessWidget {
|
||||
final SnFeedEntry data;
|
||||
const FeedUnknownEntry({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Symbols.help, size: 36),
|
||||
const Gap(4),
|
||||
Text('feedUnknownItem').tr(),
|
||||
Text(data.type, style: GoogleFonts.robotoMono()),
|
||||
],
|
||||
).padding(horizontal: 12, vertical: 8);
|
||||
}
|
||||
}
|
108
lib/widgets/html.dart
Normal file
108
lib/widgets/html.dart
Normal file
@ -0,0 +1,108 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
List<Widget> parseHtmlToWidgets(
|
||||
BuildContext context, Iterable<dom.Element>? elements) {
|
||||
if (elements == null) return [];
|
||||
|
||||
final List<Widget> widgets = [];
|
||||
|
||||
for (final node in elements) {
|
||||
switch (node.localName) {
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
widgets.add(Text(node.text.trim(),
|
||||
style: Theme.of(context).textTheme.titleMedium));
|
||||
break;
|
||||
case 'p':
|
||||
if (node.text.trim().isEmpty) continue;
|
||||
widgets.add(
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: node.text.trim(),
|
||||
children: [
|
||||
for (final child in node.children)
|
||||
switch (child.localName) {
|
||||
'a' => TextSpan(
|
||||
text: child.text.trim(),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlString(child.attributes['href']!);
|
||||
},
|
||||
),
|
||||
_ => TextSpan(text: child.text.trim()),
|
||||
},
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'a':
|
||||
// drop single link
|
||||
break;
|
||||
case 'div':
|
||||
// ignore div text, normally it is not meaningful
|
||||
widgets.addAll(parseHtmlToWidgets(context, node.children));
|
||||
break;
|
||||
case 'hr':
|
||||
widgets.add(const Divider());
|
||||
break;
|
||||
case 'img':
|
||||
var src = node.attributes['src'];
|
||||
if (src == null) break;
|
||||
final width = double.tryParse(node.attributes['width'] ?? 'null');
|
||||
final height = double.tryParse(node.attributes['height'] ?? 'null');
|
||||
final ratio = width != null && height != null ? width / height : 1.0;
|
||||
if (src.startsWith('//')) {
|
||||
src = 'https:$src';
|
||||
} else if (!src.startsWith('http')) {
|
||||
// final baseUri = Uri.parse(_article!.url);
|
||||
// final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||
src = src;
|
||||
}
|
||||
widgets.add(
|
||||
AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
height: height ?? double.infinity,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AutoResizeUniversalImage(
|
||||
src,
|
||||
fit: width != null && height != null
|
||||
? BoxFit.cover
|
||||
: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
widgets.addAll(parseHtmlToWidgets(context, node.children));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
@ -76,7 +76,10 @@ class _LoadingIndicatorState extends State<LoadingIndicator>
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Text('loading').tr(),
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_highlight/flutter_highlight.dart';
|
||||
@ -174,7 +176,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: attachment.metadata['ratio'] ??
|
||||
aspectRatio: attachment.metadata['ratio']?.toDouble() ??
|
||||
switch (attachment.mimetype
|
||||
.split('/')
|
||||
.firstOrNull) {
|
||||
@ -207,10 +209,14 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
width ??= math.min(MediaQuery.of(context).size.width, 640);
|
||||
height ??= width;
|
||||
return UniversalImage(
|
||||
url,
|
||||
width: width,
|
||||
height: height,
|
||||
cacheHeight: height,
|
||||
cacheWidth: width,
|
||||
fit: fit,
|
||||
);
|
||||
},
|
||||
|
114
lib/widgets/menu_bar.dart
Normal file
114
lib/widgets/menu_bar.dart
Normal file
@ -0,0 +1,114 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/navigation.dart';
|
||||
import 'package:surface/router.dart';
|
||||
|
||||
// https://api.flutter.dev/flutter/widgets/PlatformMenuBar-class.html
|
||||
// All the code following is only works on macOS
|
||||
class AppSystemMenuBar extends StatelessWidget {
|
||||
final Function? onQuit;
|
||||
final Widget child;
|
||||
const AppSystemMenuBar({super.key, this.onQuit, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (kIsWeb || !Platform.isMacOS) return child;
|
||||
|
||||
final nav = context.watch<NavigationProvider>();
|
||||
|
||||
return PlatformMenuBar(
|
||||
menus: <PlatformMenuItem>[
|
||||
PlatformMenu(
|
||||
label: 'Solian',
|
||||
menus: <PlatformMenuItem>[
|
||||
PlatformMenuItemGroup(
|
||||
members: <PlatformMenuItem>[
|
||||
PlatformMenuItem(
|
||||
label: 'screenAbout'.tr(),
|
||||
onSelected: () {
|
||||
appRouter.goNamed('about');
|
||||
nav.autoDetectIndex(appRouter);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
PlatformMenuItemGroup(
|
||||
members: [
|
||||
PlatformMenuItem(
|
||||
label: 'screenHome'.tr(),
|
||||
shortcut: const SingleActivator(
|
||||
LogicalKeyboardKey.digit1,
|
||||
meta: true,
|
||||
),
|
||||
onSelected: () {
|
||||
appRouter.goNamed('home');
|
||||
nav.autoDetectIndex(appRouter);
|
||||
},
|
||||
),
|
||||
PlatformMenuItem(
|
||||
label: 'screenExplore'.tr(),
|
||||
shortcut: const SingleActivator(
|
||||
LogicalKeyboardKey.digit2,
|
||||
meta: true,
|
||||
),
|
||||
onSelected: () {
|
||||
appRouter.goNamed('explore');
|
||||
nav.autoDetectIndex(appRouter);
|
||||
},
|
||||
),
|
||||
PlatformMenuItem(
|
||||
label: 'screenChat'.tr(),
|
||||
shortcut: const SingleActivator(
|
||||
LogicalKeyboardKey.digit3,
|
||||
meta: true,
|
||||
),
|
||||
onSelected: () {
|
||||
appRouter.goNamed('chat');
|
||||
},
|
||||
),
|
||||
PlatformMenuItem(
|
||||
label: 'screenAccount'.tr(),
|
||||
shortcut: const SingleActivator(
|
||||
LogicalKeyboardKey.digit4,
|
||||
meta: true,
|
||||
),
|
||||
onSelected: () {
|
||||
appRouter.goNamed('account');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
PlatformMenuItem(
|
||||
shortcut: const SingleActivator(
|
||||
LogicalKeyboardKey.keyH,
|
||||
meta: true,
|
||||
),
|
||||
label: 'trayMenuHide'.tr(),
|
||||
onSelected: () {
|
||||
appWindow.hide();
|
||||
},
|
||||
),
|
||||
if (onQuit != null)
|
||||
PlatformMenuItem(
|
||||
shortcut: const SingleActivator(
|
||||
LogicalKeyboardKey.keyQ,
|
||||
meta: true,
|
||||
),
|
||||
label: 'trayMenuExit'.tr(),
|
||||
onSelected: () {
|
||||
onQuit?.call();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
@ -37,17 +37,15 @@ class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> {
|
||||
...nav.destinations.where((ele) => ele.isPinned),
|
||||
];
|
||||
|
||||
return BottomNavigationBar(
|
||||
currentIndex: nav.getIndexInRange(0, nav.pinnedDestinationCount),
|
||||
type: BottomNavigationBarType.fixed,
|
||||
showUnselectedLabels: false,
|
||||
items: destinations.map((ele) {
|
||||
return BottomNavigationBarItem(
|
||||
return NavigationBar(
|
||||
selectedIndex: nav.getIndexInRange(0, nav.pinnedDestinationCount),
|
||||
destinations: destinations.map((ele) {
|
||||
return NavigationDestination(
|
||||
icon: ele.icon,
|
||||
label: ele.label.tr(),
|
||||
);
|
||||
}).toList(),
|
||||
onTap: (idx) {
|
||||
onDestinationSelected: (idx) {
|
||||
nav.setIndex(idx);
|
||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||
},
|
||||
|
@ -1,14 +1,23 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/navigation.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_realm.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:surface/widgets/version_label.dart';
|
||||
|
||||
class AppNavigationDrawer extends StatefulWidget {
|
||||
@ -25,12 +34,15 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
|
||||
context
|
||||
.read<NavigationProvider>()
|
||||
.autoDetectIndex(GoRouter.maybeOf(context));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.read<UserProvider>();
|
||||
final nav = context.watch<NavigationProvider>();
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
|
||||
@ -39,60 +51,258 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
return ListenableBuilder(
|
||||
listenable: nav,
|
||||
builder: (context, _) {
|
||||
final destinations = [
|
||||
...nav.destinations.where((ele) => ele.isPinned),
|
||||
...nav.destinations.where((ele) => !ele.isPinned),
|
||||
];
|
||||
|
||||
return NavigationDrawer(
|
||||
return Drawer(
|
||||
elevation: widget.elevation,
|
||||
backgroundColor: backgroundColor,
|
||||
selectedIndex: nav.currentIndex,
|
||||
children: [
|
||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS) && !cfg.drawerIsExpanded)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!kIsWeb &&
|
||||
(Platform.isWindows ||
|
||||
Platform.isLinux ||
|
||||
Platform.isMacOS) &&
|
||||
!cfg.drawerIsExpanded)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: WindowTitleBarBox(),
|
||||
),
|
||||
child: WindowTitleBarBox(),
|
||||
Gap(MediaQuery.of(context).padding.top),
|
||||
Expanded(
|
||||
child: _DrawerContentList(),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Solar Network').bold(),
|
||||
AppVersionLabel(),
|
||||
],
|
||||
).padding(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
...destinations.where((ele) => ele.isPinned).map((ele) {
|
||||
return NavigationDrawerDestination(
|
||||
icon: ele.icon,
|
||||
label: Text(ele.label).tr(),
|
||||
);
|
||||
}),
|
||||
const Divider(),
|
||||
...destinations.where((ele) => !ele.isPinned).map((ele) {
|
||||
return NavigationDrawerDestination(
|
||||
icon: ele.icon,
|
||||
label: Text(ele.label).tr(),
|
||||
);
|
||||
}),
|
||||
],
|
||||
onDestinationSelected: (idx) {
|
||||
nav.setIndex(idx);
|
||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
if (cfg.hideBottomNav)
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: nav.destinations.where((ele) => ele.isPinned).map(
|
||||
(ele) {
|
||||
return Expanded(
|
||||
child: Tooltip(
|
||||
message: ele.label.tr(),
|
||||
child: IconButton.filledTonal(
|
||||
icon: ele.icon,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
onPressed: () {
|
||||
GoRouter.of(context).goNamed(ele.screen);
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
).padding(horizontal: 16),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: AccountImage(content: ua.user?.avatar),
|
||||
title: Text(ua.user?.nick ?? 'unknown'.tr()).fontSize(15),
|
||||
subtitle:
|
||||
Text('@${ua.user?.name ?? 'unknown'.tr()}').fontSize(13),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.notifications, fill: 1),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('notification');
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.settings, fill: 1),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('settings');
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('account');
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
),
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DrawerContentList extends StatelessWidget {
|
||||
const _DrawerContentList();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ct = context.read<ChatChannelProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final nav = context.watch<NavigationProvider>();
|
||||
final rel = context.read<SnRealmProvider>();
|
||||
|
||||
return PageTransitionSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (Widget child, Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Colors.transparent,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: nav.focusedRealm == null
|
||||
? ListView(
|
||||
key: const Key('realm-list-view'),
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Solar Network').bold(),
|
||||
AppVersionLabel(),
|
||||
],
|
||||
).padding(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
contentPadding: EdgeInsets.only(left: 28, right: 16),
|
||||
leading: const Icon(Symbols.home),
|
||||
title: Text('screenHome').tr(),
|
||||
onTap: () {
|
||||
GoRouter.of(context).goNamed('home');
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
),
|
||||
...rel.availableRealms.map((ele) {
|
||||
return ListTile(
|
||||
minTileHeight: 48,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: AccountImage(
|
||||
content: ele.avatar,
|
||||
radius: 16,
|
||||
),
|
||||
title: Text(ele.name),
|
||||
onTap: () {
|
||||
nav.setFocusedRealm(ele);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
)
|
||||
: ListView(
|
||||
key: ValueKey(nav.focusedRealm),
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
if (nav.focusedRealm!.banner != null)
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(
|
||||
nav.focusedRealm!.banner!,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 24,
|
||||
right: 16,
|
||||
),
|
||||
leading: AccountImage(
|
||||
content: nav.focusedRealm!.avatar,
|
||||
radius: 16,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () {
|
||||
nav.setFocusedRealm(null);
|
||||
},
|
||||
),
|
||||
title: Text(nav.focusedRealm!.name),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmDetail',
|
||||
pathParameters: {
|
||||
'alias': nav.focusedRealm!.alias,
|
||||
},
|
||||
);
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 28,
|
||||
right: 8,
|
||||
),
|
||||
leading: const Icon(Symbols.globe),
|
||||
title: Text('community').tr(),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmCommunity',
|
||||
pathParameters: {
|
||||
'alias': nav.focusedRealm!.alias,
|
||||
},
|
||||
);
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
),
|
||||
if (ct.availableChannels
|
||||
.where((ele) => ele.realmId == nav.focusedRealm?.id)
|
||||
.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
...(ct.availableChannels
|
||||
.where((ele) => ele.realmId == nav.focusedRealm?.id)
|
||||
.map((ele) {
|
||||
return ListTile(
|
||||
minTileHeight: 48,
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 28,
|
||||
right: 8,
|
||||
),
|
||||
leading: const Icon(Symbols.tag),
|
||||
title: Text(ele.name),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {
|
||||
'scope': ele.realm?.alias ?? 'global',
|
||||
'alias': ele.alias,
|
||||
},
|
||||
);
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
);
|
||||
}))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -107,6 +107,7 @@ class AppRootScaffold extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
final nav = context.watch<NavigationProvider>();
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
final isCollapseDrawer = cfg.drawerIsCollapsed;
|
||||
@ -118,8 +119,9 @@ class AppRootScaffold extends StatelessWidget {
|
||||
.last
|
||||
.route
|
||||
.name;
|
||||
final isShowBottomNavigation =
|
||||
NavigationProvider.kShowBottomNavScreen.contains(routeName)
|
||||
final isShowBottomNavigation = cfg.hideBottomNav
|
||||
? false
|
||||
: nav.showBottomNavScreen.contains(routeName)
|
||||
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
||||
: false;
|
||||
final isPopable = !NavigationProvider.kAllDestination
|
||||
|
168
lib/widgets/post/fediverse_post_item.dart
Normal file
168
lib/widgets/post/fediverse_post_item.dart
Normal file
@ -0,0 +1,168 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class FediversePostWidget extends StatelessWidget {
|
||||
final SnFediversePost data;
|
||||
final double maxWidth;
|
||||
const FediversePostWidget({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.maxWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AccountImage(
|
||||
content: data.user.avatar,
|
||||
radius: 20,
|
||||
),
|
||||
const Gap(12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.user.nick.isNotEmpty
|
||||
? data.user.nick
|
||||
: '@${data.user.name}',
|
||||
maxLines: 1,
|
||||
).bold(),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
data.user.identifier.contains('@')
|
||||
? data.user.identifier
|
||||
: '${data.user.identifier}@${data.user.origin}',
|
||||
maxLines: 1,
|
||||
).fontSize(13),
|
||||
const Gap(4),
|
||||
Text(
|
||||
RelativeTime(context)
|
||||
.format(data.createdAt.toLocal()),
|
||||
).fontSize(13),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12, vertical: 8),
|
||||
MarkdownTextContent(
|
||||
isAutoWarp: true,
|
||||
content: html2md.convert(data.content),
|
||||
).padding(horizontal: 16, bottom: 6),
|
||||
if (data.images.isNotEmpty)
|
||||
_FediversePostImageList(
|
||||
data: data,
|
||||
maxWidth: maxWidth,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FediversePostImageList extends StatelessWidget {
|
||||
const _FediversePostImageList({
|
||||
required this.data,
|
||||
required this.maxWidth,
|
||||
});
|
||||
|
||||
final SnFediversePost data;
|
||||
final double maxWidth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderSide =
|
||||
BorderSide(width: 1, color: Theme.of(context).dividerColor);
|
||||
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||
|
||||
if (data.images.length == 1) {
|
||||
return AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AutoResizeUniversalImage(
|
||||
data.images.first,
|
||||
),
|
||||
),
|
||||
),
|
||||
).padding(horizontal: 8);
|
||||
}
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ScrollConfiguration(
|
||||
behavior: AttachmentListScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: data.images.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
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: AutoResizeUniversalImage(
|
||||
data.images[idx],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(
|
||||
label: Text('${idx + 1}/${data.images.length}'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -14,14 +15,13 @@ import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
import '../../providers/sn_network.dart';
|
||||
|
||||
class PostCommentQuickAction extends StatelessWidget {
|
||||
final double? maxWidth;
|
||||
final SnPost parentPost;
|
||||
final Function? onPosted;
|
||||
|
||||
const PostCommentQuickAction({super.key, this.maxWidth, required this.parentPost, this.onPosted});
|
||||
const PostCommentQuickAction(
|
||||
{super.key, this.maxWidth, required this.parentPost, this.onPosted});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -30,7 +30,9 @@ class PostCommentQuickAction extends StatelessWidget {
|
||||
return Container(
|
||||
height: 240,
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.symmetric(vertical: 8) : EdgeInsets.zero,
|
||||
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? const EdgeInsets.symmetric(vertical: 8)
|
||||
: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? const BorderRadius.all(Radius.circular(8))
|
||||
@ -99,7 +101,8 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
Future<void> _selectAnswer(SnPost answer) async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
|
||||
await sn.client
|
||||
.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
|
||||
'publisher': answer.publisherId,
|
||||
'answer_id': answer.id,
|
||||
});
|
||||
@ -135,7 +138,10 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
child: PostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: widget.maxWidth,
|
||||
onSelectAnswer: widget.parentPost.type == 'question' ? () => _selectAnswer(_posts[idx]) : null,
|
||||
showExpandableComments: true,
|
||||
onSelectAnswer: widget.parentPost.type == 'question'
|
||||
? () => _selectAnswer(_posts[idx])
|
||||
: null,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
@ -145,6 +151,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {'slug': _posts[idx].id.toString()},
|
||||
@ -153,7 +160,8 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider().padding(vertical: 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -161,11 +169,13 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
class PostCommentListPopup extends StatefulWidget {
|
||||
final SnPost post;
|
||||
final int commentCount;
|
||||
final int depth;
|
||||
|
||||
const PostCommentListPopup({
|
||||
super.key,
|
||||
required this.post,
|
||||
this.commentCount = 0,
|
||||
this.depth = 1,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -180,48 +190,57 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
||||
final ua = context.watch<UserProvider>();
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.comment, size: 24),
|
||||
const Gap(16),
|
||||
Text('postCommentsDetailed').plural(widget.commentCount).textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (ua.isAuthorized)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 240,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.85,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.comment, size: 24),
|
||||
const Gap(16),
|
||||
Text('postCommentsDetailed')
|
||||
.plural(widget.commentCount)
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (ua.isAuthorized)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
height: 240,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PostMiniEditor(
|
||||
postReplyId: widget.post.id,
|
||||
onPost: () {
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
child: PostMiniEditor(
|
||||
postReplyId: widget.post.id,
|
||||
onPost: () {
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
onExpand: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
PostCommentSliverList(
|
||||
parentPost: widget.post,
|
||||
key: _childListKey,
|
||||
),
|
||||
PostCommentSliverList(
|
||||
parentPost: widget.post,
|
||||
key: _childListKey,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,7 @@ import 'dart:ui';
|
||||
import 'package:croppy/croppy.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -491,6 +492,14 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _selectFile() async {
|
||||
final result = await FilePicker.platform.pickFiles(type: FileType.any);
|
||||
if (result == null) return;
|
||||
onAdd(
|
||||
result.files.map((e) => PostWriteMedia.fromFile(e.xFile)),
|
||||
);
|
||||
}
|
||||
|
||||
void _pasteMedia() async {
|
||||
final imageBytes = await Pasteboard.image;
|
||||
if (imageBytes == null) return;
|
||||
@ -605,6 +614,18 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
_selectMedia();
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.file_upload),
|
||||
const Gap(16),
|
||||
Text('addAttachmentFromFiles').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
_selectFile();
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
|
@ -9,6 +9,7 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/post/post_editor.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -17,8 +18,10 @@ import 'package:surface/widgets/loading_indicator.dart';
|
||||
class PostMiniEditor extends StatefulWidget {
|
||||
final int? postReplyId;
|
||||
final Function? onPost;
|
||||
final Function? onExpand;
|
||||
|
||||
const PostMiniEditor({super.key, this.postReplyId, this.onPost});
|
||||
const PostMiniEditor(
|
||||
{super.key, this.postReplyId, this.onPost, this.onExpand});
|
||||
|
||||
@override
|
||||
State<PostMiniEditor> createState() => _PostMiniEditorState();
|
||||
@ -214,12 +217,16 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
extra: PostEditorExtra(
|
||||
text: _writeController.contentController.text,
|
||||
),
|
||||
queryParameters: {
|
||||
if (widget.postReplyId != null)
|
||||
'replying': widget.postReplyId.toString(),
|
||||
'mode': 'stories',
|
||||
},
|
||||
);
|
||||
widget.onExpand?.call();
|
||||
},
|
||||
),
|
||||
TextButton.icon(
|
||||
|
@ -80,59 +80,64 @@ class _PostPollState extends State<PostPoll> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
for (final option in _poll.options)
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: MediaQuery.of(context).size.width *
|
||||
(_poll.metric.byOptionsPercentage[option.id] ?? 0)
|
||||
.toDouble(),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
minTileHeight: 60,
|
||||
leading: _answeredChoice == option.id
|
||||
? const Icon(Symbols.circle, fill: 1)
|
||||
: const Icon(Symbols.circle),
|
||||
title: Text(option.name),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
for (final option in _poll.options)
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: constraints.maxWidth *
|
||||
(_poll.metric.byOptionsPercentage[option.id] ?? 0)
|
||||
.toDouble(),
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
minTileHeight: 60,
|
||||
leading: _answeredChoice == option.id
|
||||
? const Icon(Symbols.circle, fill: 1)
|
||||
: const Icon(Symbols.circle),
|
||||
title: Text(option.name),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'pollVotes'
|
||||
.plural(_poll.metric.byOptions[option.id] ?? 0),
|
||||
),
|
||||
Text(' · ').padding(horizontal: 4),
|
||||
Text(
|
||||
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%',
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'pollVotes'.plural(
|
||||
_poll.metric.byOptions[option.id] ?? 0),
|
||||
),
|
||||
Text(' · ').padding(horizontal: 4),
|
||||
Text(
|
||||
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (option.description.isNotEmpty)
|
||||
Text(option.description),
|
||||
],
|
||||
),
|
||||
if (option.description.isNotEmpty)
|
||||
Text(option.description),
|
||||
],
|
||||
),
|
||||
onTap: _isBusy ? null : () => _voteForOption(option),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
onTap: _isBusy ? null : () => _voteForOption(option),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,36 +14,37 @@ class UnauthorizedHint extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.login, size: 36),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'unauthorized',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr(),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'unauthorizedDescription',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
).tr(),
|
||||
],
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.login, size: 36),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'unauthorized',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'unauthorizedDescription',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('authLogin').then((value) {
|
||||
if (value == true && context.mounted) {
|
||||
final ua = context.read<UserProvider>();
|
||||
context.showSnackbar('loginSuccess'.tr(args: [
|
||||
'@${ua.user?.name} (${ua.user?.nick})',
|
||||
]));
|
||||
}
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('authLogin').then((value) {
|
||||
if (value == true && context.mounted) {
|
||||
final ua = context.read<UserProvider>();
|
||||
context.showSnackbar('loginSuccess'.tr(args: [
|
||||
'@${ua.user?.name} (${ua.user?.nick})',
|
||||
]));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -102,6 +102,6 @@ static void my_application_init(MyApplication* self) {}
|
||||
MyApplication* my_application_new() {
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID,
|
||||
"flags", G_APPLICATION_NON_UNIQUE,
|
||||
"flags", G_APPLICATION_DEFAULT_FLAGS,
|
||||
nullptr));
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ import flutter_timezone
|
||||
import flutter_udid
|
||||
import flutter_webrtc
|
||||
import gal
|
||||
import geolocator_apple
|
||||
import hotkey_manager_macos
|
||||
import in_app_review
|
||||
import livekit_client
|
||||
@ -56,7 +55,6 @@ 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"))
|
||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
|
||||
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
||||
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
|
||||
|
@ -90,8 +90,6 @@ PODS:
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- geolocator_apple (1.2.0):
|
||||
- FlutterMacOS
|
||||
- GoogleAppMeasurement (11.8.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.8.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -148,7 +146,7 @@ PODS:
|
||||
- HotKey
|
||||
- in_app_review (2.0.0):
|
||||
- FlutterMacOS
|
||||
- livekit_client (2.4.0):
|
||||
- livekit_client (2.4.1):
|
||||
- flutter_webrtc
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@ -192,6 +190,8 @@ PODS:
|
||||
- sqlite3/common
|
||||
- sqlite3/fts5 (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/math (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/perf-threadsafe (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/rtree (3.49.1):
|
||||
@ -202,6 +202,7 @@ PODS:
|
||||
- sqlite3 (~> 3.49.1)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/math
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- tray_manager (0.0.1):
|
||||
@ -232,7 +233,6 @@ DEPENDENCIES:
|
||||
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
||||
- geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos`)
|
||||
- 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`)
|
||||
@ -307,8 +307,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral
|
||||
gal:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
||||
geolocator_apple:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos
|
||||
hotkey_manager_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
|
||||
in_app_review:
|
||||
@ -372,14 +370,13 @@ SPEC CHECKSUMS:
|
||||
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
geolocator_apple: 72a78ae3f3e4ec0db62117bd93e34523f5011d58
|
||||
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
|
||||
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
|
||||
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
||||
livekit_client: 2e766be2c3ee6274a8e2633b356b98b5eb842987
|
||||
livekit_client: d03409f83df069a1bb00a4c8dc78c54fb2287262
|
||||
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff
|
||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||
@ -396,7 +393,7 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
||||
sqlite3_flutter_libs: 487032b9008b28de37c72a3aa66849ef3745f3e6
|
||||
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
|
||||
|
@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
148
pubspec.lock
148
pubspec.lock
@ -53,10 +53,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -213,10 +213,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: chalkdart
|
||||
sha256: "08c910ee341fcdd1e46f20ddce59b13c1d85f5d97f2fd2f12014c46ede670e40"
|
||||
sha256: "82dfa884e3cf97641eb0742a3b9ffd41490666b9ece548b2e32cbfefe540bf86"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -301,10 +301,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: croppy
|
||||
sha256: "99f4fbb4a4b44d2712e8dcd61c57c1acac151bd53cab11de3babec80407ed266"
|
||||
sha256: "2a69059d9ec007b79d6a494854094b2e3c0a4f7ed609cf55a4805c9de9ec171d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.5"
|
||||
version: "1.3.6"
|
||||
cross_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -314,7 +314,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.3.4+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
@ -517,10 +517,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fast_rsa
|
||||
sha256: "205a36c0412b9fabebf3e18ccb5221d819cc28cfb3da988c0bf7b646368d0270"
|
||||
sha256: a26ad752734dc52fd51abd55248df868d7480e68d8cc8dd12413b0124bba0a7e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.8.0"
|
||||
version: "3.8.1"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -541,10 +541,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: "7423298f08f6fc8cce05792bae329f9a93653fc9c08712831b1a55540127995d"
|
||||
sha256: "8d938fd5c11dc81bf1acd4f7f0486c683fe9e79a0b13419e27730f9ce4d8a25b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.2"
|
||||
version: "9.2.1"
|
||||
file_saver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -702,6 +702,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
flutter_blurhash:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_blurhash
|
||||
sha256: "5e67678e479ac639069d7af1e133f4a4702311491188ff3e0227486430db0c06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.2"
|
||||
flutter_cache_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -738,10 +746,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_expandable_fab
|
||||
sha256: "85275279d19faf4fbe5639dc1f139b4555b150e079d056f085601a45688af12c"
|
||||
sha256: b14caf78720a48f650e6e1a38d724e33b1f5348d646fa1c266570c31a7f87ef3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.4.0"
|
||||
flutter_highlight:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -937,18 +945,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_webrtc
|
||||
sha256: "6ea3a86d95b61cfe42d5715426d355b3cece6c88d0119de428d56f6c653811ce"
|
||||
sha256: b832dc76c0d1577f14aaf35e9c38d4ed7667cbc89c492b7bf4505d8d5f62e08b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.11"
|
||||
version: "0.12.12+hotfix.1"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: a6274c34d61b3d68082f2b0e9a641a3ec197e525d269f35b82f62d5b2c6d9f75
|
||||
sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.4"
|
||||
freezed_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -981,54 +989,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
geolocator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: geolocator
|
||||
sha256: d2ec66329cab29cb297d51d96c067d457ca519dca8589665fa0b82ebacb7dbe4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.2"
|
||||
geolocator_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_android
|
||||
sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.1"
|
||||
geolocator_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_apple
|
||||
sha256: c4ecead17985ede9634f21500072edfcb3dba0ef7b97f8d7bc556d2d722b3ba3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.9"
|
||||
geolocator_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_platform_interface
|
||||
sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
geolocator_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_web
|
||||
sha256: "2ed69328e05cd94e7eb48bb0535f5fc0c0c44d1c4fa1e9737267484d05c29b5e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
geolocator_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_windows
|
||||
sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1133,6 +1093,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.5"
|
||||
html2md:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html2md
|
||||
sha256: "465cf8ffa1b510fe0e97941579bf5b22e2d575f2cecb500a9c0254efe33a8036"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1209,10 +1177,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
|
||||
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
version: "0.2.1+2"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1294,7 +1262,7 @@ packages:
|
||||
source: hosted
|
||||
version: "6.9.4"
|
||||
latlong2:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: latlong2
|
||||
sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe"
|
||||
@ -1353,10 +1321,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: livekit_client
|
||||
sha256: "753bbf484c6b70f10f3dc1dc808dfe3755f472d80eb9682323cff07ad8e2609d"
|
||||
sha256: "7f489fa415253d8d99c649b7efc95a733c5e5ac38dcfb02362ced99feb139376"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.1"
|
||||
local_notifier:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1417,10 +1385,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: material_symbols_icons
|
||||
sha256: ca30ccbd97763353bde6bb1076aa4f4d17a40db0804384da77df142102aa225d
|
||||
sha256: "99d5b0e7c65232dfe1247e0ac67eeeee2cab9da2d860748fc495d34f5e9e6397"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2808.0"
|
||||
version: "4.2811.0"
|
||||
media_kit:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1553,10 +1521,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.2.0"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -2134,10 +2102,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
|
||||
sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.31"
|
||||
version: "0.5.32"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2206,34 +2174,34 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker
|
||||
sha256: "5ab7d974ad92042b3e2382441c41ec4c6e5b3fa2b4b024d8ccbfc4bc2244b7bb"
|
||||
sha256: "45abef5b92f9b9bd42c3f20133ad4b20ab12e1da2aa206fc0a40ea874bed7c5d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.14"
|
||||
version: "4.7.1"
|
||||
talker_dio_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker_dio_logger
|
||||
sha256: "71780c52951d36e94964ca06158d827dfc67aa2fb75c8b880603cfefa4377b39"
|
||||
sha256: "52c1b554cccedec6073637a6d4f6a3e267dd4451c1545fe57e1b26897a560ccb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.14"
|
||||
version: "4.7.1"
|
||||
talker_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker_flutter
|
||||
sha256: "0cc816260b226c0ff930909c9f22984316b652b140f5eabb97ae9813ee0de135"
|
||||
sha256: "77458ca11638dfefb651e898a26101ee54e60dc0b168ad7481a05b1c97ce2680"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.14"
|
||||
version: "4.7.1"
|
||||
talker_logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: talker_logger
|
||||
sha256: "16ff0cfdf011f65b37957c9ff7ef7043dd9f1c8af3ccb4a44ac4a448defb9eb5"
|
||||
sha256: ed9b20b8c09efff9f6b7c63fc6630ee2f84aa92661ae09e5ba04e77272bf2ad2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.14"
|
||||
version: "4.7.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2250,6 +2218,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
timelines_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: timelines_plus
|
||||
sha256: be31f493402dc24df7fe410dc5f82a605807bb4ca13183de6d4362886449b593
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2526,10 +2502,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: b89e6e24d1454e149ab20fbb225af58660f0c0bf4475544650700d8e2da54aef
|
||||
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.11.0"
|
||||
version: "5.12.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
10
pubspec.yaml
10
pubspec.yaml
@ -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.4.2+78
|
||||
version: 2.4.2+83
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@ -135,10 +135,14 @@ dependencies:
|
||||
talker: ^4.6.14
|
||||
flutter_cache_manager: ^3.4.1
|
||||
flutter_timezone: ^4.1.0
|
||||
flutter_map: ^8.1.0
|
||||
geolocator: ^13.0.2
|
||||
flutter_map: ^8.1.1
|
||||
fast_rsa: ^3.8.0
|
||||
flutter_card_swiper: ^7.0.2
|
||||
html2md: ^1.3.2
|
||||
flutter_blurhash: ^0.8.2
|
||||
timelines_plus: ^1.0.6
|
||||
latlong2: ^0.9.1
|
||||
crypto: ^3.0.6
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -6,6 +6,7 @@ import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v1.dart' as v1;
|
||||
import 'schema_v2.dart' as v2;
|
||||
import 'schema_v3.dart' as v3;
|
||||
import 'schema_v4.dart' as v4;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
@ -17,10 +18,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
return v2.DatabaseAtV2(db);
|
||||
case 3:
|
||||
return v3.DatabaseAtV3(db);
|
||||
case 4:
|
||||
return v4.DatabaseAtV4(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [1, 2, 3];
|
||||
static const versions = const [1, 2, 3, 4];
|
||||
}
|
||||
|
2391
test/drift/my_database/generated/schema_v4.dart
Normal file
2391
test/drift/my_database/generated/schema_v4.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,6 @@
|
||||
#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 <geolocator_windows/geolocator_windows.h>
|
||||
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
|
||||
#include <livekit_client/live_kit_plugin.h>
|
||||
#include <local_notifier/local_notifier_plugin.h>
|
||||
@ -54,8 +53,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
||||
GalPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("GalPluginCApi"));
|
||||
GeolocatorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||
HotkeyManagerWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
|
||||
LiveKitPluginRegisterWithRegistrar(
|
||||
|
@ -14,7 +14,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
gal
|
||||
geolocator_windows
|
||||
hotkey_manager_windows
|
||||
livekit_client
|
||||
local_notifier
|
||||
|
@ -8,8 +8,29 @@ auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
|
||||
#include "flutter_window.h"
|
||||
#include "utils.h"
|
||||
|
||||
HANDLE g_hMutex = NULL;
|
||||
|
||||
bool CheckIfAlreadyRunning() {
|
||||
g_hMutex = CreateMutex(NULL, FALSE, L"Global\\SolianDesktop");
|
||||
|
||||
if (g_hMutex == NULL) {
|
||||
return true; // Mutex creation failed
|
||||
}
|
||||
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
CloseHandle(g_hMutex);
|
||||
return true; // Another instance is running
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||
if (CheckIfAlreadyRunning()) {
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
// Attach to console when present (e.g., 'flutter run') or create a
|
||||
// new console when running with a debugger.
|
||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||
|
Reference in New Issue
Block a user