Compare commits

..

17 Commits

Author SHA1 Message Date
3c0e4046a4 ♻️ Refactor to replace Hive with Sqlite 2025-02-22 20:43:24 +08:00
338c22a606 Add sqlite3 dependency 2025-02-22 16:22:33 +08:00
25dd895e0d 💄 Optimize category selector 2025-02-22 14:58:20 +08:00
ea9ef9e82a ♻️ Refactored explore page 2025-02-22 14:52:58 +08:00
edd86eda77 Realm posts 2025-02-22 13:13:38 +08:00
671b857a79 💄 Optimize realm
 Post realm post
2025-02-22 12:25:56 +08:00
408fd0f35e 🐛 Bug fixes 2025-02-22 01:33:57 +08:00
30184d08b1 ♻️ Refactor the way to set thumbnail 2025-02-21 21:50:36 +08:00
LittleSheep
95f257c47a Merge pull request #7 from I21b/master 2025-02-21 00:16:41 +08:00
92
41297c6712 📃 Add issue templates
en and zh
2025-02-21 00:49:21 +09:00
a8e0ade0c8 Realm Popularity 2025-02-20 23:44:28 +08:00
3338e699c4 💄 Memorable realm view style 2025-02-20 22:09:05 +08:00
e07da3efa5 Sliding window pricing of attachment billing info displaying 2025-02-20 21:19:23 +08:00
4f7f015250 ⬆️ I forgot what did I did last night 2025-02-20 20:41:41 +08:00
2a4c15d0dc 💄 Optimize About page 2025-02-20 20:41:25 +08:00
70ef894ec5 ♻️ Transferable chat channel 2025-02-18 23:34:59 +08:00
bb9179d5f9 🐛 Fix drawer remain when device rotate 2025-02-18 16:52:15 +08:00
57 changed files with 17014 additions and 618 deletions

87
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,87 @@
name: Bug report
description: Create a report to help us address issues you are facing
title: "[Bug] "
labels: [Bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to make us better!
- type: checkboxes
id: duplication
attributes:
label:
options:
- label: This issue is not duplicated with any other open or closed issues
required: true
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is
placeholder: |
Example:
App crashes on startup every time after changing settings.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen
placeholder: |
Example:
App started normally, everything worked fine.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Steps to reproduce the bug
placeholder: |
Example:
1. Change "HyperNet Server" to "127.0.1" in "Network" settings
2. Restart the app
3. Crash
validations:
required: true
- type: textarea
id: environment
attributes:
label: Device information
description: Provide details about your system environment
placeholder: |
Example:
Device: Google Pixel 8 Pro
System: Baklava (BP22.250124.009)
Version*: 2.3.2
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem
placeholder: |
Example:
setting_items.jpg
crash_screen.jpg
validations:
required: false
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here
placeholder: |
Crash report or other useful informations
validations:
required: false

View File

@@ -0,0 +1,83 @@
name: 问题反馈
description: 提交 Bug 或其它问题的反馈
title: "[Bug] 标题"
labels: [Bug]
body:
- type: markdown
attributes:
value: |
非常感谢,你将要提交的反馈会让我们变得更好!
- type: checkboxes
id: duplication
attributes:
label:
options:
- label: 我已经搜索并确认此 issue 不与其它任何 issue 重复
required: true
- type: textarea
id: description
attributes:
label: 问题描述
description: 清楚且详细地描述你遇到的 Bug 或问题
placeholder: |
发生了什么?生动地描述你所看到的一切
validations:
required: true
- type: textarea
id: expected
attributes:
label: 期望表现
description: 清楚且详细地描述你期望发生的事
placeholder: |
什么功能应该正常运行,运行后会有什么结果
什么界面应该正常显示,应该会显示什么内容
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: 复现步骤
description: 能够复现问题的每一步
placeholder: |
1. 尽可能详细地描述每一步
2. 更改的设置、添加的好友...
3. 这里也可以描述你看到的界面
validations:
required: true
- type: textarea
id: environment
attributes:
label: 环境/版本
description: 提供运行时的环境信息
placeholder: |
示例:
设备型号: Google Pixel 8 Pro
系统板本: Baklava (BP22.250124.009)
程序版本: 2.3.2
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: 屏幕截图/录制
description: 提供截屏或录屏来更好地描述问题
placeholder: |
错误显示的界面/崩溃时的界面、先前改动的设置
validations:
required: false
- type: textarea
id: additional
attributes:
label: 更多信息
description: 任何与问题有关且有用的信息
placeholder: |
崩溃报告、日志,或是你的用户名
validations:
required: false

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Solsynth Releases
url: https://files.solsynth.dev/production01/solian
about: Another place to download released apps

View File

@@ -0,0 +1,59 @@
name: Feature request
description: Suggest features you want to add or suggest to modify existing features
title: "[Feature] "
labels: [Feature]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to make us better!
- type: checkboxes
id: duplication
attributes:
label:
options:
- label: This issue is not duplicated with any other open or closed issues
required: true
- type: textarea
id: description
attributes:
label: Describe the feature
description: A clear and concise description of what the feature is
placeholder: |
Example:
A Quick Settings tile to start the service, long press to launch the app.
validations:
required: true
- type: textarea
id: reasons
attributes:
label: Reason for adding
description: Explain why this feature would be useful to you
placeholder: |
Example:
Start the service quickly from the Quick Settings tile and save lots of time.
validations:
required: true
- type: textarea
id: examples
attributes:
label: Example(s)
description: Post screenshots/drawings/links/etc of the feature request, or proof-of-concept images about the feature
placeholder: |
Example:
shazam_toggle.jpg
nekobox_switch.jpg
validations:
required: false
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the feature here
validations:
required: false

View File

@@ -0,0 +1,49 @@
name: 功能建议
description: 提出你想要添加或更改的功能
title: "[Feature] 标题"
labels: [Feature]
body:
- type: markdown
attributes:
value: |
非常感谢,你将要提交的请求会让我们变得更好!
- type: checkboxes
id: duplication
attributes:
label:
options:
- label: 我已经搜索并确认此 issue 不与其它任何 issue 重复
required: true
- type: textarea
id: description
attributes:
label: 功能描述
description: 清楚且详细地描述要添加/更改后的功能
validations:
required: true
- type: textarea
id: reasons
attributes:
label: 添加/更改理由
description: 解释为什么要这样做,对用户有什么好处
validations:
required: true
- type: textarea
id: examples
attributes:
label: 功能示例
description: 相似/已存在功能的截图,或画出大致的界面
validations:
required: false
- type: textarea
id: additional
attributes:
label: 更多信息
description: 任何与功能有关且有用的信息,或已存在功能的代码/仓库
validations:
required: false

View File

@@ -12,9 +12,9 @@ post {
body:json { body:json {
{ {
"alias": "BaLoading", "alias": "Deadge",
"name": "BaLoading", "name": "Dead",
"attachment_id": "2JCI2uh21mKkfk9P", "attachment_id": "pcbFd0u4zgdM39HM",
"pack_id": 3 "pack_id": 4
} }
} }

View File

@@ -5,7 +5,7 @@ meta {
} }
post { post {
url: {{endpoint}}/cgi/id/dev/notify/122 url: {{endpoint}}/cgi/id/dev/notify/328
body: json body: json
auth: inherit auth: inherit
} }
@@ -15,9 +15,9 @@ body:json {
"client_id": "{{third_client_id}}", "client_id": "{{third_client_id}}",
"client_secret":"{{third_client_tk}}", "client_secret":"{{third_client_tk}}",
"type": "general", "type": "general",
"subject": "处理该帐号 @solian 的决定", "subject": "处理该发布者 @vedal987 的决定",
"subtitle": "违反用户协议", "subtitle": "一条来自 Solar Network 客户支持的信息",
"content": "您的帐号违反了我们用户协议中关于冒充我们官方的行为,至此做出停权的决定。还请见谅。该决定是最终决定,不接受上诉。", "content": "您的发布者违反了我们用户协议中的「禁止冒充他人」的相关条例,经管理决定,将相关内容隐藏。冒充他人的判定无论作者是否有主观意志,只要造成了误解我们就有责任处理。希望您能理解,本次决定未作出任何帐号相关的连带处罚。",
"priority": 10 "priority": 10
} }
} }

View File

@@ -548,6 +548,7 @@
"termAcceptNextWithAgree": "By clicking the \"Next\", it means you agree to our terms and its updates.", "termAcceptNextWithAgree": "By clicking the \"Next\", it means you agree to our terms and its updates.",
"unauthorized": "Unauthorized", "unauthorized": "Unauthorized",
"unauthorizedDescription": "Login to explore the entire Solar Network.", "unauthorizedDescription": "Login to explore the entire Solar Network.",
"projectDetail": "Project Details",
"serviceStatus": "Service Status", "serviceStatus": "Service Status",
"termRelated": "Related Terms", "termRelated": "Related Terms",
"appDetails": "App Details", "appDetails": "App Details",
@@ -583,6 +584,7 @@
"colorSchemeBlack": "Black", "colorSchemeBlack": "Black",
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.", "colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
"postFeaturedComment": "Featured Comment", "postFeaturedComment": "Featured Comment",
"postCategory": "Category",
"postCategoryTechnology": "Technology", "postCategoryTechnology": "Technology",
"postCategoryGaming": "Gaming", "postCategoryGaming": "Gaming",
"postCategoryLife": "Life", "postCategoryLife": "Life",
@@ -625,6 +627,7 @@
"realmJoin": "Join Realm", "realmJoin": "Join Realm",
"realmCommunityHint": "This realm is a community realm, you can freely join.", "realmCommunityHint": "This realm is a community realm, you can freely join.",
"realmCommunityPublicChannelsHint": "The public channels in this realm", "realmCommunityPublicChannelsHint": "The public channels in this realm",
"realmCommunityPublishersHint": "The publishers in this realm",
"realmJoined": "Joined realm {}.", "realmJoined": "Joined realm {}.",
"join": "Join", "join": "Join",
"pollEditorNew": "New Poll", "pollEditorNew": "New Poll",
@@ -665,5 +668,24 @@
"zero": "No views", "zero": "No views",
"one": "{} view", "one": "{} view",
"other": "{} views" "other": "{} views"
} },
"attachmentBillingUploaded": "Used space",
"attachmentBillingDiscount": "Free space",
"attachmentBillingRatio": "Usage",
"attachmentBillingHint": "Sliding Window Pricing®\nFees will only apply if the size of the file uploaded within 24 hours exceeds the free space.",
"postThumbnail": "Post Thumbnail",
"accountRealms": "Realms",
"postInGlobal": "Global",
"postInGlobalDescription": "Do not link this post with any realm.",
"postChannelGlobal": "Global",
"postChannelFriends": "Friends",
"postChannelFollowing": "Following",
"postChannelRealm": "Realms",
"postFilterReset": "Reset Filter",
"postFilterResetDescription": "Clear filter and show all posts.",
"postFilterWithCategory": "Viewing posts in {}",
"databaseSize": "Database Size",
"databaseDelete": "Delete Database",
"databaseDeleteDescription": "Remove the database on your local disk, the content will be fetched from server again.",
"databaseDeleted": "The local database has been deleted."
} }

View File

@@ -546,6 +546,7 @@
"termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。", "termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。",
"unauthorized": "未登陆", "unauthorized": "未登陆",
"unauthorizedDescription": "登陆以探索整个 Solar Network。", "unauthorizedDescription": "登陆以探索整个 Solar Network。",
"projectDetail": "项目详情",
"serviceStatus": "服务状态", "serviceStatus": "服务状态",
"termRelated": "相关条款", "termRelated": "相关条款",
"appDetails": "应用程序详情", "appDetails": "应用程序详情",
@@ -581,6 +582,7 @@
"colorSchemeBlack": "黑色", "colorSchemeBlack": "黑色",
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。", "colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
"postFeaturedComment": "精选评论", "postFeaturedComment": "精选评论",
"postCategory": "分类",
"postCategoryTechnology": "技术", "postCategoryTechnology": "技术",
"postCategoryGaming": "游戏", "postCategoryGaming": "游戏",
"postCategoryLife": "生活", "postCategoryLife": "生活",
@@ -624,6 +626,7 @@
"realmJoin": "加入领域", "realmJoin": "加入领域",
"realmCommunityHint": "该领域是一个社区领域,你可以自由加入。", "realmCommunityHint": "该领域是一个社区领域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "该领域包含的公共频道", "realmCommunityPublicChannelsHint": "该领域包含的公共频道",
"realmCommunityPublishersHint": "该领域的发布者",
"realmJoined": "已加入领域 {}。", "realmJoined": "已加入领域 {}。",
"join": "加入", "join": "加入",
"pollEditorNew": "新投票", "pollEditorNew": "新投票",
@@ -664,5 +667,23 @@
"zero": "{} 次浏览", "zero": "{} 次浏览",
"one": "{} 次浏览", "one": "{} 次浏览",
"other": "{} 次浏览" "other": "{} 次浏览"
} },
"attachmentBillingUploaded": "已占用的字节数",
"attachmentBillingDiscount": "免费的字节数",
"attachmentBillingHint": "滑动窗口计价®\n在24小时内上传的文件大小超出免费空间才会适用扣费。",
"postThumbnail": "帖子缩略图",
"accountRealms": "领域",
"postInGlobal": "全站",
"postInGlobalDescription": "不关联此帖子与任何领域。",
"postChannelGlobal": "全站",
"postChannelFriends": "好友",
"postChannelFollowing": "关注",
"postChannelRealm": "领域",
"postFilterReset": "重置过滤器",
"postFilterResetDescription": "清除过滤器并显示所有帖子。",
"postFilterWithCategory": "查看{}区中的帖子",
"databaseSize": "数据库大小",
"databaseDelete": "删除数据库",
"databaseDeleteDescription": "删除本地数据库,内容将从服务器重新获取。",
"databaseDeleted": "本地数据库已被删除。"
} }

View File

@@ -546,6 +546,7 @@
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。", "termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
"unauthorized": "未登陸", "unauthorized": "未登陸",
"unauthorizedDescription": "登陸以探索整個 Solar Network。", "unauthorizedDescription": "登陸以探索整個 Solar Network。",
"projectDetail": "項目詳情",
"serviceStatus": "服務狀態", "serviceStatus": "服務狀態",
"termRelated": "相關條款", "termRelated": "相關條款",
"appDetails": "應用程序詳情", "appDetails": "應用程序詳情",
@@ -581,6 +582,7 @@
"colorSchemeBlack": "黑色", "colorSchemeBlack": "黑色",
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。", "colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
"postFeaturedComment": "精選評論", "postFeaturedComment": "精選評論",
"postCategory": "分類",
"postCategoryTechnology": "技術", "postCategoryTechnology": "技術",
"postCategoryGaming": "遊戲", "postCategoryGaming": "遊戲",
"postCategoryLife": "生活", "postCategoryLife": "生活",
@@ -624,6 +626,7 @@
"realmJoin": "加入領域", "realmJoin": "加入領域",
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。", "realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道", "realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
"realmCommunityPublishersHint": "該領域的發佈者",
"realmJoined": "已加入領域 {}。", "realmJoined": "已加入領域 {}。",
"join": "加入", "join": "加入",
"pollEditorNew": "新投票", "pollEditorNew": "新投票",
@@ -664,5 +667,23 @@
"zero": "{} 次瀏覽", "zero": "{} 次瀏覽",
"one": "{} 次瀏覽", "one": "{} 次瀏覽",
"other": "{} 次瀏覽" "other": "{} 次瀏覽"
} },
"attachmentBillingUploaded": "已佔用的字節數",
"attachmentBillingDiscount": "免費的字節數",
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。",
"postThumbnail": "帖子縮略圖",
"accountRealms": "領域",
"postInGlobal": "全站",
"postInGlobalDescription": "不關聯此帖子與任何領域。",
"postChannelGlobal": "全站",
"postChannelFriends": "好友",
"postChannelFollowing": "關注",
"postChannelRealm": "領域",
"postFilterReset": "重置過濾器",
"postFilterResetDescription": "清除過濾器並顯示所有帖子。",
"postFilterWithCategory": "查看{}區中的帖子",
"databaseSize": "數據庫大小",
"databaseDelete": "刪除數據庫",
"databaseDeleteDescription": "刪除本地數據庫,內容將從服務器重新獲取。",
"databaseDeleted": "本地數據庫已被刪除。"
} }

View File

@@ -546,6 +546,7 @@
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。", "termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
"unauthorized": "未登陸", "unauthorized": "未登陸",
"unauthorizedDescription": "登陸以探索整個 Solar Network。", "unauthorizedDescription": "登陸以探索整個 Solar Network。",
"projectDetail": "項目詳情",
"serviceStatus": "服務狀態", "serviceStatus": "服務狀態",
"termRelated": "相關條款", "termRelated": "相關條款",
"appDetails": "應用程序詳情", "appDetails": "應用程序詳情",
@@ -581,6 +582,7 @@
"colorSchemeBlack": "黑色", "colorSchemeBlack": "黑色",
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。", "colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
"postFeaturedComment": "精選評論", "postFeaturedComment": "精選評論",
"postCategory": "分類",
"postCategoryTechnology": "技術", "postCategoryTechnology": "技術",
"postCategoryGaming": "遊戲", "postCategoryGaming": "遊戲",
"postCategoryLife": "生活", "postCategoryLife": "生活",
@@ -624,6 +626,7 @@
"realmJoin": "加入領域", "realmJoin": "加入領域",
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。", "realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道", "realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
"realmCommunityPublishersHint": "該領域的發佈者",
"realmJoined": "已加入領域 {}。", "realmJoined": "已加入領域 {}。",
"join": "加入", "join": "加入",
"pollEditorNew": "新投票", "pollEditorNew": "新投票",
@@ -664,5 +667,23 @@
"zero": "{} 次瀏覽", "zero": "{} 次瀏覽",
"one": "{} 次瀏覽", "one": "{} 次瀏覽",
"other": "{} 次瀏覽" "other": "{} 次瀏覽"
} },
"attachmentBillingUploaded": "已佔用的字節數",
"attachmentBillingDiscount": "免費的字節數",
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。",
"postThumbnail": "帖子縮略圖",
"accountRealms": "領域",
"postInGlobal": "全站",
"postInGlobalDescription": "不關聯此帖子與任何領域。",
"postChannelGlobal": "全站",
"postChannelFriends": "好友",
"postChannelFollowing": "關注",
"postChannelRealm": "領域",
"postFilterReset": "重置過濾器",
"postFilterResetDescription": "清除過濾器並顯示所有帖子。",
"postFilterWithCategory": "查看{}區中的帖子",
"databaseSize": "數據庫大小",
"databaseDelete": "刪除數據庫",
"databaseDeleteDescription": "刪除本地數據庫,內容將從服務器重新獲取。",
"databaseDeleted": "本地數據庫已被刪除。"
} }

View File

@@ -221,6 +221,25 @@ PODS:
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (3.49.0):
- sqlite3/common (= 3.49.0)
- sqlite3/common (3.49.0)
- sqlite3/dbstatvtab (3.49.0):
- sqlite3/common
- sqlite3/fts5 (3.49.0):
- sqlite3/common
- sqlite3/perf-threadsafe (3.49.0):
- sqlite3/common
- sqlite3/rtree (3.49.0):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.49.0)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
- SwiftyGif (5.4.5) - SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
@@ -268,6 +287,7 @@ DEPENDENCIES:
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_compress (from `.symlinks/plugins/video_compress/ios`) - video_compress (from `.symlinks/plugins/video_compress/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`) - volume_controller (from `.symlinks/plugins/volume_controller/ios`)
@@ -294,6 +314,7 @@ SPEC REPOS:
- PromisesObjC - PromisesObjC
- SAMKeychain - SAMKeychain
- SDWebImage - SDWebImage
- sqlite3
- SwiftyGif - SwiftyGif
- WebRTC-SDK - WebRTC-SDK
@@ -360,6 +381,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin: sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin" :path: ".symlinks/plugins/sqflite_darwin/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
video_compress: video_compress:
@@ -421,6 +444,8 @@ SPEC CHECKSUMS:
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: 4922312598b67e1825c6a6c821296dcbf6783046
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe

View File

@@ -2,11 +2,12 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/database/database.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
@@ -16,13 +17,13 @@ import 'package:surface/types/websocket.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatMessageController extends ChangeNotifier { class ChatMessageController extends ChangeNotifier {
static const kChatMessageBoxPrefix = 'nex_chat_messages_';
static const kSingleBatchLoadLimit = 100; static const kSingleBatchLoadLimit = 100;
late final SnNetworkProvider _sn; late final SnNetworkProvider _sn;
late final UserDirectoryProvider _ud; late final UserDirectoryProvider _ud;
late final WebSocketProvider _ws; late final WebSocketProvider _ws;
late final SnAttachmentProvider _attach; late final SnAttachmentProvider _attach;
late final DatabaseProvider _dt;
StreamSubscription? _wsSubscription; StreamSubscription? _wsSubscription;
@@ -31,6 +32,7 @@ class ChatMessageController extends ChangeNotifier {
_ud = context.read<UserDirectoryProvider>(); _ud = context.read<UserDirectoryProvider>();
_ws = context.read<WebSocketProvider>(); _ws = context.read<WebSocketProvider>();
_attach = context.read<SnAttachmentProvider>(); _attach = context.read<SnAttachmentProvider>();
_dt = context.read<DatabaseProvider>();
} }
bool isPending = true; bool isPending = true;
@@ -38,9 +40,9 @@ class ChatMessageController extends ChangeNotifier {
int? messageTotal; int? messageTotal;
bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!; bool get isAllLoaded =>
messageTotal != null && messages.length >= messageTotal!;
String? _boxKey;
SnChannel? channel; SnChannel? channel;
SnChannelMember? profile; SnChannelMember? profile;
@@ -51,25 +53,17 @@ class ChatMessageController extends ChangeNotifier {
/// Stored as a list of nonce to provide the loading state /// Stored as a list of nonce to provide the loading state
final List<String> unconfirmedMessages = List.empty(growable: true); final List<String> unconfirmedMessages = List.empty(growable: true);
Box<SnChatMessage>? get _box => (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
final List<SnChannelMember> typingMembers = List.empty(growable: true); final List<SnChannelMember> typingMembers = List.empty(growable: true);
final Map<int, Timer> typingInactiveTimer = {}; final Map<int, Timer> typingInactiveTimer = {};
Future<void> initialize(SnChannel chan) async { Future<void> initialize(SnChannel chan) async {
channel = chan; channel = chan;
// Initialize local data
_boxKey = '$kChatMessageBoxPrefix${chan.id}';
await Hive.openBox<SnChatMessage>(_boxKey!);
// Fetch channel profile // Fetch channel profile
final resp = await _sn.client.get( final resp = await _sn.client.get(
'/cgi/im/channels/${chan.keyPath}/me', '/cgi/im/channels/${chan.keyPath}/me',
); );
profile = SnChannelMember.fromJson( profile = SnChannelMember.fromJson(resp.data);
resp.data as Map<String, dynamic>,
);
_wsSubscription = _ws.pk.stream.listen((event) { _wsSubscription = _ws.pk.stream.listen((event) {
switch (event.method) { switch (event.method) {
@@ -87,7 +81,8 @@ class ChatMessageController extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
typingInactiveTimer[member.id]?.cancel(); typingInactiveTimer[member.id]?.cancel();
typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () { typingInactiveTimer[member.id] =
Timer(const Duration(seconds: 3), () {
typingMembers.removeWhere((x) => x.id == member.id); typingMembers.removeWhere((x) => x.id == member.id);
typingInactiveTimer.remove(member.id); typingInactiveTimer.remove(member.id);
notifyListeners(); notifyListeners();
@@ -129,10 +124,16 @@ class ChatMessageController extends ChangeNotifier {
} }
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async { Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
if (_box == null) return; await _dt.db.snLocalChatMessage.insertAll(
await _box!.putAll({ messages.map(
for (final message in messages) message.id: message, (ele) => SnLocalChatMessageCompanion.insert(
}); id: Value(ele.id),
content: ele,
channelId: channel!.id,
createdAt: Value(ele.createdAt),
),
),
onConflict: DoNothing());
} }
Future<void> _addUnconfirmedMessage(SnChatMessage message) async { Future<void> _addUnconfirmedMessage(SnChatMessage message) async {
@@ -184,8 +185,21 @@ class ChatMessageController extends ChangeNotifier {
await _applyMessage(message); await _applyMessage(message);
notifyListeners(); notifyListeners();
if (_box == null) return; if (isCheckedUpdate) {
await _box!.put(message.id, message); await _dt.db.snLocalChatMessage.insertOne(
SnLocalChatMessageCompanion.insert(
id: Value(message.id),
content: message,
channelId: channel!.id,
createdAt: Value(message.createdAt),
),
onConflict: DoUpdate((_) => SnLocalChatMessageCompanion.custom(
content: Constant(jsonEncode(message.toJson())),
)),
);
} else {
incomeStrandedQueue.add(message);
}
} }
Future<void> _applyMessage(SnChatMessage message) async { Future<void> _applyMessage(SnChatMessage message) async {
@@ -194,7 +208,8 @@ class ChatMessageController extends ChangeNotifier {
switch (message.type) { switch (message.type) {
case 'messages.edit': case 'messages.edit':
if (message.relatedEventId != null) { if (message.relatedEventId != null) {
final idx = messages.indexWhere((x) => x.id == message.relatedEventId); final idx =
messages.indexWhere((x) => x.id == message.relatedEventId);
if (idx != -1) { if (idx != -1) {
final newBody = message.body; final newBody = message.body;
newBody.remove('related_event'); newBody.remove('related_event');
@@ -202,16 +217,24 @@ class ChatMessageController extends ChangeNotifier {
body: newBody, body: newBody,
updatedAt: message.updatedAt, updatedAt: message.updatedAt,
); );
if (_box!.containsKey(message.relatedEventId)) { if (message.relatedEventId != null) {
await _box!.put(message.relatedEventId, messages[idx]); await (_dt.db.snLocalChatMessage.update()
..where((e) => e.id.equals(message.relatedEventId!)))
.write(
SnLocalChatMessageCompanion.custom(
content: Constant(jsonEncode(messages[idx].toJson())),
),
);
} }
} }
} }
case 'messages.delete': case 'messages.delete':
if (message.relatedEventId != null) { if (message.relatedEventId != null) {
messages.removeWhere((x) => x.id == message.relatedEventId); messages.removeWhere((x) => x.id == message.relatedEventId);
if (_box!.containsKey(message.relatedEventId)) { if (message.relatedEventId != null) {
await _box!.delete(message.relatedEventId); await (_dt.db.snLocalChatMessage.delete()
..where((e) => e.id.equals(message.relatedEventId!)))
.go();
} }
} }
} }
@@ -233,7 +256,8 @@ class ChatMessageController extends ChangeNotifier {
'algorithm': 'plain', 'algorithm': 'plain',
if (quoteId != null) 'quote_event': quoteId, if (quoteId != null) 'quote_event': quoteId,
if (relatedId != null) 'related_event': relatedId, if (relatedId != null) 'related_event': relatedId,
if (attachments != null && attachments.isNotEmpty) 'attachments': attachments, if (attachments != null && attachments.isNotEmpty)
'attachments': attachments,
}; };
// Mock the message locally // Mock the message locally
@@ -287,20 +311,34 @@ class ChatMessageController extends ChangeNotifier {
} }
} }
bool isCheckedUpdate = false;
List<SnChatMessage> incomeStrandedQueue = List.empty(growable: true);
/// Check the local storage is up to date with the server. /// Check the local storage is up to date with the server.
/// If the local storage is not up to date, it will be updated. /// If the local storage is not up to date, it will be updated.
Future<void> checkUpdate() async { Future<void> checkUpdate() async {
if (_box == null) return;
if (_box!.isEmpty) return;
isLoading = true; isLoading = true;
notifyListeners(); notifyListeners();
final mostRecentMessage = await (_dt.db.snLocalChatMessage.select()
..limit(1)
..orderBy([
(e) =>
OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
]))
.getSingleOrNull();
if (mostRecentMessage == null) {
// Initial load
await loadMessages(take: 20);
isCheckedUpdate = true;
return;
}
try { try {
final resp = await _sn.client.get( final resp = await _sn.client.get(
'/cgi/im/channels/${channel!.keyPath}/events/update', '/cgi/im/channels/${channel!.keyPath}/events/update',
queryParameters: { queryParameters: {
'pivot': _box!.values.last.id, 'pivot': mostRecentMessage.content.id,
}, },
); );
if (resp.data['up_to_date'] == true) return; if (resp.data['up_to_date'] == true) return;
@@ -316,6 +354,12 @@ class ChatMessageController extends ChangeNotifier {
} finally { } finally {
await loadMessages(); await loadMessages();
isLoading = false; isLoading = false;
isCheckedUpdate = true;
_saveMessageToLocal(incomeStrandedQueue).then((_) {
incomeStrandedQueue.clear();
});
notifyListeners(); notifyListeners();
} }
} }
@@ -324,13 +368,18 @@ class ChatMessageController extends ChangeNotifier {
/// If it was not found in local storage we will look it up in remote /// If it was not found in local storage we will look it up in remote
Future<SnChatMessage?> getMessage(int id) async { Future<SnChatMessage?> getMessage(int id) async {
SnChatMessage? out; SnChatMessage? out;
if (_box != null && _box!.containsKey(id)) { final local = await (_dt.db.snLocalChatMessage.select()
out = _box!.get(id); ..limit(1)
..where((e) => e.id.equals(id)))
.getSingleOrNull();
if (local != null) {
out = local.content;
} }
if (out == null) { if (out == null) {
try { try {
final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id'); final resp = await _sn.client
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
out = SnChatMessage.fromJson(resp.data); out = SnChatMessage.fromJson(resp.data);
_saveMessageToLocal([out]); _saveMessageToLocal([out]);
} catch (_) { } catch (_) {
@@ -364,16 +413,21 @@ class ChatMessageController extends ChangeNotifier {
bool forceLocal = false, bool forceLocal = false,
bool forceRemote = false, bool forceRemote = false,
}) async { }) async {
final localTotal = await _dt.db.snLocalChatMessage
.count(where: (e) => e.channelId.equals(channel!.id))
.getSingle();
late List<SnChatMessage> out; late List<SnChatMessage> out;
if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) { if ((localTotal >= take + offset || forceLocal) && !forceRemote) {
out = _box!.keys final result = await (_dt.db.snLocalChatMessage.select()
.toList() ..where((e) => e.channelId.equals(channel!.id))
.cast<int>() ..orderBy([
.sorted((a, b) => b.compareTo(a)) (e) =>
.skip(offset) OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
.take(take) ])
.map((key) => _box!.get(key)!) ..limit(take, offset: offset))
.toList(); .get();
out = result.map((e) => e.content).toList();
} else { } else {
final resp = await _sn.client.get( final resp = await _sn.client.get(
'/cgi/im/channels/${channel!.keyPath}/events', '/cgi/im/channels/${channel!.keyPath}/events',
@@ -408,7 +462,8 @@ class ChatMessageController extends ChangeNotifier {
quoteEvent: quoteEvent, quoteEvent: quoteEvent,
attachments: attachments attachments: attachments
.where( .where(
(ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false, (ele) =>
out[i].body['attachments']?.contains(ele?.rid) ?? false,
) )
.toList(), .toList(),
), ),
@@ -416,7 +471,10 @@ class ChatMessageController extends ChangeNotifier {
} }
// Preload sender accounts // Preload sender accounts
final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet(); final accountId = out
.where((ele) => ele.sender.accountId >= 0)
.map((ele) => ele.sender.accountId)
.toSet();
await _ud.listAccount(accountId); await _ud.listAccount(accountId);
return out; return out;
@@ -443,7 +501,6 @@ class ChatMessageController extends ChangeNotifier {
@override @override
void dispose() { void dispose() {
_box?.close();
_wsSubscription?.cancel(); _wsSubscription?.cancel();
super.dispose(); super.dispose();
} }

View File

@@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/poll.dart'; import 'package:surface/types/poll.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:video_compress/video_compress.dart'; import 'package:video_compress/video_compress.dart';
@@ -159,12 +160,13 @@ class PostWriteController extends ChangeNotifier {
final TextEditingController rewardController = TextEditingController(); final TextEditingController rewardController = TextEditingController();
ContentInsertionConfiguration get contentInsertionConfiguration => ContentInsertionConfiguration( ContentInsertionConfiguration get contentInsertionConfiguration => ContentInsertionConfiguration(
onContentInserted: (KeyboardInsertedContent content) { onContentInserted: (KeyboardInsertedContent content) {
if (content.hasData) { if (content.hasData) {
addAttachments([PostWriteMedia.fromBytes(content.data!, 'attachmentInsertedImage'.tr(), SnMediaType.image)]); addAttachments(
} [PostWriteMedia.fromBytes(content.data!, 'attachmentInsertedImage'.tr(), SnMediaType.image)]);
}, }
); },
);
bool _temporarySaveActive = false; bool _temporarySaveActive = false;
@@ -196,6 +198,7 @@ class PostWriteController extends ChangeNotifier {
bool isLoading = false, isBusy = false; bool isLoading = false, isBusy = false;
double? progress; double? progress;
SnRealm? realm;
SnPublisher? publisher; SnPublisher? publisher;
SnPost? editingPost, repostingPost, replyingPost; SnPost? editingPost, repostingPost, replyingPost;
@@ -244,6 +247,9 @@ class PostWriteController extends ChangeNotifier {
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) { if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
thumbnail = PostWriteMedia(post.preload!.thumbnail); thumbnail = PostWriteMedia(post.preload!.thumbnail);
} }
if (post.preload?.realm != null) {
realm = post.preload!.realm!;
}
editingPost = post; editingPost = post;
} }
@@ -379,6 +385,7 @@ class PostWriteController extends ChangeNotifier {
if (replyingPost != null) 'reply_to': replyingPost!.toJson(), if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
if (repostingPost != null) 'repost_to': repostingPost!.toJson(), if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
if (poll != null) 'poll': poll!.toJson(), if (poll != null) 'poll': poll!.toJson(),
if (realm != null) 'realm': realm!.toJson(),
}), }),
); );
}); });
@@ -409,6 +416,7 @@ class PostWriteController extends ChangeNotifier {
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null; replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null; repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null; poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null;
realm = data['realm'] != null ? SnRealm.fromJson(data['realm']) : null;
temporaryRestored = true; temporaryRestored = true;
notifyListeners(); notifyListeners();
}); });
@@ -525,6 +533,7 @@ class PostWriteController extends ChangeNotifier {
if (reward != null) 'reward': reward, if (reward != null) 'reward': reward,
if (videoAttachment != null) 'video': videoAttachment!.rid, if (videoAttachment != null) 'video': videoAttachment!.rid,
if (poll != null) 'poll': poll!.id, if (poll != null) 'poll': poll!.id,
if (realm != null) 'realm': realm!.id,
}, },
onSendProgress: (count, total) { onSendProgress: (count, total) {
progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2); progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
@@ -571,17 +580,8 @@ class PostWriteController extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void setThumbnail(int? idx) { void setThumbnail(SnAttachment? value) {
if (idx == null) { thumbnail = value == null ? null : PostWriteMedia(value);
attachments.add(thumbnail!);
thumbnail = null;
} else {
if (thumbnail != null) {
attachments.add(thumbnail!);
}
thumbnail = attachments[idx];
attachments.removeAt(idx);
}
notifyListeners(); notifyListeners();
} }
@@ -633,6 +633,11 @@ class PostWriteController extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void setRealm(SnRealm? value) {
realm = value;
notifyListeners();
}
void setProgress(double? value) { void setProgress(double? value) {
progress = value; progress = value;
_temporaryPlanSave(); _temporaryPlanSave();

74
lib/database/chat.dart Normal file
View File

@@ -0,0 +1,74 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:surface/types/chat.dart';
class SnChannelConverter extends TypeConverter<SnChannel, String>
with JsonTypeConverter2<SnChannel, String, Map<String, Object?>> {
const SnChannelConverter();
@override
SnChannel fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnChannel value) {
return jsonEncode(toJson(value));
}
@override
SnChannel fromJson(Map<String, Object?> json) {
return SnChannel.fromJson(json);
}
@override
Map<String, Object?> toJson(SnChannel value) {
return value.toJson();
}
}
class SnLocalChatChannel extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get alias => text()();
TextColumn get content => text().map(const SnChannelConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
class SnMessageConverter extends TypeConverter<SnChatMessage, String>
with JsonTypeConverter2<SnChatMessage, String, Map<String, Object?>> {
const SnMessageConverter();
@override
SnChatMessage fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnChatMessage value) {
return jsonEncode(toJson(value));
}
@override
SnChatMessage fromJson(Map<String, Object?> json) {
return SnChatMessage.fromJson(json);
}
@override
Map<String, Object?> toJson(SnChatMessage value) {
return value.toJson();
}
}
class SnLocalChatMessage extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get channelId => integer()();
TextColumn get content => text().map(const SnMessageConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}

View File

@@ -0,0 +1,28 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:surface/database/chat.dart';
import 'package:surface/types/chat.dart';
part 'database.g.dart';
@DriftDatabase(tables: [SnLocalChatChannel, SnLocalChatMessage])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(
name: 'solar_network_data',
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
web: DriftWebOptions(
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
driftWorker: Uri.parse('drift_worker.dart.js'),
),
);
}
}

View File

@@ -0,0 +1,880 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'database.dart';
// ignore_for_file: type=lint
class $SnLocalChatChannelTable extends SnLocalChatChannel
with TableInfo<$SnLocalChatChannelTable, SnLocalChatChannelData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$SnLocalChatChannelTable(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);
static const VerificationMeta _contentMeta =
const VerificationMeta('content');
@override
late final GeneratedColumnWithTypeConverter<SnChannel, String> content =
GeneratedColumn<String>('content', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<SnChannel>($SnLocalChatChannelTable.$convertercontent);
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);
@override
List<GeneratedColumn> get $columns => [id, alias, content, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'sn_local_chat_channel';
@override
VerificationContext validateIntegrity(
Insertable<SnLocalChatChannelData> 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);
}
context.handle(_contentMeta, const VerificationResult.success());
if (data.containsKey('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
SnLocalChatChannelData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return SnLocalChatChannelData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
alias: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}alias'])!,
content: $SnLocalChatChannelTable.$convertercontent.fromSql(
attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}content'])!),
createdAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
);
}
@override
$SnLocalChatChannelTable createAlias(String alias) {
return $SnLocalChatChannelTable(attachedDatabase, alias);
}
static JsonTypeConverter2<SnChannel, String, Map<String, Object?>>
$convertercontent = const SnChannelConverter();
}
class SnLocalChatChannelData extends DataClass
implements Insertable<SnLocalChatChannelData> {
final int id;
final String alias;
final SnChannel content;
final DateTime createdAt;
const SnLocalChatChannelData(
{required this.id,
required this.alias,
required this.content,
required this.createdAt});
@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>(
$SnLocalChatChannelTable.$convertercontent.toSql(content));
}
map['created_at'] = Variable<DateTime>(createdAt);
return map;
}
SnLocalChatChannelCompanion toCompanion(bool nullToAbsent) {
return SnLocalChatChannelCompanion(
id: Value(id),
alias: Value(alias),
content: Value(content),
createdAt: Value(createdAt),
);
}
factory SnLocalChatChannelData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return SnLocalChatChannelData(
id: serializer.fromJson<int>(json['id']),
alias: serializer.fromJson<String>(json['alias']),
content: $SnLocalChatChannelTable.$convertercontent
.fromJson(serializer.fromJson<Map<String, Object?>>(json['content'])),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@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?>>(
$SnLocalChatChannelTable.$convertercontent.toJson(content)),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
SnLocalChatChannelData copyWith(
{int? id, String? alias, SnChannel? content, DateTime? createdAt}) =>
SnLocalChatChannelData(
id: id ?? this.id,
alias: alias ?? this.alias,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
);
SnLocalChatChannelData copyWithCompanion(SnLocalChatChannelCompanion data) {
return SnLocalChatChannelData(
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,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('SnLocalChatChannelData(')
..write('id: $id, ')
..write('alias: $alias, ')
..write('content: $content, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, alias, content, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is SnLocalChatChannelData &&
other.id == this.id &&
other.alias == this.alias &&
other.content == this.content &&
other.createdAt == this.createdAt);
}
class SnLocalChatChannelCompanion
extends UpdateCompanion<SnLocalChatChannelData> {
final Value<int> id;
final Value<String> alias;
final Value<SnChannel> content;
final Value<DateTime> createdAt;
const SnLocalChatChannelCompanion({
this.id = const Value.absent(),
this.alias = const Value.absent(),
this.content = const Value.absent(),
this.createdAt = const Value.absent(),
});
SnLocalChatChannelCompanion.insert({
this.id = const Value.absent(),
required String alias,
required SnChannel content,
this.createdAt = const Value.absent(),
}) : alias = Value(alias),
content = Value(content);
static Insertable<SnLocalChatChannelData> custom({
Expression<int>? id,
Expression<String>? alias,
Expression<String>? content,
Expression<DateTime>? createdAt,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (alias != null) 'alias': alias,
if (content != null) 'content': content,
if (createdAt != null) 'created_at': createdAt,
});
}
SnLocalChatChannelCompanion copyWith(
{Value<int>? id,
Value<String>? alias,
Value<SnChannel>? content,
Value<DateTime>? createdAt}) {
return SnLocalChatChannelCompanion(
id: id ?? this.id,
alias: alias ?? this.alias,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
);
}
@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>(
$SnLocalChatChannelTable.$convertercontent.toSql(content.value));
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('SnLocalChatChannelCompanion(')
..write('id: $id, ')
..write('alias: $alias, ')
..write('content: $content, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
}
class $SnLocalChatMessageTable extends SnLocalChatMessage
with TableInfo<$SnLocalChatMessageTable, SnLocalChatMessageData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$SnLocalChatMessageTable(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 _channelIdMeta =
const VerificationMeta('channelId');
@override
late final GeneratedColumn<int> channelId = GeneratedColumn<int>(
'channel_id', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
static const VerificationMeta _contentMeta =
const VerificationMeta('content');
@override
late final GeneratedColumnWithTypeConverter<SnChatMessage, String> content =
GeneratedColumn<String>('content', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<SnChatMessage>(
$SnLocalChatMessageTable.$convertercontent);
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);
@override
List<GeneratedColumn> get $columns => [id, channelId, content, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'sn_local_chat_message';
@override
VerificationContext validateIntegrity(
Insertable<SnLocalChatMessageData> 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('channel_id')) {
context.handle(_channelIdMeta,
channelId.isAcceptableOrUnknown(data['channel_id']!, _channelIdMeta));
} else if (isInserting) {
context.missing(_channelIdMeta);
}
context.handle(_contentMeta, const VerificationResult.success());
if (data.containsKey('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
SnLocalChatMessageData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return SnLocalChatMessageData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
channelId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}channel_id'])!,
content: $SnLocalChatMessageTable.$convertercontent.fromSql(
attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}content'])!),
createdAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
);
}
@override
$SnLocalChatMessageTable createAlias(String alias) {
return $SnLocalChatMessageTable(attachedDatabase, alias);
}
static JsonTypeConverter2<SnChatMessage, String, Map<String, Object?>>
$convertercontent = const SnMessageConverter();
}
class SnLocalChatMessageData extends DataClass
implements Insertable<SnLocalChatMessageData> {
final int id;
final int channelId;
final SnChatMessage content;
final DateTime createdAt;
const SnLocalChatMessageData(
{required this.id,
required this.channelId,
required this.content,
required this.createdAt});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['channel_id'] = Variable<int>(channelId);
{
map['content'] = Variable<String>(
$SnLocalChatMessageTable.$convertercontent.toSql(content));
}
map['created_at'] = Variable<DateTime>(createdAt);
return map;
}
SnLocalChatMessageCompanion toCompanion(bool nullToAbsent) {
return SnLocalChatMessageCompanion(
id: Value(id),
channelId: Value(channelId),
content: Value(content),
createdAt: Value(createdAt),
);
}
factory SnLocalChatMessageData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return SnLocalChatMessageData(
id: serializer.fromJson<int>(json['id']),
channelId: serializer.fromJson<int>(json['channelId']),
content: $SnLocalChatMessageTable.$convertercontent
.fromJson(serializer.fromJson<Map<String, Object?>>(json['content'])),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'channelId': serializer.toJson<int>(channelId),
'content': serializer.toJson<Map<String, Object?>>(
$SnLocalChatMessageTable.$convertercontent.toJson(content)),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
SnLocalChatMessageData copyWith(
{int? id,
int? channelId,
SnChatMessage? content,
DateTime? createdAt}) =>
SnLocalChatMessageData(
id: id ?? this.id,
channelId: channelId ?? this.channelId,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
);
SnLocalChatMessageData copyWithCompanion(SnLocalChatMessageCompanion data) {
return SnLocalChatMessageData(
id: data.id.present ? data.id.value : this.id,
channelId: data.channelId.present ? data.channelId.value : this.channelId,
content: data.content.present ? data.content.value : this.content,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('SnLocalChatMessageData(')
..write('id: $id, ')
..write('channelId: $channelId, ')
..write('content: $content, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, channelId, content, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is SnLocalChatMessageData &&
other.id == this.id &&
other.channelId == this.channelId &&
other.content == this.content &&
other.createdAt == this.createdAt);
}
class SnLocalChatMessageCompanion
extends UpdateCompanion<SnLocalChatMessageData> {
final Value<int> id;
final Value<int> channelId;
final Value<SnChatMessage> content;
final Value<DateTime> createdAt;
const SnLocalChatMessageCompanion({
this.id = const Value.absent(),
this.channelId = const Value.absent(),
this.content = const Value.absent(),
this.createdAt = const Value.absent(),
});
SnLocalChatMessageCompanion.insert({
this.id = const Value.absent(),
required int channelId,
required SnChatMessage content,
this.createdAt = const Value.absent(),
}) : channelId = Value(channelId),
content = Value(content);
static Insertable<SnLocalChatMessageData> custom({
Expression<int>? id,
Expression<int>? channelId,
Expression<String>? content,
Expression<DateTime>? createdAt,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (channelId != null) 'channel_id': channelId,
if (content != null) 'content': content,
if (createdAt != null) 'created_at': createdAt,
});
}
SnLocalChatMessageCompanion copyWith(
{Value<int>? id,
Value<int>? channelId,
Value<SnChatMessage>? content,
Value<DateTime>? createdAt}) {
return SnLocalChatMessageCompanion(
id: id ?? this.id,
channelId: channelId ?? this.channelId,
content: content ?? this.content,
createdAt: createdAt ?? this.createdAt,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (channelId.present) {
map['channel_id'] = Variable<int>(channelId.value);
}
if (content.present) {
map['content'] = Variable<String>(
$SnLocalChatMessageTable.$convertercontent.toSql(content.value));
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('SnLocalChatMessageCompanion(')
..write('id: $id, ')
..write('channelId: $channelId, ')
..write('content: $content, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
}
abstract class _$AppDatabase extends GeneratedDatabase {
_$AppDatabase(QueryExecutor e) : super(e);
$AppDatabaseManager get managers => $AppDatabaseManager(this);
late final $SnLocalChatChannelTable snLocalChatChannel =
$SnLocalChatChannelTable(this);
late final $SnLocalChatMessageTable snLocalChatMessage =
$SnLocalChatMessageTable(this);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities =>
[snLocalChatChannel, snLocalChatMessage];
}
typedef $$SnLocalChatChannelTableCreateCompanionBuilder
= SnLocalChatChannelCompanion Function({
Value<int> id,
required String alias,
required SnChannel content,
Value<DateTime> createdAt,
});
typedef $$SnLocalChatChannelTableUpdateCompanionBuilder
= SnLocalChatChannelCompanion Function({
Value<int> id,
Value<String> alias,
Value<SnChannel> content,
Value<DateTime> createdAt,
});
class $$SnLocalChatChannelTableFilterComposer
extends Composer<_$AppDatabase, $SnLocalChatChannelTable> {
$$SnLocalChatChannelTableFilterComposer({
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<SnChannel, SnChannel, String> get content =>
$composableBuilder(
column: $table.content,
builder: (column) => ColumnWithTypeConverterFilters(column));
ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => ColumnFilters(column));
}
class $$SnLocalChatChannelTableOrderingComposer
extends Composer<_$AppDatabase, $SnLocalChatChannelTable> {
$$SnLocalChatChannelTableOrderingComposer({
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<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
}
class $$SnLocalChatChannelTableAnnotationComposer
extends Composer<_$AppDatabase, $SnLocalChatChannelTable> {
$$SnLocalChatChannelTableAnnotationComposer({
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<SnChannel, String> get content =>
$composableBuilder(column: $table.content, builder: (column) => column);
GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
}
class $$SnLocalChatChannelTableTableManager extends RootTableManager<
_$AppDatabase,
$SnLocalChatChannelTable,
SnLocalChatChannelData,
$$SnLocalChatChannelTableFilterComposer,
$$SnLocalChatChannelTableOrderingComposer,
$$SnLocalChatChannelTableAnnotationComposer,
$$SnLocalChatChannelTableCreateCompanionBuilder,
$$SnLocalChatChannelTableUpdateCompanionBuilder,
(
SnLocalChatChannelData,
BaseReferences<_$AppDatabase, $SnLocalChatChannelTable,
SnLocalChatChannelData>
),
SnLocalChatChannelData,
PrefetchHooks Function()> {
$$SnLocalChatChannelTableTableManager(
_$AppDatabase db, $SnLocalChatChannelTable table)
: super(TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$$SnLocalChatChannelTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$$SnLocalChatChannelTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$$SnLocalChatChannelTableAnnotationComposer(
$db: db, $table: table),
updateCompanionCallback: ({
Value<int> id = const Value.absent(),
Value<String> alias = const Value.absent(),
Value<SnChannel> content = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
}) =>
SnLocalChatChannelCompanion(
id: id,
alias: alias,
content: content,
createdAt: createdAt,
),
createCompanionCallback: ({
Value<int> id = const Value.absent(),
required String alias,
required SnChannel content,
Value<DateTime> createdAt = const Value.absent(),
}) =>
SnLocalChatChannelCompanion.insert(
id: id,
alias: alias,
content: content,
createdAt: createdAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$SnLocalChatChannelTableProcessedTableManager = ProcessedTableManager<
_$AppDatabase,
$SnLocalChatChannelTable,
SnLocalChatChannelData,
$$SnLocalChatChannelTableFilterComposer,
$$SnLocalChatChannelTableOrderingComposer,
$$SnLocalChatChannelTableAnnotationComposer,
$$SnLocalChatChannelTableCreateCompanionBuilder,
$$SnLocalChatChannelTableUpdateCompanionBuilder,
(
SnLocalChatChannelData,
BaseReferences<_$AppDatabase, $SnLocalChatChannelTable,
SnLocalChatChannelData>
),
SnLocalChatChannelData,
PrefetchHooks Function()>;
typedef $$SnLocalChatMessageTableCreateCompanionBuilder
= SnLocalChatMessageCompanion Function({
Value<int> id,
required int channelId,
required SnChatMessage content,
Value<DateTime> createdAt,
});
typedef $$SnLocalChatMessageTableUpdateCompanionBuilder
= SnLocalChatMessageCompanion Function({
Value<int> id,
Value<int> channelId,
Value<SnChatMessage> content,
Value<DateTime> createdAt,
});
class $$SnLocalChatMessageTableFilterComposer
extends Composer<_$AppDatabase, $SnLocalChatMessageTable> {
$$SnLocalChatMessageTableFilterComposer({
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<int> get channelId => $composableBuilder(
column: $table.channelId, builder: (column) => ColumnFilters(column));
ColumnWithTypeConverterFilters<SnChatMessage, SnChatMessage, String>
get content => $composableBuilder(
column: $table.content,
builder: (column) => ColumnWithTypeConverterFilters(column));
ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => ColumnFilters(column));
}
class $$SnLocalChatMessageTableOrderingComposer
extends Composer<_$AppDatabase, $SnLocalChatMessageTable> {
$$SnLocalChatMessageTableOrderingComposer({
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<int> get channelId => $composableBuilder(
column: $table.channelId, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get content => $composableBuilder(
column: $table.content, builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
}
class $$SnLocalChatMessageTableAnnotationComposer
extends Composer<_$AppDatabase, $SnLocalChatMessageTable> {
$$SnLocalChatMessageTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<int> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<int> get channelId =>
$composableBuilder(column: $table.channelId, builder: (column) => column);
GeneratedColumnWithTypeConverter<SnChatMessage, String> get content =>
$composableBuilder(column: $table.content, builder: (column) => column);
GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
}
class $$SnLocalChatMessageTableTableManager extends RootTableManager<
_$AppDatabase,
$SnLocalChatMessageTable,
SnLocalChatMessageData,
$$SnLocalChatMessageTableFilterComposer,
$$SnLocalChatMessageTableOrderingComposer,
$$SnLocalChatMessageTableAnnotationComposer,
$$SnLocalChatMessageTableCreateCompanionBuilder,
$$SnLocalChatMessageTableUpdateCompanionBuilder,
(
SnLocalChatMessageData,
BaseReferences<_$AppDatabase, $SnLocalChatMessageTable,
SnLocalChatMessageData>
),
SnLocalChatMessageData,
PrefetchHooks Function()> {
$$SnLocalChatMessageTableTableManager(
_$AppDatabase db, $SnLocalChatMessageTable table)
: super(TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$$SnLocalChatMessageTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$$SnLocalChatMessageTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$$SnLocalChatMessageTableAnnotationComposer(
$db: db, $table: table),
updateCompanionCallback: ({
Value<int> id = const Value.absent(),
Value<int> channelId = const Value.absent(),
Value<SnChatMessage> content = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
}) =>
SnLocalChatMessageCompanion(
id: id,
channelId: channelId,
content: content,
createdAt: createdAt,
),
createCompanionCallback: ({
Value<int> id = const Value.absent(),
required int channelId,
required SnChatMessage content,
Value<DateTime> createdAt = const Value.absent(),
}) =>
SnLocalChatMessageCompanion.insert(
id: id,
channelId: channelId,
content: content,
createdAt: createdAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$SnLocalChatMessageTableProcessedTableManager = ProcessedTableManager<
_$AppDatabase,
$SnLocalChatMessageTable,
SnLocalChatMessageData,
$$SnLocalChatMessageTableFilterComposer,
$$SnLocalChatMessageTableOrderingComposer,
$$SnLocalChatMessageTableAnnotationComposer,
$$SnLocalChatMessageTableCreateCompanionBuilder,
$$SnLocalChatMessageTableUpdateCompanionBuilder,
(
SnLocalChatMessageData,
BaseReferences<_$AppDatabase, $SnLocalChatMessageTable,
SnLocalChatMessageData>
),
SnLocalChatMessageData,
PrefetchHooks Function()>;
class $AppDatabaseManager {
final _$AppDatabase _db;
$AppDatabaseManager(this._db);
$$SnLocalChatChannelTableTableManager get snLocalChatChannel =>
$$SnLocalChatChannelTableTableManager(_db, _db.snLocalChatChannel);
$$SnLocalChatMessageTableTableManager get snLocalChatMessage =>
$$SnLocalChatMessageTableTableManager(_db, _db.snLocalChatMessage);
}

View File

@@ -0,0 +1,8 @@
import 'package:drift/wasm.dart';
// Use `dart compile js -O4 ./drift_worker.dart` to compile this file.
// And place it in the web/ directory.
// When compiled with dart2js, this file defines a dedicated or shared web
// worker used by drift.
void main() => WasmDatabase.workerMainForOpen();

View File

@@ -24,6 +24,7 @@ import 'package:surface/firebase_options.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart'; import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/link_preview.dart'; import 'package:surface/providers/link_preview.dart';
import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/notification.dart'; import 'package:surface/providers/notification.dart';
@@ -31,6 +32,7 @@ import 'package:surface/providers/post.dart';
import 'package:surface/providers/relationship.dart'; import 'package:surface/providers/relationship.dart';
import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/providers/sn_sticker.dart'; import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/special_day.dart'; import 'package:surface/providers/special_day.dart';
import 'package:surface/providers/theme.dart'; import 'package:surface/providers/theme.dart';
@@ -87,7 +89,7 @@ void main() async {
Hive.registerAdapter(SnChannelMemberImplAdapter()); Hive.registerAdapter(SnChannelMemberImplAdapter());
Hive.registerAdapter(SnChatMessageImplAdapter()); Hive.registerAdapter(SnChatMessageImplAdapter());
if (kIsWeb && !Platform.isLinux) { if (!kIsWeb && !Platform.isLinux) {
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
@@ -141,6 +143,9 @@ class SolianApp extends StatelessWidget {
assetLoader: JsonAssetLoader(), assetLoader: JsonAssetLoader(),
child: MultiProvider( child: MultiProvider(
providers: [ providers: [
// Infrastructure layer
Provider(create: (ctx) => DatabaseProvider(ctx)),
// System extensions layer // System extensions layer
Provider(create: (ctx) => HomeWidgetProvider(ctx)), Provider(create: (ctx) => HomeWidgetProvider(ctx)),
@@ -155,6 +160,7 @@ class SolianApp extends StatelessWidget {
Provider(create: (ctx) => SnNetworkProvider(ctx)), Provider(create: (ctx) => SnNetworkProvider(ctx)),
Provider(create: (ctx) => UserDirectoryProvider(ctx)), Provider(create: (ctx) => UserDirectoryProvider(ctx)),
Provider(create: (ctx) => SnAttachmentProvider(ctx)), Provider(create: (ctx) => SnAttachmentProvider(ctx)),
Provider(create: (ctx) => SnRealmProvider(ctx)),
Provider(create: (ctx) => SnPostContentProvider(ctx)), Provider(create: (ctx) => SnPostContentProvider(ctx)),
Provider(create: (ctx) => SnRelationshipProvider(ctx)), Provider(create: (ctx) => SnRelationshipProvider(ctx)),
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)), Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
@@ -427,8 +433,16 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
}); });
return false; return false;
}, },
child: SizeChangedLayoutNotifier( child: OrientationBuilder(
child: widget.child, builder: (context, orientation) {
final cfg = context.read<ConfigProvider>();
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
return SizeChangedLayoutNotifier(
child: widget.child,
);
},
), ),
); );
} }

View File

@@ -1,7 +1,11 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/controllers/chat_message_controller.dart'; import 'package:surface/database/database.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
@@ -12,12 +16,12 @@ class ChatChannelProvider extends ChangeNotifier {
late final SnNetworkProvider _sn; late final SnNetworkProvider _sn;
late final UserDirectoryProvider _ud; late final UserDirectoryProvider _ud;
late final DatabaseProvider _dt;
Box<SnChannel>? get _channelBox => Hive.box<SnChannel>(kChatChannelBoxName);
ChatChannelProvider(BuildContext context) { ChatChannelProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>(); _sn = context.read<SnNetworkProvider>();
_ud = context.read<UserDirectoryProvider>(); _ud = context.read<UserDirectoryProvider>();
_dt = context.read<DatabaseProvider>();
_initializeLocalData(); _initializeLocalData();
} }
@@ -26,10 +30,23 @@ class ChatChannelProvider extends ChangeNotifier {
} }
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async { Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
if (_channelBox == null) return; await Future.wait(
await _channelBox!.putAll({ channels.map(
for (final channel in channels) channel.key: channel, (ele) => _dt.db.snLocalChatChannel.insertOne(
}); SnLocalChatChannelCompanion.insert(
id: Value(ele.id),
alias: ele.key,
content: ele,
createdAt: Value(ele.createdAt),
),
onConflict: DoUpdate(
(_) => SnLocalChatChannelCompanion.custom(
content: Constant(jsonEncode(ele.toJson())),
),
),
),
),
);
} }
Future<List<SnChannel>> _fetchChannelsFromServer({ Future<List<SnChannel>> _fetchChannelsFromServer({
@@ -54,12 +71,13 @@ class ChatChannelProvider extends ChangeNotifier {
/// It will use the local storage as much as possible. /// It will use the local storage as much as possible.
/// The alias should include the scope, formatted as `scope:alias`. /// The alias should include the scope, formatted as `scope:alias`.
Future<SnChannel> getChannel(String key) async { Future<SnChannel> getChannel(String key) async {
if (_channelBox != null) { final local = await (_dt.db.snLocalChatChannel.select()
final local = _channelBox!.get(key); ..where((e) => e.alias.equals(key)))
if (local != null) return local; .getSingleOrNull();
} if (local != null) return local.content;
var resp = await _sn.client.get('/cgi/im/channels/$key'); var resp =
await _sn.client.get('/cgi/im/channels/${key.replaceAll(':', '/')}');
var out = SnChannel.fromJson(resp.data); var out = SnChannel.fromJson(resp.data);
// Preload realm of the channel // Preload realm of the channel
@@ -77,8 +95,19 @@ class ChatChannelProvider extends ChangeNotifier {
/// And the second time is when the data was fetched from the server. /// And the second time is when the data was fetched from the server.
/// But there is some exception that will only cause one of them to be emitted. /// But there is some exception that will only cause one of them to be emitted.
/// Like the local storage is broken or the server is down. /// Like the local storage is broken or the server is down.
Stream<List<SnChannel>> fetchChannels() async* { Stream<List<SnChannel>> fetchChannels(
if (_channelBox != null) yield _channelBox!.values.toList(); {bool noRemote = false, bool noLocal = false}) async* {
if (!noLocal) {
final local = await (_dt.db.snLocalChatChannel.select()
..orderBy([
(e) =>
OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
]))
.get();
yield local.map((e) => e.content).toList();
}
if (noRemote) return;
var resp = await _sn.client.get('/cgi/id/realms/me/available'); var resp = await _sn.client.get('/cgi/id/realms/me/available');
final realms = List<SnRealm>.from( final realms = List<SnRealm>.from(
@@ -120,23 +149,23 @@ class ChatChannelProvider extends ChangeNotifier {
Future<List<SnChatMessage>> getLastMessages( Future<List<SnChatMessage>> getLastMessages(
Iterable<SnChannel> channels, Iterable<SnChannel> channels,
) async { ) async {
final result = List<SnChatMessage>.empty(growable: true); final result = List<Future<SnLocalChatMessageData?>>.empty(growable: true);
for (final channel in channels) { for (final channel in channels) {
final channelBox = await Hive.openBox<SnChatMessage>( final out = (_dt.db.snLocalChatMessage.select()
'${ChatMessageController.kChatMessageBoxPrefix}${channel.id}', ..where((e) => e.channelId.equals(channel.id))
); ..orderBy([
final lastMessage = (e) =>
channelBox.isNotEmpty ? channelBox.values.reduce((a, b) => a.createdAt.isAfter(b.createdAt) ? a : b) : null; OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
if (lastMessage != null) result.add(lastMessage); ])
channelBox.close(); ..limit(1))
.getSingleOrNull();
result.add(out);
} }
await _ud.listAccount(result.map((ele) => ele.sender.accountId).toSet()); final out = (await Future.wait(result))
return result; .where((e) => e != null)
} .map((e) => e!.content)
.toList();
@override await _ud.listAccount(out.map((ele) => ele.sender.accountId).toSet());
void dispose() { return out;
_channelBox?.close();
super.dispose();
} }
} }

View File

@@ -17,6 +17,7 @@ const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
const kAppNotifyWithHaptic = 'app_notify_with_haptic'; const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const kAppExpandPostLink = 'app_expand_post_link'; const kAppExpandPostLink = 'app_expand_post_link';
const kAppExpandChatLink = 'app_expand_chat_link'; const kAppExpandChatLink = 'app_expand_chat_link';
const kAppRealmCompactView = 'app_realm_compact_view';
const Map<String, FilterQuality> kImageQualityLevel = { const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none, 'settingsImageQualityLowest': FilterQuality.none,
@@ -72,6 +73,13 @@ class ConfigProvider extends ChangeNotifier {
return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
} }
bool get realmCompactView {
return prefs.getBool(kAppRealmCompactView) ?? false;
}
set realmCompactView(bool value) {
prefs.setBool(kAppRealmCompactView, value);
}
set serverUrl(String url) { set serverUrl(String url) {
prefs.setString(kNetworkServerStoreKey, url); prefs.setString(kNetworkServerStoreKey, url);
_home.saveWidgetData("nex_server_url", url); _home.saveWidgetData("nex_server_url", url);

View File

@@ -0,0 +1,31 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' show join;
import 'package:path_provider/path_provider.dart';
import 'package:surface/database/database.dart';
class DatabaseProvider {
late AppDatabase db;
DatabaseProvider(BuildContext context) {
db = AppDatabase();
}
Future<int> getDatabaseSize() async {
if (kIsWeb) return 0;
final basepath = await getApplicationSupportDirectory();
return await File(join(basepath.path, 'solar_network_data.sqlite'))
.length();
}
Future<void> removeDatabase() async {
if (kIsWeb) return;
final basepath = await getApplicationSupportDirectory();
final file = File(join(basepath.path, 'solar_network_data.sqlite'));
db.close();
await file.delete();
db = AppDatabase();
}
}

View File

@@ -2,19 +2,23 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/poll.dart'; import 'package:surface/types/poll.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart';
class SnPostContentProvider { class SnPostContentProvider {
late final SnNetworkProvider _sn; late final SnNetworkProvider _sn;
late final UserDirectoryProvider _ud; late final UserDirectoryProvider _ud;
late final SnAttachmentProvider _attach; late final SnAttachmentProvider _attach;
late final SnRealmProvider _realm;
SnPostContentProvider(BuildContext context) { SnPostContentProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>(); _sn = context.read<SnNetworkProvider>();
_ud = context.read<UserDirectoryProvider>(); _ud = context.read<UserDirectoryProvider>();
_attach = context.read<SnAttachmentProvider>(); _attach = context.read<SnAttachmentProvider>();
_realm = context.read<SnRealmProvider>();
} }
Future<SnPoll> _fetchPoll(int id) async { Future<SnPoll> _fetchPoll(int id) async {
@@ -42,9 +46,13 @@ class SnPostContentProvider {
final attachments = await _attach.getMultiple(rids.toList()); final attachments = await _attach.getMultiple(rids.toList());
for (var i = 0; i < out.length; i++) { for (var i = 0; i < out.length; i++) {
SnPoll? poll; SnPoll? poll;
SnRealm? realm;
if (out[i].pollId != null) { if (out[i].pollId != null) {
poll = await _fetchPoll(out[i].pollId!); poll = await _fetchPoll(out[i].pollId!);
} }
if (out[i].realmId != null) {
realm = await _realm.getRealm(out[i].realmId!);
}
out[i] = out[i].copyWith( out[i] = out[i].copyWith(
preload: SnPostPreload( preload: SnPostPreload(
@@ -52,6 +60,7 @@ class SnPostContentProvider {
attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(), attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull, video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull,
poll: poll, poll: poll,
realm: realm,
), ),
); );
} }
@@ -81,9 +90,13 @@ class SnPostContentProvider {
final attachments = await _attach.getMultiple(rids.toList()); final attachments = await _attach.getMultiple(rids.toList());
SnPoll? poll; SnPoll? poll;
SnRealm? realm;
if (out.pollId != null) { if (out.pollId != null) {
poll = await _fetchPoll(out.pollId!); poll = await _fetchPoll(out.pollId!);
} }
if (out.realmId != null) {
realm = await _realm.getRealm(out.realmId!);
}
out = out.copyWith( out = out.copyWith(
preload: SnPostPreload( preload: SnPostPreload(
@@ -91,6 +104,7 @@ class SnPostContentProvider {
attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(), attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull, video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull,
poll: poll, poll: poll,
realm: realm,
), ),
); );
@@ -112,6 +126,8 @@ class SnPostContentProvider {
String? author, String? author,
Iterable<String>? categories, Iterable<String>? categories,
Iterable<String>? tags, Iterable<String>? tags,
String? realm,
String? channel,
}) async { }) async {
final resp = await _sn.client.get('/cgi/co/posts', queryParameters: { final resp = await _sn.client.get('/cgi/co/posts', queryParameters: {
'take': take, 'take': take,
@@ -120,6 +136,8 @@ class SnPostContentProvider {
if (author != null) 'author': author, if (author != null) 'author': author,
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','), if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','), if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
if (realm != null) 'realm': realm,
if (channel != null) 'channel': channel,
}); });
final List<SnPost> out = await _preloadRelatedDataInBatch( final List<SnPost> out = await _preloadRelatedDataInBatch(
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/realm.dart';
class SnRealmProvider {
late final SnNetworkProvider _sn;
SnRealmProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
}
final Map<String, SnRealm> _cache = {};
Future<List<SnRealm>> listAvailableRealms() async {
final resp = await _sn.client.get('/cgi/id/realms/me/available');
final out = List<SnRealm>.from(
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
);
for (final realm in out) {
_cache[realm.alias] = realm;
_cache[realm.id.toString()] = realm;
}
return out;
}
Future<SnRealm> getRealm(dynamic aliasOrId) async {
if (_cache.containsKey(aliasOrId.toString())) {
return _cache[aliasOrId.toString()]!;
}
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;
return out;
}
}

View File

@@ -2,6 +2,9 @@ import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@@ -27,9 +30,23 @@ class _AlbumScreenState extends State<AlbumScreen> {
bool _isBusy = false; bool _isBusy = false;
int? _totalCount; int? _totalCount;
SnAttachmentBilling? _billing;
final List<SnAttachment> _attachments = List.empty(growable: true); final List<SnAttachment> _attachments = List.empty(growable: true);
final List<String> _heroTags = List.empty(growable: true); final List<String> _heroTags = List.empty(growable: true);
Future<void> _fetchBillingStatus() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/uc/billing');
final out = SnAttachmentBilling.fromJson(resp.data);
setState(() => _billing = out);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
Future<void> _fetchAttachments() async { Future<void> _fetchAttachments() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
@@ -62,6 +79,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fetchBillingStatus();
_fetchAttachments(); _fetchAttachments();
_scrollController.addListener(() { _scrollController.addListener(() {
if (_scrollController.position.atEdge) { if (_scrollController.position.atEdge) {
@@ -91,6 +109,48 @@ class _AlbumScreenState extends State<AlbumScreen> {
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenAlbum').tr(), title: Text('screenAlbum').tr(),
), ),
SliverToBoxAdapter(
child: Card(
child: Row(
children: [
SizedBox(
width: 80,
height: 80,
child: CircularProgressIndicator(
value: _billing?.includedRatio ?? 0,
strokeWidth: 8,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
),
).padding(all: 12),
const Gap(24),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('attachmentBillingUploaded').tr().bold(),
Text(
(_billing?.currentBytes ?? 0).formatBytes(decimals: 4),
style: GoogleFonts.robotoMono(),
),
Text('attachmentBillingDiscount').tr().bold(),
Text(
'${(_billing?.discountFileSize ?? 0).formatBytes(decimals: 2)} · ${((_billing?.includedRatio ?? 0) * 100).toStringAsFixed(2)}%',
style: GoogleFonts.robotoMono(),
),
],
),
),
Tooltip(
message: 'attachmentBillingHint'.tr(),
child: IconButton(
icon: const Icon(Symbols.info),
onPressed: () {},
),
),
],
).padding(horizontal: 24, vertical: 8),
),
),
SliverMasonryGrid.extent( SliverMasonryGrid.extent(
childCount: _attachments.length, childCount: _attachments.length,
maxCrossAxisExtent: 320, maxCrossAxisExtent: 320,

View File

@@ -6,7 +6,9 @@ import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_select.dart'; import 'package:surface/widgets/account/account_select.dart';
@@ -17,9 +19,6 @@ import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../providers/sn_network.dart';
import '../providers/userinfo.dart';
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
const ChatScreen({super.key}); const ChatScreen({super.key});
@@ -35,7 +34,7 @@ class _ChatScreenState extends State<ChatScreen> {
List<SnChannel>? _channels; List<SnChannel>? _channels;
Map<int, SnChatMessage>? _lastMessages; Map<int, SnChatMessage>? _lastMessages;
void _refreshChannels() { void _refreshChannels({bool noRemote = false}) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
setState(() => _isBusy = false); setState(() => _isBusy = false);
@@ -43,12 +42,15 @@ class _ChatScreenState extends State<ChatScreen> {
} }
final chan = context.read<ChatChannelProvider>(); final chan = context.read<ChatChannelProvider>();
chan.fetchChannels().listen((channels) async { chan.fetchChannels(noRemote: noRemote).listen((channels) async {
final lastMessages = await chan.getLastMessages(channels); final lastMessages = await chan.getLastMessages(channels);
_lastMessages = {for (final val in lastMessages) val.channelId: val}; _lastMessages = {for (final val in lastMessages) val.channelId: val};
channels.sort((a, b) { channels.sort((a, b) {
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) { if (_lastMessages!.containsKey(a.id) &&
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt); _lastMessages!.containsKey(b.id)) {
return _lastMessages![b.id]!
.createdAt
.compareTo(_lastMessages![a.id]!.createdAt);
} }
if (_lastMessages!.containsKey(a.id)) return -1; if (_lastMessages!.containsKey(a.id)) return -1;
if (_lastMessages!.containsKey(b.id)) return 1; if (_lastMessages!.containsKey(b.id)) return 1;
@@ -86,7 +88,8 @@ class _ChatScreenState extends State<ChatScreen> {
void _newDirectMessage() async { void _newDirectMessage() async {
final user = await showModalBottomSheet( final user = await showModalBottomSheet(
context: context, context: context,
builder: (context) => AccountSelect(title: 'channelNewDirectMessage'.tr()), builder: (context) =>
AccountSelect(title: 'channelNewDirectMessage'.tr()),
); );
if (user == null) return; if (user == null) return;
if (!mounted) return; if (!mounted) return;
@@ -98,7 +101,8 @@ class _ChatScreenState extends State<ChatScreen> {
await sn.client.post('/cgi/im/channels/global/dm', data: { await sn.client.post('/cgi/im/channels/global/dm', data: {
'alias': uuid.v4().replaceAll('-', '').substring(0, 12), 'alias': uuid.v4().replaceAll('-', '').substring(0, 12),
'name': 'DM', 'name': 'DM',
'description': 'A direct message channel between @${ua.user?.name} and @${user.name}', 'description':
'A direct message channel between @${ua.user?.name} and @${user.name}',
'related_user': user.id, 'related_user': user.id,
}); });
_fabKey.currentState!.toggle(); _fabKey.currentState!.toggle();
@@ -144,20 +148,27 @@ class _ChatScreenState extends State<ChatScreen> {
type: ExpandableFabType.up, type: ExpandableFabType.up,
childrenAnimation: ExpandableFabAnimation.none, childrenAnimation: ExpandableFabAnimation.none,
overlayStyle: ExpandableFabOverlayStyle( overlayStyle: ExpandableFabOverlayStyle(
color: Theme.of(context).colorScheme.surface.withAlpha((255 * 0.5).round()), color: Theme.of(context)
.colorScheme
.surface
.withAlpha((255 * 0.5).round()),
), ),
openButtonBuilder: RotateFloatingActionButtonBuilder( openButtonBuilder: RotateFloatingActionButtonBuilder(
child: const Icon(Symbols.add, size: 28), child: const Icon(Symbols.add, size: 28),
fabSize: ExpandableFabSize.regular, fabSize: ExpandableFabSize.regular,
foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor, foregroundColor:
backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor, Theme.of(context).floatingActionButtonTheme.foregroundColor,
backgroundColor:
Theme.of(context).floatingActionButtonTheme.backgroundColor,
shape: const CircleBorder(), shape: const CircleBorder(),
), ),
closeButtonBuilder: DefaultFloatingActionButtonBuilder( closeButtonBuilder: DefaultFloatingActionButtonBuilder(
child: const Icon(Symbols.close, size: 28), child: const Icon(Symbols.close, size: 28),
fabSize: ExpandableFabSize.regular, fabSize: ExpandableFabSize.regular,
foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor, foregroundColor:
backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor, Theme.of(context).floatingActionButtonTheme.foregroundColor,
backgroundColor:
Theme.of(context).floatingActionButtonTheme.backgroundColor,
shape: const CircleBorder(), shape: const CircleBorder(),
), ),
children: [ children: [
@@ -208,13 +219,17 @@ class _ChatScreenState extends State<ChatScreen> {
final lastMessage = _lastMessages?[channel.id]; final lastMessage = _lastMessages?[channel.id];
if (channel.type == 1) { if (channel.type == 1) {
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere( final otherMember =
(ele) => ele?.accountId != ua.user?.id, channel.members?.cast<SnChannelMember?>().firstWhere(
orElse: () => null, (ele) => ele?.accountId != ua.user?.id,
); orElse: () => null,
);
return ListTile( return ListTile(
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), title: Text(ud
.getAccountFromCache(otherMember?.accountId)
?.nick ??
channel.name),
subtitle: lastMessage != null subtitle: lastMessage != null
? Text( ? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
@@ -228,9 +243,12 @@ class _ChatScreenState extends State<ChatScreen> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 16), contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage( leading: AccountImage(
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, content: ud
.getAccountFromCache(otherMember?.accountId)
?.avatar,
), ),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
@@ -240,7 +258,7 @@ class _ChatScreenState extends State<ChatScreen> {
'alias': channel.alias, 'alias': channel.alias,
}, },
).then((value) { ).then((value) {
if (mounted) _refreshChannels(); if (mounted) _refreshChannels(noRemote: true);
}); });
}, },
); );
@@ -259,7 +277,8 @@ class _ChatScreenState extends State<ChatScreen> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 16), contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage( leading: AccountImage(
content: null, content: null,
fallbackWidget: const Icon(Symbols.chat, size: 20), fallbackWidget: const Icon(Symbols.chat, size: 20),

View File

@@ -104,7 +104,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client.delete( await sn.client.delete(
'/cgi/im/channels/${_channel!.realm?.alias ?? 'global'}/${_channel!.id}/members/me', '/cgi/im/channels/${_channel!.realm?.alias ?? 'global'}/${_channel!.alias}/me',
); );
if (!mounted) return; if (!mounted) return;
Navigator.pop(context, false); Navigator.pop(context, false);

View File

@@ -95,6 +95,10 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
'description': _descriptionController.text, 'description': _descriptionController.text,
'is_public': _isPublic, 'is_public': _isPublic,
'is_community': _isCommunity, 'is_community': _isCommunity,
if (_editingChannel != null && _belongToRealm == null)
'new_belongs_realm': 'global'
else if (_editingChannel != null && _belongToRealm?.id != _editingChannel?.realm?.id)
'new_belongs_realm': _belongToRealm!.alias,
}; };
try { try {
@@ -171,7 +175,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
items: [ items: [
...(_realms?.map( ...(_realms?.map(
(SnRealm item) => DropdownMenuItem<SnRealm>( (SnRealm item) => DropdownMenuItem<SnRealm>(
enabled: _editingChannel == null || _editingChannel?.realmId == item.id,
value: item, value: item,
child: Row( child: Row(
children: [ children: [
@@ -204,7 +207,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
) ?? ) ??
[]), []),
DropdownMenuItem<SnRealm>( DropdownMenuItem<SnRealm>(
enabled: _editingChannel == null,
value: null, value: null,
child: Row( child: Row(
children: [ children: [

View File

@@ -58,6 +58,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
StreamSubscription? _wsSubscription; StreamSubscription? _wsSubscription;
// TODO fetch user identity and ask them to join the channel or not
Future<void> _fetchChannel() async { Future<void> _fetchChannel() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);

View File

@@ -1,3 +1,4 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
@@ -8,7 +9,10 @@ import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/types/post.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/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
@@ -35,61 +39,49 @@ class ExploreScreen extends StatefulWidget {
State<ExploreScreen> createState() => _ExploreScreenState(); State<ExploreScreen> createState() => _ExploreScreenState();
} }
class _ExploreScreenState extends State<ExploreScreen> { // You know what? I'm not going to make this a global variable.
// Cuz the global key make the selected category not update to child widget when the category is changed.
SnPostCategory? _selectedCategory;
class _ExploreScreenState extends State<ExploreScreen> with SingleTickerProviderStateMixin {
late final TabController _tabController = TabController(length: 4, vsync: this);
final _fabKey = GlobalKey<ExpandableFabState>(); final _fabKey = GlobalKey<ExpandableFabState>();
final _listKeys = List.generate(4, (_) => GlobalKey<_PostListWidgetState>());
bool _isBusy = true;
final List<SnPost> _posts = List.empty(growable: true);
final List<SnPostCategory> _categories = List.empty(growable: true); final List<SnPostCategory> _categories = List.empty(growable: true);
int? _postCount;
String? _selectedCategory;
Future<void> _fetchCategories() async { Future<void> _fetchCategories() async {
_categories.clear(); _categories.clear();
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/categories?take=100'); final resp = await sn.client.get('/cgi/co/categories?take=100');
_categories.addAll(resp.data.map((e) => SnPostCategory.fromJson(e)).cast<SnPostCategory>() ?? []); setState(() {
_categories.addAll(resp.data.map((e) => SnPostCategory.fromJson(e)).cast<SnPostCategory>() ?? []);
});
} catch (err) { } catch (err) {
if (!mounted) return; if (mounted) context.showErrorDialog(err);
context.showErrorDialog(err);
} }
} }
Future<void> _fetchPosts() async { void _clearFilter() {
if (_postCount != null && _posts.length >= _postCount!) return; _selectedCategory = null;
setState(() => _isBusy = true);
final pt = context.read<SnPostContentProvider>();
final result = await pt.listPosts(
take: 10,
offset: _posts.length,
categories: _selectedCategory != null ? [_selectedCategory!] : null,
);
final out = result.$1;
if (!mounted) return;
_postCount = result.$2;
_posts.addAll(out);
if (mounted) setState(() => _isBusy = false);
}
Future<void> _refreshPosts() {
_postCount = null;
_posts.clear();
return _fetchPosts();
} }
@override @override
void initState() { void initState() {
super.initState();
_fetchPosts();
_fetchCategories(); _fetchCategories();
super.initState();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> refreshPosts() async {
await _listKeys[_tabController.index].currentState?.refreshPosts();
} }
@override @override
@@ -131,7 +123,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
'mode': 'stories', 'mode': 'stories',
}).then((value) { }).then((value) {
if (value == true) { if (value == true) {
_refreshPosts(); refreshPosts();
} }
}); });
_fabKey.currentState!.toggle(); _fabKey.currentState!.toggle();
@@ -152,7 +144,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
'mode': 'articles', 'mode': 'articles',
}).then((value) { }).then((value) {
if (value == true) { if (value == true) {
_refreshPosts(); refreshPosts();
} }
}); });
_fabKey.currentState!.toggle(); _fabKey.currentState!.toggle();
@@ -173,7 +165,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
'mode': 'questions', 'mode': 'questions',
}).then((value) { }).then((value) {
if (value == true) { if (value == true) {
_refreshPosts(); refreshPosts();
} }
}); });
_fabKey.currentState!.toggle(); _fabKey.currentState!.toggle();
@@ -194,7 +186,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
'mode': 'videos', 'mode': 'videos',
}).then((value) { }).then((value) {
if (value == true) { if (value == true) {
_refreshPosts(); refreshPosts();
} }
}); });
_fabKey.currentState!.toggle(); _fabKey.currentState!.toggle();
@@ -205,74 +197,137 @@ class _ExploreScreenState extends State<ExploreScreen> {
), ),
], ],
), ),
body: RefreshIndicator( body: NestedScrollView(
displacement: 40 + MediaQuery.of(context).padding.top, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
onRefresh: () => _refreshPosts(), return [
child: CustomScrollView( SliverOverlapAbsorber(
slivers: [ handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
SliverAppBar( sliver: SliverAppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenExplore').tr(), title: Text('screenExplore').tr(),
floating: true, floating: true,
snap: true, snap: true,
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Symbols.search), icon: const Icon(Symbols.category),
onPressed: () { onPressed: () {
GoRouter.of(context).pushNamed('postSearch'); showModalBottomSheet(
}, context: context,
), builder: (context) => _PostCategoryPickerPopup(
const Gap(8), categories: _categories,
], selected: _selectedCategory,
bottom: PreferredSize( ),
preferredSize: const Size.fromHeight(50), ).then((value) {
child: SizedBox( if (value != null && context.mounted) {
height: 50, _selectedCategory = value == false ? null : value;
child: SingleChildScrollView( refreshPosts();
scrollDirection: Axis.horizontal, }
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12), });
child: Row( },
mainAxisAlignment: MainAxisAlignment.center,
children: _categories.map((ele) {
return StyledWidget(ChoiceChip(
avatar: Icon(kCategoryIcons[ele.alias] ?? Symbols.question_mark),
label: Text(
'postCategory${ele.alias.capitalize()}'.trExists()
? 'postCategory${ele.alias.capitalize()}'.tr()
: ele.name,
),
selected: _selectedCategory == ele.alias,
onSelected: (value) {
_selectedCategory = value ? ele.alias : null;
_refreshPosts();
},
)).padding(horizontal: 4);
}).toList(),
),
), ),
IconButton(
icon: const Icon(Symbols.search),
onPressed: () {
GoRouter.of(context).pushNamed('postSearch');
},
),
const Gap(8),
],
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Symbols.globe, size: 20, color: Theme.of(context).appBarTheme.foregroundColor),
const Gap(8),
Flexible(
child: Text(
'postChannelGlobal',
maxLines: 1,
).tr().textColor(Theme.of(context).appBarTheme.foregroundColor),
),
],
),
),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Symbols.group, size: 20, color: Theme.of(context).appBarTheme.foregroundColor),
const Gap(8),
Flexible(
child: Text(
'postChannelFriends',
maxLines: 1,
textAlign: TextAlign.center,
).tr().textColor(Theme.of(context).appBarTheme.foregroundColor),
),
],
),
),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Symbols.subscriptions, size: 20, color: Theme.of(context).appBarTheme.foregroundColor),
const Gap(8),
Flexible(
child: Text(
'postChannelFollowing',
maxLines: 1,
).tr().textColor(Theme.of(context).appBarTheme.foregroundColor),
),
],
),
),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Symbols.workspaces, size: 20, color: Theme.of(context).appBarTheme.foregroundColor),
const Gap(8),
Flexible(
child: Text(
'postChannelRealm',
maxLines: 1,
).tr().textColor(Theme.of(context).appBarTheme.foregroundColor),
),
],
),
),
],
), ),
), ),
), ),
const SliverGap(12), ];
SliverInfiniteList( },
itemCount: _posts.length, body: TabBarView(
isLoading: _isBusy, controller: _tabController,
centerLoading: true, children: [
hasReachedMax: _postCount != null && _posts.length >= _postCount!, _PostListWidget(
onFetchData: _fetchPosts, key: _listKeys[0],
itemBuilder: (context, idx) { onClearFilter: _clearFilter,
return OpenablePostItem( ),
data: _posts[idx], _PostListWidget(
maxWidth: 640, key: _listKeys[1],
onChanged: (data) { channel: 'friends',
setState(() => _posts[idx] = data); onClearFilter: _clearFilter,
}, ),
onDeleted: () { _PostListWidget(
_refreshPosts(); key: _listKeys[2],
}, channel: 'following',
); onClearFilter: _clearFilter,
}, ),
separatorBuilder: (_, __) => const Gap(8), _PostListWidget(
key: _listKeys[3],
withRealm: true,
onClearFilter: _clearFilter,
), ),
], ],
), ),
@@ -280,3 +335,246 @@ class _ExploreScreenState extends State<ExploreScreen> {
); );
} }
} }
class _PostListWidget extends StatefulWidget {
final String? channel;
final bool withRealm;
final Function onClearFilter;
const _PostListWidget({super.key, this.channel, this.withRealm = false, required this.onClearFilter});
@override
State<_PostListWidget> createState() => _PostListWidgetState();
}
class _PostListWidgetState extends State<_PostListWidget> {
bool _isBusy = false;
final List<SnPost> _posts = List.empty(growable: true);
final List<SnRealm> _realms = List.empty(growable: true);
SnRealm? _selectedRealm;
int? _postCount;
Future<void> _fetchRealms() async {
try {
final rels = context.read<SnRealmProvider>();
final out = await rels.listAvailableRealms();
setState(() {
_realms.addAll(out);
_selectedRealm = out.firstOrNull;
});
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
rethrow;
}
}
Future<void> _fetchPosts() async {
if (_postCount != null && _posts.length >= _postCount!) return;
setState(() => _isBusy = true);
final pt = context.read<SnPostContentProvider>();
final result = await pt.listPosts(
take: 10,
offset: _posts.length,
categories: _selectedCategory != null ? [_selectedCategory!.alias] : null,
channel: widget.channel,
realm: _selectedRealm?.alias,
);
final out = result.$1;
if (!mounted) return;
_postCount = result.$2;
_posts.addAll(out);
if (mounted) setState(() => _isBusy = false);
}
Future<void> refreshPosts() {
_postCount = null;
_posts.clear();
return _fetchPosts();
}
@override
void initState() {
super.initState();
if (widget.withRealm) {
_fetchRealms().then((_) {
_fetchPosts();
});
} 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: () {
widget.onClearFilter.call();
refreshPosts();
},
),
],
padding: const EdgeInsets.only(left: 20, right: 4),
),
if (widget.withRealm)
DropdownButtonHideUnderline(
child: DropdownButton2<SnRealm>(
isExpanded: true,
items: _realms
.map(
(ele) => DropdownMenuItem<SnRealm>(
value: ele,
child: Row(
children: [
AccountImage(
content: ele.avatar,
fallbackWidget: const Icon(Symbols.group, size: 16),
radius: 14,
),
const Gap(8),
Text(
ele.name,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
)
.toList(),
value: _selectedRealm,
onChanged: (SnRealm? value) {
setState(() => _selectedRealm = value);
refreshPosts();
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.only(left: 4, right: 12),
),
menuItemStyleData: const MenuItemStyleData(
height: 48,
),
),
),
if (widget.withRealm) const Divider(height: 1),
Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
displacement: 40 + MediaQuery.of(context).padding.top,
onRefresh: () => refreshPosts(),
child: InfiniteList(
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),
),
),
).padding(top: 8),
),
],
);
}
}
class _PostCategoryPickerPopup extends StatelessWidget {
final List<SnPostCategory> categories;
final SnPostCategory? selected;
const _PostCategoryPickerPopup({required this.categories, this.selected});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.category, size: 24),
const Gap(16),
Text('postCategory').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, top: 16, bottom: 12),
ListTile(
leading: const Icon(Symbols.clear),
title: Text('postFilterReset').tr(),
subtitle: Text('postFilterResetDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
onTap: () {
Navigator.pop(context, false);
},
),
const Divider(height: 1),
GridView.count(
crossAxisCount: 4,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
childAspectRatio: 1,
children: categories
.map(
(ele) => InkWell(
onTap: () {
_selectedCategory = ele;
Navigator.pop(context, ele);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
kCategoryIcons[ele.alias] ?? Symbols.question_mark,
color: selected == ele ? Theme.of(context).colorScheme.primary : null,
),
const Gap(4),
Text(
'postCategory${ele.alias.capitalize()}'.trExists()
? 'postCategory${ele.alias.capitalize()}'.tr()
: ele.name,
)
.textStyle(Theme.of(context).textTheme.titleMedium!)
.textColor(selected == ele ? Theme.of(context).colorScheme.primary : null),
],
),
),
)
.toList(),
),
],
);
}
}

View File

@@ -20,6 +20,7 @@ import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/attachment/attachment_input.dart'; import 'package:surface/widgets/attachment/attachment_input.dart';
import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_item.dart';
@@ -35,6 +36,8 @@ import 'package:provider/provider.dart';
import 'package:surface/widgets/post/post_poll_editor.dart'; import 'package:surface/widgets/post/post_poll_editor.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../../providers/sn_realm.dart';
class PostEditorExtra { class PostEditorExtra {
final String? text; final String? text;
final String? title; final String? title;
@@ -79,6 +82,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
bool get _isLoading => _isFetching || _writeController.isLoading; bool get _isLoading => _isFetching || _writeController.isLoading;
List<SnPublisher>? _publishers; List<SnPublisher>? _publishers;
List<SnRealm>? _realms;
Future<void> _fetchPublishers() async { Future<void> _fetchPublishers() async {
setState(() => _isFetching = true); setState(() => _isFetching = true);
@@ -101,6 +105,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
} }
} }
Future<void> _fetchRealms() async {
final rels = context.read<SnRealmProvider>();
try {
_realms = await rels.listAvailableRealms();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
void _updateMeta() { void _updateMeta() {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@@ -144,6 +158,19 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
); );
} }
void _showRealmPopup() {
showModalBottomSheet(
context: context,
builder: (context) => _PostRealmPopup(
controller: _writeController,
realms: _realms,
onUpdate: () {
_fetchRealms();
},
),
);
}
void _showPollEditorDialog() async { void _showPollEditorDialog() async {
final poll = await showDialog<dynamic>( final poll = await showDialog<dynamic>(
context: context, context: context,
@@ -161,6 +188,20 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
} }
} }
void _showThumbnailEditorDialog() async {
final attachment = await showDialog<SnAttachment?>(
context: context,
builder: (context) => AttachmentInputDialog(
title: 'postThumbnail'.tr(),
pool: 'interactive',
mediaType: SnMediaType.image,
),
);
if (!context.mounted) return;
if (attachment == null) return;
_writeController.setThumbnail(attachment);
}
@override @override
void dispose() { void dispose() {
_writeController.dispose(); _writeController.dispose();
@@ -180,6 +221,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
} else { } else {
_writeController.setMode(widget.mode); _writeController.setMode(widget.mode);
} }
_fetchRealms();
_fetchPublishers(); _fetchPublishers();
_writeController.fetchRelatedPost( _writeController.fetchRelatedPost(
context, context,
@@ -321,18 +363,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
'stories' => _PostStoryEditor( 'stories' => _PostStoryEditor(
controller: _writeController, controller: _writeController,
onTapPublisher: _showPublisherPopup, onTapPublisher: _showPublisherPopup,
onTapRealm: _showRealmPopup,
), ),
'articles' => _PostArticleEditor( 'articles' => _PostArticleEditor(
controller: _writeController, controller: _writeController,
onTapPublisher: _showPublisherPopup, onTapPublisher: _showPublisherPopup,
onTapRealm: _showRealmPopup,
), ),
'questions' => _PostQuestionEditor( 'questions' => _PostQuestionEditor(
controller: _writeController, controller: _writeController,
onTapPublisher: _showPublisherPopup, onTapPublisher: _showPublisherPopup,
onTapRealm: _showRealmPopup,
), ),
'videos' => _PostVideoEditor( 'videos' => _PostVideoEditor(
controller: _writeController, controller: _writeController,
onTapPublisher: _showPublisherPopup, onTapPublisher: _showPublisherPopup,
onTapRealm: _showRealmPopup,
), ),
_ => const Placeholder(), _ => const Placeholder(),
}) })
@@ -344,15 +390,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
left: 0, left: 0,
right: 0, right: 0,
child: PostMediaPendingList( child: PostMediaPendingList(
thumbnail: _writeController.thumbnail,
attachments: _writeController.attachments, attachments: _writeController.attachments,
isBusy: _writeController.isBusy, isBusy: _writeController.isBusy,
onUpload: (int idx) async { onUpload: (int idx) async {
await _writeController.uploadSingleAttachment(context, idx); await _writeController.uploadSingleAttachment(context, idx);
}, },
onPostSetThumbnail: (int? idx) {
_writeController.setThumbnail(idx);
},
onInsertLink: (int idx) async { onInsertLink: (int idx) async {
_writeController.contentController.text += _writeController.contentController.text +=
'\n![](solink://attachments/${_writeController.attachments[idx].attachment!.rid})'; '\n![](solink://attachments/${_writeController.attachments[idx].attachment!.rid})';
@@ -453,6 +495,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
_showPollEditorDialog(); _showPollEditorDialog();
}, },
), ),
if (_writeController.mode == 'articles')
IconButton(
icon: Icon(Symbols.full_coverage, color: Theme.of(context).colorScheme.primary),
style: ButtonStyle(
backgroundColor: _writeController.thumbnail == null
? null
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
),
onPressed: () {
if (_writeController.thumbnail != null) {
_writeController.setThumbnail(null);
return;
}
_showThumbnailEditorDialog();
},
),
], ],
), ),
), ),
@@ -549,11 +607,65 @@ class _PostPublisherPopup extends StatelessWidget {
} }
} }
class _PostRealmPopup extends StatelessWidget {
final PostWriteController controller;
final List<SnRealm>? realms;
final Function onUpdate;
const _PostRealmPopup({required this.controller, this.realms, required this.onUpdate});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.face, size: 24),
const Gap(16),
Text('accountRealms', 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('postInGlobalDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () {
controller.setRealm(null);
Navigator.pop(context, true);
},
),
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: () {
controller.setRealm(realm);
Navigator.pop(context, true);
},
);
},
),
),
],
);
}
}
class _PostStoryEditor extends StatelessWidget { class _PostStoryEditor extends StatelessWidget {
final PostWriteController controller; final PostWriteController controller;
final Function? onTapPublisher; final Function? onTapPublisher;
final Function? onTapRealm;
const _PostStoryEditor({required this.controller, this.onTapPublisher}); const _PostStoryEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -563,17 +675,36 @@ class _PostStoryEditor extends StatelessWidget {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Material( Column(
elevation: 2, children: [
borderRadius: const BorderRadius.all(Radius.circular(24)), Material(
child: GestureDetector( elevation: 2,
onTap: () { borderRadius: const BorderRadius.all(Radius.circular(24)),
onTapPublisher?.call(); child: GestureDetector(
}, onTap: () {
child: AccountImage( onTapPublisher?.call();
content: controller.publisher?.avatar, },
child: AccountImage(
content: controller.publisher?.avatar,
),
),
), ),
), const Gap(11),
Material(
elevation: 1,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapRealm?.call();
},
child: AccountImage(
content: controller.realm?.avatar,
fallbackWidget: const Icon(Symbols.globe, size: 20),
radius: 14,
),
),
),
],
), ),
Expanded( Expanded(
child: Column( child: Column(
@@ -616,8 +747,9 @@ class _PostStoryEditor extends StatelessWidget {
class _PostArticleEditor extends StatelessWidget { class _PostArticleEditor extends StatelessWidget {
final PostWriteController controller; final PostWriteController controller;
final Function? onTapPublisher; final Function? onTapPublisher;
final Function? onTapRealm;
const _PostArticleEditor({required this.controller, this.onTapPublisher}); const _PostArticleEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -638,6 +770,21 @@ class _PostArticleEditor extends StatelessWidget {
], ],
), ),
), ),
Material(
elevation: 1,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapRealm?.call();
},
child: AccountImage(
content: controller.realm?.avatar,
fallbackWidget: const Icon(Symbols.globe, size: 20),
radius: 14,
),
),
),
const Gap(8),
], ],
).padding(horizontal: 12, vertical: 8), ).padding(horizontal: 12, vertical: 8),
onTap: () { onTap: () {
@@ -668,7 +815,24 @@ class _PostArticleEditor extends StatelessWidget {
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration, contentInsertionConfiguration: controller.contentInsertionConfiguration,
).padding(horizontal: 16), ).padding(horizontal: 16),
const Gap(4), if (controller.thumbnail != null)
Container(
margin: const EdgeInsets.only(left: 12, right: 12, top: 8, bottom: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Theme.of(context).dividerColor),
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: 16 / 9,
child: AttachmentItem(
data: controller.thumbnail!.attachment!,
heroTag: "post-editor-thumbnail-preview",
),
),
),
),
]; ];
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) { if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) {
@@ -740,8 +904,9 @@ class _PostArticleEditor extends StatelessWidget {
class _PostQuestionEditor extends StatelessWidget { class _PostQuestionEditor extends StatelessWidget {
final PostWriteController controller; final PostWriteController controller;
final Function? onTapPublisher; final Function? onTapPublisher;
final Function? onTapRealm;
const _PostQuestionEditor({required this.controller, this.onTapPublisher}); const _PostQuestionEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -751,17 +916,36 @@ class _PostQuestionEditor extends StatelessWidget {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Material( Column(
elevation: 1, children: [
borderRadius: const BorderRadius.all(Radius.circular(24)), Material(
child: GestureDetector( elevation: 2,
onTap: () { borderRadius: const BorderRadius.all(Radius.circular(24)),
onTapPublisher?.call(); child: GestureDetector(
}, onTap: () {
child: AccountImage( onTapPublisher?.call();
content: controller.publisher?.avatar, },
child: AccountImage(
content: controller.publisher?.avatar,
),
),
), ),
), const Gap(11),
Material(
elevation: 1,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapRealm?.call();
},
child: AccountImage(
content: controller.realm?.avatar,
fallbackWidget: const Icon(Symbols.globe, size: 20),
radius: 14,
),
),
),
],
), ),
Expanded( Expanded(
child: Column( child: Column(
@@ -815,8 +999,9 @@ class _PostQuestionEditor extends StatelessWidget {
class _PostVideoEditor extends StatelessWidget { class _PostVideoEditor extends StatelessWidget {
final PostWriteController controller; final PostWriteController controller;
final Function? onTapPublisher; final Function? onTapPublisher;
final Function? onTapRealm;
const _PostVideoEditor({required this.controller, this.onTapPublisher}); const _PostVideoEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
void _selectVideo(BuildContext context) async { void _selectVideo(BuildContext context) async {
final video = await showDialog<SnAttachment?>( final video = await showDialog<SnAttachment?>(
@@ -904,28 +1089,36 @@ class _PostVideoEditor extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
Material( Column(
color: Theme.of(context).colorScheme.surfaceContainerHigh, children: [
child: InkWell( Material(
child: Row( elevation: 2,
children: [ borderRadius: const BorderRadius.all(Radius.circular(24)),
AccountImage(content: controller.publisher?.avatar, radius: 20), child: GestureDetector(
const Gap(8), onTap: () {
Expanded( onTapPublisher?.call();
child: Column( },
crossAxisAlignment: CrossAxisAlignment.start, child: AccountImage(
children: [ content: controller.publisher?.avatar,
Text(controller.publisher?.nick ?? 'loading'.tr()).bold(),
Text('@${controller.publisher?.name}'),
],
),
), ),
], ),
).padding(horizontal: 12, vertical: 8), ),
onTap: () { const Gap(11),
onTapPublisher?.call(); Material(
}, elevation: 1,
), borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapRealm?.call();
},
child: AccountImage(
content: controller.realm?.avatar,
fallbackWidget: const Icon(Symbols.globe, size: 20),
radius: 14,
),
),
),
],
), ),
const Gap(16), const Gap(16),
TextField( TextField(

View File

@@ -4,17 +4,16 @@ import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/realm.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/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/realm/realm_item.dart';
import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:surface/widgets/universal_image.dart';
class RealmScreen extends StatefulWidget { class RealmScreen extends StatefulWidget {
const RealmScreen({super.key}); const RealmScreen({super.key});
@@ -75,12 +74,12 @@ class _RealmScreenState extends State<RealmScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_isCompactView = context.read<ConfigProvider>().realmCompactView;
_fetchRealms(); _fetchRealms();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
@@ -110,6 +109,7 @@ class _RealmScreenState extends State<RealmScreen> {
icon: !_isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module), icon: !_isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
onPressed: () { onPressed: () {
setState(() => _isCompactView = !_isCompactView); setState(() => _isCompactView = !_isCompactView);
context.read<ConfigProvider>().realmCompactView = _isCompactView;
}, },
), ),
const Gap(8), const Gap(8),
@@ -134,129 +134,46 @@ class _RealmScreenState extends State<RealmScreen> {
itemCount: _realms?.length ?? 0, itemCount: _realms?.length ?? 0,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final realm = _realms![idx]; final realm = _realms![idx];
if (_isCompactView) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: realm.avatar,
fallbackWidget: const Icon(Symbols.group, size: 20),
),
title: Text(realm.name),
subtitle: Text(
realm.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: PopupMenuButton(
itemBuilder: (BuildContext context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.edit),
const Gap(16),
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': realm.alias},
).then((value) {
if (value != null) {
_fetchRealms();
}
});
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deleteRealm(realm);
},
),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': realm.alias},
).then((value) {
if (value == true) {
_fetchRealms();
}
});
},
);
}
return Container( return RealmItemWidget(
constraints: BoxConstraints(maxWidth: 640), showPopularity: false,
child: Card( item: realm,
margin: const EdgeInsets.all(12), isListView: _isCompactView,
child: InkWell( actionListView: [
borderRadius: const BorderRadius.all(Radius.circular(8)), PopupMenuItem(
child: Column( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AspectRatio( const Icon(Symbols.edit),
aspectRatio: 16 / 7, const Gap(16),
child: Stack( Text('edit').tr(),
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: (realm.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoResizeUniversalImage(
sn.getAttachmentUrl(realm.banner!),
fit: BoxFit.cover,
),
),
),
Positioned(
bottom: -30,
left: 18,
child: AccountImage(
content: realm.avatar,
radius: 24,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
),
],
),
),
const Gap(20 + 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
], ],
), ),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'realmDetail', 'realmManage',
pathParameters: {'alias': realm.alias}, queryParameters: {'editing': realm.alias},
).then((value) { ).then((value) {
if (value == true) { if (value != null) {
_fetchRealms(); _fetchRealms();
} }
}); });
}, },
), ),
), PopupMenuItem(
).center(); child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deleteRealm(realm);
},
),
],
onUpdate: _fetchRealms,
);
}, },
), ),
), ),

View File

@@ -5,16 +5,19 @@ import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/account.dart'; import 'package:surface/types/account.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_select.dart'; import 'package:surface/widgets/account/account_select.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class RealmDetailScreen extends StatefulWidget { class RealmDetailScreen extends StatefulWidget {
@@ -60,18 +63,36 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
} }
} }
List<SnChannel>? _channels;
Future<void> _fetchChannels() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/im/channels/${widget.alias}/public');
_channels = List<SnChannel>.from(
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
);
} catch (err) {
if (mounted) context.showErrorDialog(err);
rethrow;
} finally {
setState(() {});
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fetchRealm().then((_) { _fetchRealm().then((_) {
_fetchPublishers(); _fetchPublishers();
_fetchChannels();
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DefaultTabController( return DefaultTabController(
length: 3, length: 4,
child: AppScaffold( child: AppScaffold(
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
@@ -83,6 +104,7 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
bottom: TabBar( bottom: TabBar(
tabs: [ tabs: [
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)), Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.explore, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)), Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)), Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
], ],
@@ -93,7 +115,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
}, },
body: TabBarView( body: TabBarView(
children: [ children: [
_RealmDetailHomeWidget(realm: _realm, publishers: _publishers), _RealmDetailHomeWidget(realm: _realm, publishers: _publishers, channels: _channels),
_RealmPostListWidget(realm: _realm),
_RealmMemberListWidget(realm: _realm), _RealmMemberListWidget(realm: _realm),
_RealmSettingsWidget( _RealmSettingsWidget(
realm: _realm, realm: _realm,
@@ -112,8 +135,9 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
class _RealmDetailHomeWidget extends StatelessWidget { class _RealmDetailHomeWidget extends StatelessWidget {
final SnRealm? realm; final SnRealm? realm;
final List<SnPublisher>? publishers; final List<SnPublisher>? publishers;
final List<SnChannel>? channels;
const _RealmDetailHomeWidget({required this.realm, this.publishers}); const _RealmDetailHomeWidget({required this.realm, this.publishers, this.channels});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -135,30 +159,76 @@ class _RealmDetailHomeWidget extends StatelessWidget {
], ],
).padding(horizontal: 24), ).padding(horizontal: 24),
const Gap(16), const Gap(16),
const Divider(), const Divider(height: 1),
Expanded( Expanded(
child: ListView.builder( child: CustomScrollView(
padding: EdgeInsets.zero, slivers: [
itemCount: publishers?.length ?? 0, if (publishers?.isNotEmpty ?? false)
itemBuilder: (context, idx) { SliverToBoxAdapter(
final ele = publishers![idx]; child: Container(
return ListTile( width: double.infinity,
contentPadding: const EdgeInsets.symmetric(horizontal: 20), color: Theme.of(context).colorScheme.surfaceContainerHigh,
leading: AccountImage( child: Text('realmCommunityPublishersHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
content: ele.avatar, .padding(horizontal: 24, vertical: 8),
fallbackWidget: const Icon(Symbols.group, size: 24), ),
), ),
title: Text(ele.nick), SliverList.builder(
subtitle: Text('@${ele.name}'), itemCount: publishers?.length ?? 0,
trailing: const Icon(Symbols.chevron_right), itemBuilder: (context, idx) {
onTap: () { final ele = publishers![idx];
GoRouter.of(context).pushNamed( return ListTile(
'postPublisher', contentPadding: const EdgeInsets.symmetric(horizontal: 20),
pathParameters: {'name': ele.name}, leading: AccountImage(
content: ele.avatar,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
title: Text(ele.nick),
subtitle: Text('@${ele.name}'),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed(
'postPublisher',
pathParameters: {'name': ele.name},
);
},
); );
}, },
); ),
}, if (channels?.isNotEmpty ?? false)
SliverToBoxAdapter(
child: Container(
width: double.infinity,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
.padding(horizontal: 24, vertical: 8),
),
),
SliverList.builder(
itemCount: channels?.length ?? 0,
itemBuilder: (context, idx) {
final ele = channels![idx];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: AccountImage(
content: null,
fallbackWidget: const Icon(Symbols.chat, size: 20),
),
title: Text(ele.name),
subtitle: Text('#${ele.alias}'),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': realm?.alias ?? 'global',
'alias': ele.alias,
},
);
},
);
},
),
],
), ),
), ),
], ],
@@ -166,6 +236,72 @@ class _RealmDetailHomeWidget extends StatelessWidget {
} }
} }
class _RealmPostListWidget extends StatefulWidget {
final SnRealm? realm;
const _RealmPostListWidget({this.realm});
@override
State<_RealmPostListWidget> createState() => _RealmPostListWidgetState();
}
class _RealmPostListWidgetState extends State<_RealmPostListWidget> {
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: widget.realm?.id.toString(),
);
_totalCount = out.$2;
_posts.addAll(out.$1);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
return MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: _fetchPosts,
child: InfiniteList(
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 Gap(8),
),
),
).padding(top: 8);
}
}
class _RealmMemberListWidget extends StatefulWidget { class _RealmMemberListWidget extends StatefulWidget {
final SnRealm? realm; final SnRealm? realm;
@@ -365,7 +501,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
try { try {
await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}/members/me'); await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}/me');
if (!mounted) return; if (!mounted) return;
Navigator.pop(context, true); Navigator.pop(context, true);
} catch (err) { } catch (err) {

View File

@@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
@@ -12,7 +13,7 @@ import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/realm/realm_item.dart';
class RealmDiscoveryScreen extends StatefulWidget { class RealmDiscoveryScreen extends StatefulWidget {
const RealmDiscoveryScreen({super.key}); const RealmDiscoveryScreen({super.key});
@@ -24,6 +25,7 @@ class RealmDiscoveryScreen extends StatefulWidget {
class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> { class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
List<SnRealm>? _realms; List<SnRealm>? _realms;
bool _isBusy = false; bool _isBusy = false;
bool _isCompactView = false;
Future<void> _fetchRealms() async { Future<void> _fetchRealms() async {
try { try {
@@ -44,16 +46,25 @@ class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_isCompactView = context.read<ConfigProvider>().realmCompactView;
_fetchRealms(); _fetchRealms();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text('screenRealmDiscovery').tr(), title: Text('screenRealmDiscovery').tr(),
actions: [
IconButton(
icon: _isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
onPressed: () {
setState(() => _isCompactView = !_isCompactView);
context.read<ConfigProvider>().realmCompactView = _isCompactView;
},
),
const Gap(8),
],
), ),
body: Column( body: Column(
children: [ children: [
@@ -66,64 +77,16 @@ class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
itemCount: _realms?.length ?? 0, itemCount: _realms?.length ?? 0,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final realm = _realms![idx]; final realm = _realms![idx];
return Container( return RealmItemWidget(
constraints: BoxConstraints(maxWidth: 640), item: realm,
child: Card( isListView: _isCompactView,
margin: const EdgeInsets.all(12), onTap: () {
child: InkWell( showModalBottomSheet(
borderRadius: const BorderRadius.all(Radius.circular(8)), context: context,
child: Column( builder: (context) => _RealmJoinPopup(realm: realm),
crossAxisAlignment: CrossAxisAlignment.start, );
children: [ },
AspectRatio( );
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: (realm.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoResizeUniversalImage(
sn.getAttachmentUrl(realm.banner!),
fit: BoxFit.cover,
),
),
),
Positioned(
bottom: -30,
left: 18,
child: AccountImage(
content: realm.avatar,
radius: 24,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
),
],
),
),
const Gap(20 + 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
],
),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => _RealmJoinPopup(realm: realm),
);
},
),
),
).center();
}, },
), ),
), ),
@@ -235,6 +198,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
), ),
Text( Text(
widget.realm.description, widget.realm.description,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
], ],

View File

@@ -5,8 +5,10 @@ import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -14,6 +16,7 @@ import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/theme.dart'; import 'package:surface/providers/theme.dart';
import 'package:surface/theme.dart'; import 'package:surface/theme.dart';
@@ -67,6 +70,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final dt = context.read<DatabaseProvider>();
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
@@ -81,7 +85,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('settingsAppearance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), Text('settingsAppearance')
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
ListTile( ListTile(
title: Text('settingsDisplayLanguage').tr(), title: Text('settingsDisplayLanguage').tr(),
subtitle: Text('settingsDisplayLanguageDescription').tr(), subtitle: Text('settingsDisplayLanguageDescription').tr(),
@@ -91,15 +99,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: DropdownButton2<Locale?>( child: DropdownButton2<Locale?>(
isExpanded: true, isExpanded: true,
items: [ items: [
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) { ...EasyLocalization.of(context)!
.supportedLocales
.mapIndexed((idx, ele) {
return DropdownMenuItem<Locale?>( return DropdownMenuItem<Locale?>(
value: ele, value: ele,
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14), child:
Text('${ele.languageCode}-${ele.countryCode}')
.fontSize(14),
); );
}), }),
DropdownMenuItem<Locale?>( DropdownMenuItem<Locale?>(
value: null, value: null,
child: Text('settingsDisplayLanguageSystem').tr().fontSize(14), child: Text('settingsDisplayLanguageSystem')
.tr()
.fontSize(14),
), ),
], ],
value: EasyLocalization.of(context)!.currentLocale, value: EasyLocalization.of(context)!.currentLocale,
@@ -132,10 +146,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
leading: const Icon(Symbols.image), leading: const Icon(Symbols.image),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () async { onTap: () async {
final image = await ImagePicker().pickImage(source: ImageSource.gallery); final image = await ImagePicker()
.pickImage(source: ImageSource.gallery);
if (image == null) return; if (image == null) return;
await File(image.path).copy('$_docBasepath/app_background_image'); await File(image.path)
.copy('$_docBasepath/app_background_image');
_prefs.setBool(kAppBackgroundStoreKey, true); _prefs.setBool(kAppBackgroundStoreKey, true);
setState(() {}); setState(() {});
@@ -143,7 +159,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
if (!kIsWeb) if (!kIsWeb)
FutureBuilder<bool>( FutureBuilder<bool>(
future: File('$_docBasepath/app_background_image').exists(), future:
File('$_docBasepath/app_background_image').exists(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) { if (!snapshot.hasData || !snapshot.data!) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -151,12 +168,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
return ListTile( return ListTile(
title: Text('settingsBackgroundImageClear').tr(), title: Text('settingsBackgroundImageClear').tr(),
subtitle: Text('settingsBackgroundImageClearDescription').tr(), subtitle:
contentPadding: const EdgeInsets.symmetric(horizontal: 24), Text('settingsBackgroundImageClearDescription')
.tr(),
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.texture), leading: const Icon(Symbols.texture),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
File('$_docBasepath/app_background_image').deleteSync(); File('$_docBasepath/app_background_image')
.deleteSync();
_prefs.remove(kAppBackgroundStoreKey); _prefs.remove(kAppBackgroundStoreKey);
setState(() {}); setState(() {});
}, },
@@ -186,34 +207,35 @@ class _SettingsScreenState extends State<SettingsScreen> {
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () async { onTap: () async {
Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value); Color pickerColor = Color(
_prefs.getInt(kAppColorSchemeStoreKey) ??
Colors.indigo.value);
final color = await showDialog<Color?>( final color = await showDialog<Color?>(
context: context, context: context,
builder: (context) => builder: (context) => AlertDialog(
AlertDialog( content: SingleChildScrollView(
content: SingleChildScrollView( child: ColorPicker(
child: ColorPicker( pickerColor: pickerColor,
pickerColor: pickerColor, onColorChanged: (color) => pickerColor = color,
onColorChanged: (color) => pickerColor = color, enableAlpha: false,
enableAlpha: false, hexInputBar: true,
hexInputBar: true,
),
),
actions: <Widget>[
TextButton(
child: const Text('dialogDismiss').tr(),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('dialogConfirm').tr(),
onPressed: () {
Navigator.of(context).pop(pickerColor);
},
),
],
), ),
),
actions: <Widget>[
TextButton(
child: const Text('dialogDismiss').tr(),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('dialogConfirm').tr(),
onPressed: () {
Navigator.of(context).pop(pickerColor);
},
),
],
),
); );
if (color == null || !context.mounted) return; if (color == null || !context.mounted) return;
@@ -248,16 +270,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
], ],
value: _prefs.getInt(kAppColorSchemeStoreKey) == null value: _prefs.getInt(kAppColorSchemeStoreKey) == null
? 1 ? 1
: kColorSchemes.values : kColorSchemes.values.toList().indexWhere((ele) =>
.toList() ele.value ==
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)), _prefs.getInt(kAppColorSchemeStoreKey)),
onChanged: (int? value) { onChanged: (int? value) {
if (value != null && value != -1) { if (value != null && value != -1) {
_prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values _prefs.setInt(kAppColorSchemeStoreKey,
.elementAt(value) kColorSchemes.values.elementAt(value).value);
.value);
final th = context.read<ThemeProvider>(); final th = context.read<ThemeProvider>();
th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value)); th.reloadTheme(
seedColorOverride:
kColorSchemes.values.elementAt(value));
setState(() {}); setState(() {});
context.showSnackbar('colorSchemeApplied'.tr()); context.showSnackbar('colorSchemeApplied'.tr());
@@ -293,7 +316,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
CheckboxListTile( CheckboxListTile(
secondary: const Icon(Symbols.left_panel_close), secondary: const Icon(Symbols.left_panel_close),
title: Text('settingsDrawerPreferCollapse').tr(), title: Text('settingsDrawerPreferCollapse').tr(),
subtitle: Text('settingsDrawerPreferCollapseDescription').tr(), subtitle:
Text('settingsDrawerPreferCollapseDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17), contentPadding: const EdgeInsets.only(left: 24, right: 17),
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false, value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
onChanged: (value) { onChanged: (value) {
@@ -308,7 +332,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('settingsFeatures').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), Text('settingsFeatures')
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
CheckboxListTile( CheckboxListTile(
secondary: const Icon(Symbols.vibration), secondary: const Icon(Symbols.vibration),
contentPadding: const EdgeInsets.only(left: 24, right: 17), contentPadding: const EdgeInsets.only(left: 24, right: 17),
@@ -350,7 +378,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('settingsNetwork').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), Text('settingsNetwork')
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
TextField( TextField(
controller: _serverUrlController, controller: _serverUrlController,
decoration: InputDecoration( decoration: InputDecoration(
@@ -371,7 +403,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
}, },
), ),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16, top: 8, bottom: 4), ).padding(horizontal: 16, top: 8, bottom: 4),
ListTile( ListTile(
title: Text('settingsNetworkServerPreset').tr(), title: Text('settingsNetworkServerPreset').tr(),
@@ -383,12 +416,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
isExpanded: true, isExpanded: true,
items: [ items: [
...kNetworkServerDirectory, ...kNetworkServerDirectory,
if (!kNetworkServerDirectory.map((ele) => ele.$2).contains(_serverUrlController.text)) if (!kNetworkServerDirectory
.map((ele) => ele.$2)
.contains(_serverUrlController.text))
('Custom', _serverUrlController.text), ('Custom', _serverUrlController.text),
] ]
.map( .map(
(item) => (item) => DropdownMenuItem<String>(
DropdownMenuItem<String>(
value: item.$2, value: item.$2,
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
@@ -396,11 +430,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(item.$1).fontSize(14), Text(item.$1).fontSize(14),
Text(item.$2, overflow: TextOverflow.ellipsis).fontSize(11) Text(item.$2, overflow: TextOverflow.ellipsis)
.fontSize(11)
], ],
), ),
), ),
) )
.toList(), .toList(),
value: _serverUrlController.text, value: _serverUrlController.text,
onChanged: (String? value) { onChanged: (String? value) {
@@ -442,7 +477,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('settingsPerformance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), Text('settingsPerformance')
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
ListTile( ListTile(
title: Text('settingsImageQuality').tr(), title: Text('settingsImageQuality').tr(),
subtitle: Text('settingsImageQualityDescription').tr(), subtitle: Text('settingsImageQualityDescription').tr(),
@@ -450,21 +489,22 @@ class _SettingsScreenState extends State<SettingsScreen> {
leading: const Icon(Symbols.image), leading: const Icon(Symbols.image),
trailing: DropdownButtonHideUnderline( trailing: DropdownButtonHideUnderline(
child: DropdownButton2<FilterQuality>( child: DropdownButton2<FilterQuality>(
value: kImageQualityLevel.values.elementAtOrNull(_prefs.getInt('app_image_quality') ?? 3) ?? value: kImageQualityLevel.values.elementAtOrNull(
_prefs.getInt('app_image_quality') ?? 3) ??
FilterQuality.high, FilterQuality.high,
isExpanded: true, isExpanded: true,
items: kImageQualityLevel.entries items: kImageQualityLevel.entries
.map( .map(
(item) => (item) => DropdownMenuItem<FilterQuality>(
DropdownMenuItem<FilterQuality>(
value: item.value, value: item.value,
child: Text(item.key).tr().fontSize(14), child: Text(item.key).tr().fontSize(14),
), ),
) )
.toList(), .toList(),
onChanged: (FilterQuality? value) { onChanged: (FilterQuality? value) {
if (value == null) return; if (value == null) return;
_prefs.setInt('app_image_quality', kImageQualityLevel.values.toList().indexOf(value)); _prefs.setInt('app_image_quality',
kImageQualityLevel.values.toList().indexOf(value));
setState(() {}); setState(() {});
}, },
buttonStyleData: const ButtonStyleData( buttonStyleData: const ButtonStyleData(
@@ -486,7 +526,42 @@ class _SettingsScreenState extends State<SettingsScreen> {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('settingsMisc').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), Text('settingsMisc')
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
ListTile(
leading: const Icon(Symbols.database),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text('databaseSize').tr(),
subtitle: FutureBuilder(
future: dt.getDatabaseSize(),
builder: (context, snapshot) {
if (!snapshot.hasData || kIsWeb) {
return Text('unknown').tr();
}
return Text(
snapshot.data!.formatBytes(),
style: GoogleFonts.robotoMono(),
);
},
),
),
ListTile(
leading: const Icon(Symbols.database_off),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text('databaseDelete').tr(),
subtitle: Text('databaseDeleteDescription').tr(),
trailing: const Icon(Symbols.chevron_right),
onTap: () async {
await dt.removeDatabase();
if (!context.mounted) return;
HapticFeedback.heavyImpact();
context.showSnackbar('databaseDeleted'.tr());
setState(() {});
},
),
ListTile( ListTile(
title: Text('settingsMiscAbout').tr(), title: Text('settingsMiscAbout').tr(),
subtitle: Text('settingsMiscAboutDescription').tr(), subtitle: Text('settingsMiscAboutDescription').tr(),

View File

@@ -177,3 +177,14 @@ class SnStickerPack with _$SnStickerPack {
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json); factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json);
} }
@freezed
class SnAttachmentBilling with _$SnAttachmentBilling {
const factory SnAttachmentBilling({
required int currentBytes,
required int discountFileSize,
required double includedRatio,
}) = _SnAttachmentBilling;
factory SnAttachmentBilling.fromJson(Map<String, Object?> json) => _$SnAttachmentBillingFromJson(json);
}

View File

@@ -3007,3 +3007,195 @@ abstract class _SnStickerPack implements SnStickerPack {
_$$SnStickerPackImplCopyWith<_$SnStickerPackImpl> get copyWith => _$$SnStickerPackImplCopyWith<_$SnStickerPackImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
SnAttachmentBilling _$SnAttachmentBillingFromJson(Map<String, dynamic> json) {
return _SnAttachmentBilling.fromJson(json);
}
/// @nodoc
mixin _$SnAttachmentBilling {
int get currentBytes => throw _privateConstructorUsedError;
int get discountFileSize => throw _privateConstructorUsedError;
double get includedRatio => throw _privateConstructorUsedError;
/// Serializes this SnAttachmentBilling to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnAttachmentBilling
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnAttachmentBillingCopyWith<SnAttachmentBilling> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnAttachmentBillingCopyWith<$Res> {
factory $SnAttachmentBillingCopyWith(
SnAttachmentBilling value, $Res Function(SnAttachmentBilling) then) =
_$SnAttachmentBillingCopyWithImpl<$Res, SnAttachmentBilling>;
@useResult
$Res call({int currentBytes, int discountFileSize, double includedRatio});
}
/// @nodoc
class _$SnAttachmentBillingCopyWithImpl<$Res, $Val extends SnAttachmentBilling>
implements $SnAttachmentBillingCopyWith<$Res> {
_$SnAttachmentBillingCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnAttachmentBilling
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? currentBytes = null,
Object? discountFileSize = null,
Object? includedRatio = null,
}) {
return _then(_value.copyWith(
currentBytes: null == currentBytes
? _value.currentBytes
: currentBytes // ignore: cast_nullable_to_non_nullable
as int,
discountFileSize: null == discountFileSize
? _value.discountFileSize
: discountFileSize // ignore: cast_nullable_to_non_nullable
as int,
includedRatio: null == includedRatio
? _value.includedRatio
: includedRatio // ignore: cast_nullable_to_non_nullable
as double,
) as $Val);
}
}
/// @nodoc
abstract class _$$SnAttachmentBillingImplCopyWith<$Res>
implements $SnAttachmentBillingCopyWith<$Res> {
factory _$$SnAttachmentBillingImplCopyWith(_$SnAttachmentBillingImpl value,
$Res Function(_$SnAttachmentBillingImpl) then) =
__$$SnAttachmentBillingImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int currentBytes, int discountFileSize, double includedRatio});
}
/// @nodoc
class __$$SnAttachmentBillingImplCopyWithImpl<$Res>
extends _$SnAttachmentBillingCopyWithImpl<$Res, _$SnAttachmentBillingImpl>
implements _$$SnAttachmentBillingImplCopyWith<$Res> {
__$$SnAttachmentBillingImplCopyWithImpl(_$SnAttachmentBillingImpl _value,
$Res Function(_$SnAttachmentBillingImpl) _then)
: super(_value, _then);
/// Create a copy of SnAttachmentBilling
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? currentBytes = null,
Object? discountFileSize = null,
Object? includedRatio = null,
}) {
return _then(_$SnAttachmentBillingImpl(
currentBytes: null == currentBytes
? _value.currentBytes
: currentBytes // ignore: cast_nullable_to_non_nullable
as int,
discountFileSize: null == discountFileSize
? _value.discountFileSize
: discountFileSize // ignore: cast_nullable_to_non_nullable
as int,
includedRatio: null == includedRatio
? _value.includedRatio
: includedRatio // ignore: cast_nullable_to_non_nullable
as double,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnAttachmentBillingImpl implements _SnAttachmentBilling {
const _$SnAttachmentBillingImpl(
{required this.currentBytes,
required this.discountFileSize,
required this.includedRatio});
factory _$SnAttachmentBillingImpl.fromJson(Map<String, dynamic> json) =>
_$$SnAttachmentBillingImplFromJson(json);
@override
final int currentBytes;
@override
final int discountFileSize;
@override
final double includedRatio;
@override
String toString() {
return 'SnAttachmentBilling(currentBytes: $currentBytes, discountFileSize: $discountFileSize, includedRatio: $includedRatio)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnAttachmentBillingImpl &&
(identical(other.currentBytes, currentBytes) ||
other.currentBytes == currentBytes) &&
(identical(other.discountFileSize, discountFileSize) ||
other.discountFileSize == discountFileSize) &&
(identical(other.includedRatio, includedRatio) ||
other.includedRatio == includedRatio));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
Object.hash(runtimeType, currentBytes, discountFileSize, includedRatio);
/// Create a copy of SnAttachmentBilling
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnAttachmentBillingImplCopyWith<_$SnAttachmentBillingImpl> get copyWith =>
__$$SnAttachmentBillingImplCopyWithImpl<_$SnAttachmentBillingImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnAttachmentBillingImplToJson(
this,
);
}
}
abstract class _SnAttachmentBilling implements SnAttachmentBilling {
const factory _SnAttachmentBilling(
{required final int currentBytes,
required final int discountFileSize,
required final double includedRatio}) = _$SnAttachmentBillingImpl;
factory _SnAttachmentBilling.fromJson(Map<String, dynamic> json) =
_$SnAttachmentBillingImpl.fromJson;
@override
int get currentBytes;
@override
int get discountFileSize;
@override
double get includedRatio;
/// Create a copy of SnAttachmentBilling
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnAttachmentBillingImplCopyWith<_$SnAttachmentBillingImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -281,3 +281,19 @@ Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) =>
'stickers': instance.stickers?.map((e) => e.toJson()).toList(), 'stickers': instance.stickers?.map((e) => e.toJson()).toList(),
'account_id': instance.accountId, 'account_id': instance.accountId,
}; };
_$SnAttachmentBillingImpl _$$SnAttachmentBillingImplFromJson(
Map<String, dynamic> json) =>
_$SnAttachmentBillingImpl(
currentBytes: (json['current_bytes'] as num).toInt(),
discountFileSize: (json['discount_file_size'] as num).toInt(),
includedRatio: (json['included_ratio'] as num).toDouble(),
);
Map<String, dynamic> _$$SnAttachmentBillingImplToJson(
_$SnAttachmentBillingImpl instance) =>
<String, dynamic>{
'current_bytes': instance.currentBytes,
'discount_file_size': instance.discountFileSize,
'included_ratio': instance.includedRatio,
};

View File

@@ -1,6 +1,7 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/poll.dart'; import 'package:surface/types/poll.dart';
import 'package:surface/types/realm.dart';
part 'post.freezed.dart'; part 'post.freezed.dart';
part 'post.g.dart'; part 'post.g.dart';
@@ -24,6 +25,7 @@ class SnPost with _$SnPost {
required List<SnPost>? replies, required List<SnPost>? replies,
required int? replyId, required int? replyId,
required int? repostId, required int? repostId,
required int? realmId,
required SnPost? replyTo, required SnPost? replyTo,
required SnPost? repostTo, required SnPost? repostTo,
required List<int>? visibleUsersList, required List<int>? visibleUsersList,
@@ -95,6 +97,7 @@ class SnPostPreload with _$SnPostPreload {
required List<SnAttachment?>? attachments, required List<SnAttachment?>? attachments,
required SnAttachment? video, required SnAttachment? video,
required SnPoll? poll, required SnPoll? poll,
required SnRealm? realm,
}) = _SnPostPreload; }) = _SnPostPreload;
factory SnPostPreload.fromJson(Map<String, Object?> json) => factory SnPostPreload.fromJson(Map<String, Object?> json) =>

View File

@@ -34,6 +34,7 @@ mixin _$SnPost {
List<SnPost>? get replies => throw _privateConstructorUsedError; List<SnPost>? get replies => throw _privateConstructorUsedError;
int? get replyId => throw _privateConstructorUsedError; int? get replyId => throw _privateConstructorUsedError;
int? get repostId => throw _privateConstructorUsedError; int? get repostId => throw _privateConstructorUsedError;
int? get realmId => throw _privateConstructorUsedError;
SnPost? get replyTo => throw _privateConstructorUsedError; SnPost? get replyTo => throw _privateConstructorUsedError;
SnPost? get repostTo => throw _privateConstructorUsedError; SnPost? get repostTo => throw _privateConstructorUsedError;
List<int>? get visibleUsersList => throw _privateConstructorUsedError; List<int>? get visibleUsersList => throw _privateConstructorUsedError;
@@ -84,6 +85,7 @@ abstract class $SnPostCopyWith<$Res> {
List<SnPost>? replies, List<SnPost>? replies,
int? replyId, int? replyId,
int? repostId, int? repostId,
int? realmId,
SnPost? replyTo, SnPost? replyTo,
SnPost? repostTo, SnPost? repostTo,
List<int>? visibleUsersList, List<int>? visibleUsersList,
@@ -141,6 +143,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
Object? replies = freezed, Object? replies = freezed,
Object? replyId = freezed, Object? replyId = freezed,
Object? repostId = freezed, Object? repostId = freezed,
Object? realmId = freezed,
Object? replyTo = freezed, Object? replyTo = freezed,
Object? repostTo = freezed, Object? repostTo = freezed,
Object? visibleUsersList = freezed, Object? visibleUsersList = freezed,
@@ -219,6 +222,10 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
? _value.repostId ? _value.repostId
: repostId // ignore: cast_nullable_to_non_nullable : repostId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
realmId: freezed == realmId
? _value.realmId
: realmId // ignore: cast_nullable_to_non_nullable
as int?,
replyTo: freezed == replyTo replyTo: freezed == replyTo
? _value.replyTo ? _value.replyTo
: replyTo // ignore: cast_nullable_to_non_nullable : replyTo // ignore: cast_nullable_to_non_nullable
@@ -387,6 +394,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
List<SnPost>? replies, List<SnPost>? replies,
int? replyId, int? replyId,
int? repostId, int? repostId,
int? realmId,
SnPost? replyTo, SnPost? replyTo,
SnPost? repostTo, SnPost? repostTo,
List<int>? visibleUsersList, List<int>? visibleUsersList,
@@ -447,6 +455,7 @@ class __$$SnPostImplCopyWithImpl<$Res>
Object? replies = freezed, Object? replies = freezed,
Object? replyId = freezed, Object? replyId = freezed,
Object? repostId = freezed, Object? repostId = freezed,
Object? realmId = freezed,
Object? replyTo = freezed, Object? replyTo = freezed,
Object? repostTo = freezed, Object? repostTo = freezed,
Object? visibleUsersList = freezed, Object? visibleUsersList = freezed,
@@ -525,6 +534,10 @@ class __$$SnPostImplCopyWithImpl<$Res>
? _value.repostId ? _value.repostId
: repostId // ignore: cast_nullable_to_non_nullable : repostId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
realmId: freezed == realmId
? _value.realmId
: realmId // ignore: cast_nullable_to_non_nullable
as int?,
replyTo: freezed == replyTo replyTo: freezed == replyTo
? _value.replyTo ? _value.replyTo
: replyTo // ignore: cast_nullable_to_non_nullable : replyTo // ignore: cast_nullable_to_non_nullable
@@ -627,6 +640,7 @@ class _$SnPostImpl extends _SnPost {
required final List<SnPost>? replies, required final List<SnPost>? replies,
required this.replyId, required this.replyId,
required this.repostId, required this.repostId,
required this.realmId,
required this.replyTo, required this.replyTo,
required this.repostTo, required this.repostTo,
required final List<int>? visibleUsersList, required final List<int>? visibleUsersList,
@@ -715,6 +729,8 @@ class _$SnPostImpl extends _SnPost {
@override @override
final int? repostId; final int? repostId;
@override @override
final int? realmId;
@override
final SnPost? replyTo; final SnPost? replyTo;
@override @override
final SnPost? repostTo; final SnPost? repostTo;
@@ -777,7 +793,7 @@ class _$SnPostImpl extends _SnPost {
@override @override
String toString() { String toString() {
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)'; return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)';
} }
@override @override
@@ -806,6 +822,7 @@ class _$SnPostImpl extends _SnPost {
(identical(other.replyId, replyId) || other.replyId == replyId) && (identical(other.replyId, replyId) || other.replyId == replyId) &&
(identical(other.repostId, repostId) || (identical(other.repostId, repostId) ||
other.repostId == repostId) && other.repostId == repostId) &&
(identical(other.realmId, realmId) || other.realmId == realmId) &&
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) && (identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
(identical(other.repostTo, repostTo) || (identical(other.repostTo, repostTo) ||
other.repostTo == repostTo) && other.repostTo == repostTo) &&
@@ -861,6 +878,7 @@ class _$SnPostImpl extends _SnPost {
const DeepCollectionEquality().hash(_replies), const DeepCollectionEquality().hash(_replies),
replyId, replyId,
repostId, repostId,
realmId,
replyTo, replyTo,
repostTo, repostTo,
const DeepCollectionEquality().hash(_visibleUsersList), const DeepCollectionEquality().hash(_visibleUsersList),
@@ -915,6 +933,7 @@ abstract class _SnPost extends SnPost {
required final List<SnPost>? replies, required final List<SnPost>? replies,
required final int? replyId, required final int? replyId,
required final int? repostId, required final int? repostId,
required final int? realmId,
required final SnPost? replyTo, required final SnPost? replyTo,
required final SnPost? repostTo, required final SnPost? repostTo,
required final List<int>? visibleUsersList, required final List<int>? visibleUsersList,
@@ -968,6 +987,8 @@ abstract class _SnPost extends SnPost {
@override @override
int? get repostId; int? get repostId;
@override @override
int? get realmId;
@override
SnPost? get replyTo; SnPost? get replyTo;
@override @override
SnPost? get repostTo; SnPost? get repostTo;
@@ -1636,6 +1657,7 @@ mixin _$SnPostPreload {
List<SnAttachment?>? get attachments => throw _privateConstructorUsedError; List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
SnAttachment? get video => throw _privateConstructorUsedError; SnAttachment? get video => throw _privateConstructorUsedError;
SnPoll? get poll => throw _privateConstructorUsedError; SnPoll? get poll => throw _privateConstructorUsedError;
SnRealm? get realm => throw _privateConstructorUsedError;
/// Serializes this SnPostPreload to a JSON map. /// Serializes this SnPostPreload to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -1657,11 +1679,13 @@ abstract class $SnPostPreloadCopyWith<$Res> {
{SnAttachment? thumbnail, {SnAttachment? thumbnail,
List<SnAttachment?>? attachments, List<SnAttachment?>? attachments,
SnAttachment? video, SnAttachment? video,
SnPoll? poll}); SnPoll? poll,
SnRealm? realm});
$SnAttachmentCopyWith<$Res>? get thumbnail; $SnAttachmentCopyWith<$Res>? get thumbnail;
$SnAttachmentCopyWith<$Res>? get video; $SnAttachmentCopyWith<$Res>? get video;
$SnPollCopyWith<$Res>? get poll; $SnPollCopyWith<$Res>? get poll;
$SnRealmCopyWith<$Res>? get realm;
} }
/// @nodoc /// @nodoc
@@ -1683,6 +1707,7 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
Object? attachments = freezed, Object? attachments = freezed,
Object? video = freezed, Object? video = freezed,
Object? poll = freezed, Object? poll = freezed,
Object? realm = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
thumbnail: freezed == thumbnail thumbnail: freezed == thumbnail
@@ -1701,6 +1726,10 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
? _value.poll ? _value.poll
: poll // ignore: cast_nullable_to_non_nullable : poll // ignore: cast_nullable_to_non_nullable
as SnPoll?, as SnPoll?,
realm: freezed == realm
? _value.realm
: realm // ignore: cast_nullable_to_non_nullable
as SnRealm?,
) as $Val); ) as $Val);
} }
@@ -1745,6 +1774,20 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
return _then(_value.copyWith(poll: value) as $Val); return _then(_value.copyWith(poll: value) as $Val);
}); });
} }
/// Create a copy of SnPostPreload
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnRealmCopyWith<$Res>? get realm {
if (_value.realm == null) {
return null;
}
return $SnRealmCopyWith<$Res>(_value.realm!, (value) {
return _then(_value.copyWith(realm: value) as $Val);
});
}
} }
/// @nodoc /// @nodoc
@@ -1759,7 +1802,8 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res>
{SnAttachment? thumbnail, {SnAttachment? thumbnail,
List<SnAttachment?>? attachments, List<SnAttachment?>? attachments,
SnAttachment? video, SnAttachment? video,
SnPoll? poll}); SnPoll? poll,
SnRealm? realm});
@override @override
$SnAttachmentCopyWith<$Res>? get thumbnail; $SnAttachmentCopyWith<$Res>? get thumbnail;
@@ -1767,6 +1811,8 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res>
$SnAttachmentCopyWith<$Res>? get video; $SnAttachmentCopyWith<$Res>? get video;
@override @override
$SnPollCopyWith<$Res>? get poll; $SnPollCopyWith<$Res>? get poll;
@override
$SnRealmCopyWith<$Res>? get realm;
} }
/// @nodoc /// @nodoc
@@ -1786,6 +1832,7 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
Object? attachments = freezed, Object? attachments = freezed,
Object? video = freezed, Object? video = freezed,
Object? poll = freezed, Object? poll = freezed,
Object? realm = freezed,
}) { }) {
return _then(_$SnPostPreloadImpl( return _then(_$SnPostPreloadImpl(
thumbnail: freezed == thumbnail thumbnail: freezed == thumbnail
@@ -1804,6 +1851,10 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
? _value.poll ? _value.poll
: poll // ignore: cast_nullable_to_non_nullable : poll // ignore: cast_nullable_to_non_nullable
as SnPoll?, as SnPoll?,
realm: freezed == realm
? _value.realm
: realm // ignore: cast_nullable_to_non_nullable
as SnRealm?,
)); ));
} }
} }
@@ -1815,7 +1866,8 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
{required this.thumbnail, {required this.thumbnail,
required final List<SnAttachment?>? attachments, required final List<SnAttachment?>? attachments,
required this.video, required this.video,
required this.poll}) required this.poll,
required this.realm})
: _attachments = attachments; : _attachments = attachments;
factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) => factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) =>
@@ -1837,10 +1889,12 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
final SnAttachment? video; final SnAttachment? video;
@override @override
final SnPoll? poll; final SnPoll? poll;
@override
final SnRealm? realm;
@override @override
String toString() { String toString() {
return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video, poll: $poll)'; return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video, poll: $poll, realm: $realm)';
} }
@override @override
@@ -1853,13 +1907,14 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other._attachments, _attachments) && .equals(other._attachments, _attachments) &&
(identical(other.video, video) || other.video == video) && (identical(other.video, video) || other.video == video) &&
(identical(other.poll, poll) || other.poll == poll)); (identical(other.poll, poll) || other.poll == poll) &&
(identical(other.realm, realm) || other.realm == realm));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, thumbnail, int get hashCode => Object.hash(runtimeType, thumbnail,
const DeepCollectionEquality().hash(_attachments), video, poll); const DeepCollectionEquality().hash(_attachments), video, poll, realm);
/// Create a copy of SnPostPreload /// Create a copy of SnPostPreload
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -1882,7 +1937,8 @@ abstract class _SnPostPreload implements SnPostPreload {
{required final SnAttachment? thumbnail, {required final SnAttachment? thumbnail,
required final List<SnAttachment?>? attachments, required final List<SnAttachment?>? attachments,
required final SnAttachment? video, required final SnAttachment? video,
required final SnPoll? poll}) = _$SnPostPreloadImpl; required final SnPoll? poll,
required final SnRealm? realm}) = _$SnPostPreloadImpl;
factory _SnPostPreload.fromJson(Map<String, dynamic> json) = factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
_$SnPostPreloadImpl.fromJson; _$SnPostPreloadImpl.fromJson;
@@ -1895,6 +1951,8 @@ abstract class _SnPostPreload implements SnPostPreload {
SnAttachment? get video; SnAttachment? get video;
@override @override
SnPoll? get poll; SnPoll? get poll;
@override
SnRealm? get realm;
/// Create a copy of SnPostPreload /// Create a copy of SnPostPreload
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View File

@@ -31,6 +31,7 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
.toList(), .toList(),
replyId: (json['reply_id'] as num?)?.toInt(), replyId: (json['reply_id'] as num?)?.toInt(),
repostId: (json['repost_id'] as num?)?.toInt(), repostId: (json['repost_id'] as num?)?.toInt(),
realmId: (json['realm_id'] as num?)?.toInt(),
replyTo: json['reply_to'] == null replyTo: json['reply_to'] == null
? null ? null
: SnPost.fromJson(json['reply_to'] as Map<String, dynamic>), : SnPost.fromJson(json['reply_to'] as Map<String, dynamic>),
@@ -91,6 +92,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
'replies': instance.replies?.map((e) => e.toJson()).toList(), 'replies': instance.replies?.map((e) => e.toJson()).toList(),
'reply_id': instance.replyId, 'reply_id': instance.replyId,
'repost_id': instance.repostId, 'repost_id': instance.repostId,
'realm_id': instance.realmId,
'reply_to': instance.replyTo?.toJson(), 'reply_to': instance.replyTo?.toJson(),
'repost_to': instance.repostTo?.toJson(), 'repost_to': instance.repostTo?.toJson(),
'visible_users_list': instance.visibleUsersList, 'visible_users_list': instance.visibleUsersList,
@@ -178,6 +180,9 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
poll: json['poll'] == null poll: json['poll'] == null
? null ? null
: SnPoll.fromJson(json['poll'] as Map<String, dynamic>), : SnPoll.fromJson(json['poll'] as Map<String, dynamic>),
realm: json['realm'] == null
? null
: SnRealm.fromJson(json['realm'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) => Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
@@ -186,6 +191,7 @@ Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
'attachments': instance.attachments?.map((e) => e?.toJson()).toList(), 'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
'video': instance.video?.toJson(), 'video': instance.video?.toJson(),
'poll': instance.poll?.toJson(), 'poll': instance.poll?.toJson(),
'realm': instance.realm?.toJson(),
}; };
_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl( _$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(

View File

@@ -43,6 +43,7 @@ class SnRealm with _$SnRealm {
@HiveField(10) required int accountId, @HiveField(10) required int accountId,
@HiveField(11) required bool isPublic, @HiveField(11) required bool isPublic,
@HiveField(12) required bool isCommunity, @HiveField(12) required bool isCommunity,
@Default(0) int popularity,
}) = _SnRealm; }) = _SnRealm;
factory SnRealm.fromJson(Map<String, dynamic> json) => factory SnRealm.fromJson(Map<String, dynamic> json) =>

View File

@@ -394,6 +394,7 @@ mixin _$SnRealm {
bool get isPublic => throw _privateConstructorUsedError; bool get isPublic => throw _privateConstructorUsedError;
@HiveField(12) @HiveField(12)
bool get isCommunity => throw _privateConstructorUsedError; bool get isCommunity => throw _privateConstructorUsedError;
int get popularity => throw _privateConstructorUsedError;
/// Serializes this SnRealm to a JSON map. /// Serializes this SnRealm to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -423,7 +424,8 @@ abstract class $SnRealmCopyWith<$Res> {
@HiveField(9) Map<String, dynamic>? accessPolicy, @HiveField(9) Map<String, dynamic>? accessPolicy,
@HiveField(10) int accountId, @HiveField(10) int accountId,
@HiveField(11) bool isPublic, @HiveField(11) bool isPublic,
@HiveField(12) bool isCommunity}); @HiveField(12) bool isCommunity,
int popularity});
} }
/// @nodoc /// @nodoc
@@ -455,6 +457,7 @@ class _$SnRealmCopyWithImpl<$Res, $Val extends SnRealm>
Object? accountId = null, Object? accountId = null,
Object? isPublic = null, Object? isPublic = null,
Object? isCommunity = null, Object? isCommunity = null,
Object? popularity = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
id: null == id id: null == id
@@ -513,6 +516,10 @@ class _$SnRealmCopyWithImpl<$Res, $Val extends SnRealm>
? _value.isCommunity ? _value.isCommunity
: isCommunity // ignore: cast_nullable_to_non_nullable : isCommunity // ignore: cast_nullable_to_non_nullable
as bool, as bool,
popularity: null == popularity
? _value.popularity
: popularity // ignore: cast_nullable_to_non_nullable
as int,
) as $Val); ) as $Val);
} }
} }
@@ -538,7 +545,8 @@ abstract class _$$SnRealmImplCopyWith<$Res> implements $SnRealmCopyWith<$Res> {
@HiveField(9) Map<String, dynamic>? accessPolicy, @HiveField(9) Map<String, dynamic>? accessPolicy,
@HiveField(10) int accountId, @HiveField(10) int accountId,
@HiveField(11) bool isPublic, @HiveField(11) bool isPublic,
@HiveField(12) bool isCommunity}); @HiveField(12) bool isCommunity,
int popularity});
} }
/// @nodoc /// @nodoc
@@ -568,6 +576,7 @@ class __$$SnRealmImplCopyWithImpl<$Res>
Object? accountId = null, Object? accountId = null,
Object? isPublic = null, Object? isPublic = null,
Object? isCommunity = null, Object? isCommunity = null,
Object? popularity = null,
}) { }) {
return _then(_$SnRealmImpl( return _then(_$SnRealmImpl(
id: null == id id: null == id
@@ -626,6 +635,10 @@ class __$$SnRealmImplCopyWithImpl<$Res>
? _value.isCommunity ? _value.isCommunity
: isCommunity // ignore: cast_nullable_to_non_nullable : isCommunity // ignore: cast_nullable_to_non_nullable
as bool, as bool,
popularity: null == popularity
? _value.popularity
: popularity // ignore: cast_nullable_to_non_nullable
as int,
)); ));
} }
} }
@@ -648,7 +661,8 @@ class _$SnRealmImpl extends _SnRealm {
@HiveField(9) required final Map<String, dynamic>? accessPolicy, @HiveField(9) required final Map<String, dynamic>? accessPolicy,
@HiveField(10) required this.accountId, @HiveField(10) required this.accountId,
@HiveField(11) required this.isPublic, @HiveField(11) required this.isPublic,
@HiveField(12) required this.isCommunity}) @HiveField(12) required this.isCommunity,
this.popularity = 0})
: _members = members, : _members = members,
_accessPolicy = accessPolicy, _accessPolicy = accessPolicy,
super._(); super._();
@@ -713,10 +727,13 @@ class _$SnRealmImpl extends _SnRealm {
@override @override
@HiveField(12) @HiveField(12)
final bool isCommunity; final bool isCommunity;
@override
@JsonKey()
final int popularity;
@override @override
String toString() { String toString() {
return 'SnRealm(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, alias: $alias, name: $name, description: $description, members: $members, avatar: $avatar, banner: $banner, accessPolicy: $accessPolicy, accountId: $accountId, isPublic: $isPublic, isCommunity: $isCommunity)'; return 'SnRealm(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, alias: $alias, name: $name, description: $description, members: $members, avatar: $avatar, banner: $banner, accessPolicy: $accessPolicy, accountId: $accountId, isPublic: $isPublic, isCommunity: $isCommunity, popularity: $popularity)';
} }
@override @override
@@ -745,7 +762,9 @@ class _$SnRealmImpl extends _SnRealm {
(identical(other.isPublic, isPublic) || (identical(other.isPublic, isPublic) ||
other.isPublic == isPublic) && other.isPublic == isPublic) &&
(identical(other.isCommunity, isCommunity) || (identical(other.isCommunity, isCommunity) ||
other.isCommunity == isCommunity)); other.isCommunity == isCommunity) &&
(identical(other.popularity, popularity) ||
other.popularity == popularity));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -765,7 +784,8 @@ class _$SnRealmImpl extends _SnRealm {
const DeepCollectionEquality().hash(_accessPolicy), const DeepCollectionEquality().hash(_accessPolicy),
accountId, accountId,
isPublic, isPublic,
isCommunity); isCommunity,
popularity);
/// Create a copy of SnRealm /// Create a copy of SnRealm
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -798,7 +818,8 @@ abstract class _SnRealm extends SnRealm {
@HiveField(9) required final Map<String, dynamic>? accessPolicy, @HiveField(9) required final Map<String, dynamic>? accessPolicy,
@HiveField(10) required final int accountId, @HiveField(10) required final int accountId,
@HiveField(11) required final bool isPublic, @HiveField(11) required final bool isPublic,
@HiveField(12) required final bool isCommunity}) = _$SnRealmImpl; @HiveField(12) required final bool isCommunity,
final int popularity}) = _$SnRealmImpl;
const _SnRealm._() : super._(); const _SnRealm._() : super._();
factory _SnRealm.fromJson(Map<String, dynamic> json) = _$SnRealmImpl.fromJson; factory _SnRealm.fromJson(Map<String, dynamic> json) = _$SnRealmImpl.fromJson;
@@ -844,6 +865,8 @@ abstract class _SnRealm extends SnRealm {
@override @override
@HiveField(12) @HiveField(12)
bool get isCommunity; bool get isCommunity;
@override
int get popularity;
/// Create a copy of SnRealm /// Create a copy of SnRealm
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View File

@@ -128,6 +128,7 @@ _$SnRealmImpl _$$SnRealmImplFromJson(Map<String, dynamic> json) =>
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
isPublic: json['is_public'] as bool, isPublic: json['is_public'] as bool,
isCommunity: json['is_community'] as bool, isCommunity: json['is_community'] as bool,
popularity: (json['popularity'] as num?)?.toInt() ?? 0,
); );
Map<String, dynamic> _$$SnRealmImplToJson(_$SnRealmImpl instance) => Map<String, dynamic> _$$SnRealmImplToJson(_$SnRealmImpl instance) =>
@@ -146,4 +147,5 @@ Map<String, dynamic> _$$SnRealmImplToJson(_$SnRealmImpl instance) =>
'account_id': instance.accountId, 'account_id': instance.accountId,
'is_public': instance.isPublic, 'is_public': instance.isPublic,
'is_community': instance.isCommunity, 'is_community': instance.isCommunity,
'popularity': instance.popularity,
}; };

View File

@@ -97,6 +97,13 @@ class AboutScreen extends StatelessWidget {
launchUrlString('https://status.solsynth.dev'); launchUrlString('https://status.solsynth.dev');
}, },
), ),
TextButton(
style: denseButtonStyle,
child: Text('projectDetail').tr(),
onPressed: () {
launchUrlString('https://solsynth.dev/products/solar-network');
},
),
], ],
), ),
).center(), ).center(),
@@ -108,6 +115,12 @@ class AboutScreen extends StatelessWidget {
fontSize: 12, fontSize: 12,
), ),
), ),
InkWell(
child: Text('GitHub', style: TextStyle(fontSize: 12)),
onTap: () {
launchUrlString('https://github.com/Solsynth/HyperNet.Surface');
},
)
], ],
), ),
), ),

View File

@@ -30,25 +30,21 @@ import 'package:surface/widgets/universal_image.dart';
import '../attachment/pending_attachment_compress.dart'; import '../attachment/pending_attachment_compress.dart';
class PostMediaPendingList extends StatelessWidget { class PostMediaPendingList extends StatelessWidget {
final PostWriteMedia? thumbnail;
final List<PostWriteMedia> attachments; final List<PostWriteMedia> attachments;
final bool isBusy; final bool isBusy;
final Future<void> Function(int idx, PostWriteMedia updatedMedia)? onUpdate; final Future<void> Function(int idx, PostWriteMedia updatedMedia)? onUpdate;
final Future<void> Function(int idx)? onRemove; final Future<void> Function(int idx)? onRemove;
final Future<void> Function(int idx)? onUpload; final Future<void> Function(int idx)? onUpload;
final void Function(int? idx)? onPostSetThumbnail;
final void Function(int idx)? onInsertLink; final void Function(int idx)? onInsertLink;
final void Function(bool state)? onUpdateBusy; final void Function(bool state)? onUpdateBusy;
const PostMediaPendingList({ const PostMediaPendingList({
super.key, super.key,
this.thumbnail,
required this.attachments, required this.attachments,
required this.isBusy, required this.isBusy,
this.onUpdate, this.onUpdate,
this.onRemove, this.onRemove,
this.onUpload, this.onUpload,
this.onPostSetThumbnail,
this.onInsertLink, this.onInsertLink,
this.onUpdateBusy, this.onUpdateBusy,
}); });
@@ -116,7 +112,7 @@ class PostMediaPendingList extends StatelessWidget {
} }
Future<void> _deleteAttachment(BuildContext context, int idx) async { Future<void> _deleteAttachment(BuildContext context, int idx) async {
final media = idx == -1 ? thumbnail! : attachments[idx]; final media = attachments[idx];
if (media.attachment == null) return; if (media.attachment == null) return;
try { try {
@@ -212,22 +208,6 @@ class PostMediaPendingList extends StatelessWidget {
onSelected: () { onSelected: () {
onUpload!(idx); onUpload!(idx);
}), }),
if (media.attachment != null && media.type == SnMediaType.image && onPostSetThumbnail != null && idx != -1)
MenuItem(
label: 'attachmentSetAsPostThumbnail'.tr(),
icon: Symbols.gallery_thumbnail,
onSelected: () {
onPostSetThumbnail!(idx);
},
)
else if (media.attachment != null && media.type == SnMediaType.image && onPostSetThumbnail != null)
MenuItem(
label: 'attachmentUnsetAsPostThumbnail'.tr(),
icon: Symbols.cancel,
onSelected: () {
onPostSetThumbnail!(null);
},
),
if (media.attachment != null && onInsertLink != null) if (media.attachment != null && onInsertLink != null)
MenuItem( MenuItem(
label: 'attachmentInsertLink'.tr(), label: 'attachmentInsertLink'.tr(),
@@ -291,35 +271,18 @@ class PostMediaPendingList extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
constraints: const BoxConstraints(maxHeight: 120), constraints: const BoxConstraints(maxHeight: 120),
child: Row( child: ListView.separated(
children: [ scrollDirection: Axis.horizontal,
const Gap(16), padding: const EdgeInsets.symmetric(horizontal: 8),
if (thumbnail != null) separatorBuilder: (context, index) => const Gap(8),
ContextMenuArea( itemCount: attachments.length,
contextMenu: _createContextMenu(context, -1, thumbnail!), itemBuilder: (context, idx) {
child: _PostMediaPendingItem(media: thumbnail!), final media = attachments[idx];
), return ContextMenuArea(
if (thumbnail != null) contextMenu: _createContextMenu(context, idx, media),
const VerticalDivider(width: 1, thickness: 1).padding( child: _PostMediaPendingItem(media: media),
horizontal: 12, );
vertical: 16, },
),
Expanded(
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(right: 8),
separatorBuilder: (context, index) => const Gap(8),
itemCount: attachments.length,
itemBuilder: (context, idx) {
final media = attachments[idx];
return ContextMenuArea(
contextMenu: _createContextMenu(context, idx, media),
child: _PostMediaPendingItem(media: media),
);
},
),
),
],
), ),
); );
} }

View File

@@ -0,0 +1,149 @@
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/sn_network.dart';
import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/universal_image.dart';
class RealmItemWidget extends StatelessWidget {
final SnRealm item;
final bool isListView;
final List<PopupMenuItem>? actionListView;
final Function? onUpdate;
final Function? onTap;
final bool showPopularity;
const RealmItemWidget({
super.key,
required this.item,
required this.isListView,
this.actionListView,
this.onUpdate,
this.onTap,
this.showPopularity = true,
});
@override
Widget build(BuildContext context) {
if (isListView) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: item.avatar,
fallbackWidget: const Icon(Symbols.group, size: 20),
),
title: Text(item.name),
subtitle: Row(
children: [
if (showPopularity) const Icon(Symbols.local_fire_department, size: 18).padding(right: 1),
if (showPopularity) Text(item.popularity.toString()),
if (showPopularity) const Gap(6),
Expanded(
child: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
trailing:
actionListView != null ? PopupMenuButton(itemBuilder: (BuildContext context) => actionListView!) : null,
onTap: () {
if (onTap != null) {
onTap!();
return;
}
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': item.alias},
).then((value) {
if (value == true) {
onUpdate?.call();
}
});
},
);
}
final sn = context.read<SnNetworkProvider>();
return Container(
constraints: BoxConstraints(maxWidth: 640),
child: Card(
margin: const EdgeInsets.all(12),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: (item.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoResizeUniversalImage(
sn.getAttachmentUrl(item.banner!),
fit: BoxFit.cover,
),
),
),
Positioned(
bottom: -30,
left: 18,
child: AccountImage(
content: item.avatar,
radius: 24,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
),
],
),
),
const Gap(20 + 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.name).textStyle(Theme.of(context).textTheme.titleMedium!),
if (showPopularity)
Row(
children: [
Text(item.popularity.toString()),
const Icon(Symbols.local_fire_department, size: 16).padding(bottom: 2),
],
).padding(top: 6),
Text(item.description).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
],
),
onTap: () {
if (onTap != null) {
onTap!();
return;
}
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': item.alias},
).then((value) {
if (value == true) {
onUpdate?.call();
}
});
},
),
),
).center();
}
}

View File

@@ -15,6 +15,7 @@
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h> #include <media_kit_video/media_kit_video_plugin.h>
#include <pasteboard/pasteboard_plugin.h> #include <pasteboard/pasteboard_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <tray_manager/tray_manager_plugin.h> #include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
@@ -46,6 +47,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) pasteboard_registrar = g_autoptr(FlPluginRegistrar) pasteboard_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
pasteboard_plugin_register_with_registrar(pasteboard_registrar); pasteboard_plugin_register_with_registrar(pasteboard_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) tray_manager_registrar = g_autoptr(FlPluginRegistrar) tray_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
tray_manager_plugin_register_with_registrar(tray_manager_registrar); tray_manager_plugin_register_with_registrar(tray_manager_registrar);

View File

@@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_libs_linux media_kit_libs_linux
media_kit_video media_kit_video
pasteboard pasteboard
sqlite3_flutter_libs
tray_manager tray_manager
url_launcher_linux url_launcher_linux
) )

View File

@@ -30,6 +30,7 @@ import screen_brightness_macos
import share_plus import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
import sqlite3_flutter_libs
import tray_manager import tray_manager
import url_launcher_macos import url_launcher_macos
import video_compress import video_compress
@@ -61,6 +62,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))

View File

@@ -191,7 +191,7 @@ packages:
source: hosted source: hosted
version: "3.4.1" version: "3.4.1"
cached_network_image_platform_interface: cached_network_image_platform_interface:
dependency: transitive dependency: "direct main"
description: description:
name: cached_network_image_platform_interface name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
@@ -230,6 +230,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@@ -414,6 +422,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
drift:
dependency: "direct main"
description:
name: drift
sha256: "97d5832657d49f26e7a8e07de397ddc63790b039372878d5117af816d0fdb5cb"
url: "https://pub.dev"
source: hosted
version: "2.25.1"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: f1db88482dbb016b9bbddddf746d5d0a6938b156ff20e07320052981f97388cc
url: "https://pub.dev"
source: hosted
version: "2.25.2"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922"
url: "https://pub.dev"
source: hosted
version: "0.2.4"
dropdown_button2: dropdown_button2:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1083,7 +1115,7 @@ packages:
source: hosted source: hosted
version: "0.2.1+2" version: "0.2.1+2"
image_picker_platform_interface: image_picker_platform_interface:
dependency: transitive dependency: "direct main"
description: description:
name: image_picker_platform_interface name: image_picker_platform_interface
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
@@ -1642,6 +1674,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
receive_sharing_intent: receive_sharing_intent:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1935,6 +1975,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.0"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d"
url: "https://pub.dev"
source: hosted
version: "2.7.4"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "57fafacd815c981735406215966ff7caaa8eab984b094f52e692accefcbd9233"
url: "https://pub.dev"
source: hosted
version: "0.5.30"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
url: "https://pub.dev"
source: hosted
version: "0.41.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:

View File

@@ -121,6 +121,10 @@ dependencies:
tray_manager: ^0.3.2 tray_manager: ^0.3.2
hotkey_manager: ^0.2.3 hotkey_manager: ^0.2.3
image_picker_android: ^0.8.12+20 image_picker_android: ^0.8.12+20
cached_network_image_platform_interface: ^4.1.1
image_picker_platform_interface: ^2.10.1
drift: ^2.25.1
drift_flutter: ^0.2.4
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -132,13 +136,14 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
build_runner: ^2.4.13 build_runner: ^2.4.15
freezed: ^2.5.7 freezed: ^2.5.7
json_serializable: ^6.8.0 json_serializable: ^6.8.0
icons_launcher: ^3.0.0 icons_launcher: ^3.0.0
flutter_native_splash: ^2.4.2 flutter_native_splash: ^2.4.2
hive_generator: ^2.0.1 hive_generator: ^2.0.1
flutter_launcher_icons: ^0.14.1 flutter_launcher_icons: ^0.14.1
drift_dev: ^2.25.2
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

13631
web/drift_worker.dart.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,11 @@
{ {
"name": "surface", "name": "Solar Network",
"short_name": "surface", "short_name": "Solian",
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"background_color": "#ffffff", "background_color": "#ffffff",
"theme_color": "#ffffff", "theme_color": "#ffffff",
"description": "A new Flutter project.", "description": "The Solar Network is a social network app.",
"orientation": "portrait-primary", "orientation": "portrait-primary",
"prefer_related_applications": false, "prefer_related_applications": false,
"icons": [ "icons": [

BIN
web/sqlite3.wasm Normal file

Binary file not shown.

View File

@@ -23,6 +23,7 @@
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_brightness_windows/screen_brightness_windows_plugin.h> #include <screen_brightness_windows/screen_brightness_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h> #include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <tray_manager/tray_manager_plugin.h> #include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
@@ -61,6 +62,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar( SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
TrayManagerPluginRegisterWithRegistrar( TrayManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("TrayManagerPlugin")); registry->GetRegistrarForPlugin("TrayManagerPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(

View File

@@ -20,6 +20,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows permission_handler_windows
screen_brightness_windows screen_brightness_windows
share_plus share_plus
sqlite3_flutter_libs
tray_manager tray_manager
url_launcher_windows url_launcher_windows
) )