Compare commits

..

39 Commits

Author SHA1 Message Date
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
e2ecb573a2 🚀 Launch 2.3.2+70 2025-02-18 00:52:07 +08:00
8cb5dff498 🌐 Update Traditional Chinese localization files 2025-02-18 00:43:57 +08:00
a5629975ed Content insert support 2025-02-18 00:43:12 +08:00
972b304969 Flag posts 2025-02-18 00:38:32 +08:00
e8ded55055 🐛 Fix some listing non offset bugs
 Optimize user listing speed
2025-02-18 00:07:48 +08:00
04875eb164 Support notifications with multiple attachments 2025-02-18 00:07:20 +08:00
54a59aa470 💄 Recommendation post indicator 2025-02-17 18:53:46 +08:00
365f330629 🐛 Realm related bug fixes 2025-02-16 19:50:34 +08:00
a7829d15b2 🐛 Remove android predictive back 2025-02-16 13:29:41 +08:00
a3868a4281 📝 Update README.md 2025-02-16 01:09:45 +08:00
1d1d61d60c Merge pull request #1 from I21b/master 2025-02-15 23:40:05 +08:00
03c2491587 🔨 Add debian build scripts 2025-02-15 23:04:47 +08:00
2c1adc988c 🐛 Fix desktop window title 2025-02-15 22:25:23 +08:00
c0fbee55e4 🐛 Fix linux running issue 2025-02-15 21:22:22 +08:00
6e544c0b6c 🚀 Launch 2.3.2+69 2025-02-15 19:58:11 +08:00
7d56c5ef31 🐛 Fix reply / forward post type will follow the target post 2025-02-15 19:45:33 +08:00
c2df1af16d Reply & repost indicator 2025-02-15 19:43:41 +08:00
a8143c6453 🐛 Fix publisher list did not update after created 2025-02-15 19:23:02 +08:00
04065061e0 Leave realm 2025-02-15 19:20:34 +08:00
226eb452e5 Community and public chat, realm 2025-02-15 19:16:54 +08:00
a6715b0872 Chat return new line 2025-02-15 19:08:40 +08:00
43e3404dbb Delete publisher 2025-02-15 18:43:06 +08:00
c91cf7c813 🐛 Fix send empty message 2025-02-15 18:12:35 +08:00
92
9cd1cad695 Merge branch 'Solsynth:master' into master 2025-02-15 15:00:07 +09:00
92
dde280833b idk what to say 2025-02-15 14:59:45 +09:00
42ac12b53e 🔨 Fix linux build script 2025-02-15 13:48:25 +08:00
63567bf708 🔨 Fix linux build missing deps 2025-02-15 13:43:21 +08:00
5d3cadefef 🔨 Add linux build pipeline 2025-02-15 13:39:00 +08:00
251fbb2503 🐛 Try to fix github action build error 2025-02-15 13:34:23 +08:00
0b31d32217 💄 Fix some designs issue
🐛 Fix web some pages error
2025-02-15 13:06:25 +08:00
5ddd4fed2e 🐛 Fix missing new publisher button 2025-02-15 01:17:09 +08:00
48b6d5f6c1 🐛 Fix poll 2025-02-15 00:16:06 +08:00
59 changed files with 1450 additions and 423 deletions

View File

@ -39,3 +39,26 @@ jobs:
with: with:
name: build-output-windows name: build-output-windows
path: build/windows/x64/runner/Release path: build/windows/x64/runner/Release
build-linux:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
sudo apt-get install libmpv-dev mpv
sudo apt-get install libayatana-appindicator3-dev
sudo apt-get install keybinder-3.0
- run: flutter pub get
- run: flutter build linux
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-linux
path: build/linux/x64/release/bundle

27
README.md Normal file
View File

@ -0,0 +1,27 @@
# Solar Network
![](https://solsynth.dev/_next/static/media/alpha.e779a584.webp)
Hello there! Welcome to the main repository of the HyperNet (also known as the Solar Network). The code here is mainly about the frontend app (also known as Solian). But you can still post issues here to get help and request new features!
## Sub Projects
HyperNet, the Solar Network is a microservices project in which the backends are stored in separate repositories. Here is a simple index for it.
- The Core, Gateway: [Nexus](https://github.com/Solsynth/HyperNet.Nexus)
- The Auth Service: [Passport](https://github.com/Solsynth/HyperNet.Passport)
- The Posting Service: [Interactive](https://github.com/Solsynth/HyperNet.Interactive)
- The Messaging Service: [Messaging](https://github.com/Solsynth/HyperNet.Messaging)
- The Wallet Service: [Wallet](https://github.com/Solsynth/HyperNet.Wallet)
- The Crawler: [Reader](https://github.com/Solsynth/HyperNet.Reader)
- Some others may not be listed, you can search in the organization with `HyperNet.` the prefix of all HyperNet projects.
## Tech Stack
For those people who want to know the tech stack of this project, the frontend was built by Flutter, which provides the cross-platform ability.
The backend was built in Go and PostgreSQL with our very own microservice framework included in the nexus.
-----
The readme will be updated in the future, to be determined. For now, you can check out the link of this repository to learn more on our official website.

View File

@ -17,7 +17,6 @@
android:label="Solian" android:label="Solian"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"
android:requestLegacyExternalStorage="true"> android:requestLegacyExternalStorage="true">
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"

View File

@ -54,7 +54,7 @@ class CheckInWidget : GlanceAppWidget() {
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Instant::class.java, InstantAdapter()) .registerTypeAdapter(Instant::class.java, InstantAdapter())
.create() .create()
val resultTierSymbols = listOf("大凶", "", "中平", "", "大吉") val resultTierSymbols = listOf("Bad", "Poor", "Medium", "Good", "Great")
val prefs = currentState.preferences val prefs = currentState.preferences
val checkInRaw: String? = prefs.getString("pas_check_in_record", null) val checkInRaw: String? = prefs.getString("pas_check_in_record", null)
@ -120,7 +120,7 @@ class CheckInWidget : GlanceAppWidget() {
} }
Text( Text(
text = "You haven't checked in today", text = "You haven't divined today",
style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface) style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface)
) )
} }

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/1 url: {{endpoint}}/cgi/id/dev/notify/328
body: json body: json
auth: inherit auth: inherit
} }
@ -15,12 +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": "测试", "subject": "处理该发布者 @vedal987 的决定",
"subtitle": "Alphabot です", "subtitle": "一条来自 Solar Network 客户支持的信息",
"content": "全新通知动画", "content": "您的发布者违反了我们用户协议中的「禁止冒充他人」的相关条例,经管理决定,将相关内容隐藏。冒充他人的判定无论作者是否有主观意志,只要造成了误解我们就有责任处理。希望您能理解,本次决定未作出任何帐号相关的连带处罚。",
"metadata": {
"image": "D2EDbcrsTugs3xk5"
},
"priority": 10 "priority": 10
} }
} }

View File

@ -15,7 +15,7 @@ body:json {
"client_id": "alphabot", "client_id": "alphabot",
"client_secret": "_uR0sVnHTh", "client_secret": "_uR0sVnHTh",
"remark": "新年红包", "remark": "新年红包",
"amount": 9705, "amount": 150,
"payee_id": 2 "payee_id": 18
} }
} }

View File

@ -333,6 +333,7 @@
"addAttachmentFromRandomId": "Link via RID", "addAttachmentFromRandomId": "Link via RID",
"attachmentDetailInfo": "Attachment details", "attachmentDetailInfo": "Attachment details",
"attachmentPastedImage": "Pasted Image", "attachmentPastedImage": "Pasted Image",
"attachmentInsertedImage": "Inserted Image",
"attachmentInsertLink": "Insert Link", "attachmentInsertLink": "Insert Link",
"attachmentSetAsPostThumbnail": "Set as post thumbnail", "attachmentSetAsPostThumbnail": "Set as post thumbnail",
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail", "attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
@ -419,7 +420,7 @@
"callMessageEnded": "Call lasted {}", "callMessageEnded": "Call lasted {}",
"callMessageStarted": "Call started", "callMessageStarted": "Call started",
"dailyCheckIn": "Check In", "dailyCheckIn": "Check In",
"dailyCheckInNone": "You haven't checked in today", "dailyCheckInNone": "You haven't divined today",
"dailyCheckAction": "Check in right now!", "dailyCheckAction": "Check in right now!",
"dailyCheckDetail": "Can't understand the symbol? Master, help me understand it!", "dailyCheckDetail": "Can't understand the symbol? Master, help me understand it!",
"dailyCheckDetailTitle": "{}'s fortune details", "dailyCheckDetailTitle": "{}'s fortune details",
@ -547,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",
@ -638,5 +640,35 @@
"pollVotes": { "pollVotes": {
"one": "{} vote", "one": "{} vote",
"other": "{} votes" "other": "{} votes"
} },
"publisherDelete": "Delete Publisher {}",
"publisherDeleteDescription": "Are you sure you want to delete this publisher? This operation is irreversible.",
"channelIsPublic": "Public Channel",
"channelIsPublicDescription": "The channel is public, anyone can join.",
"channelIsCommunity": "Community Channel",
"channelIsCommunityDescription": "Currently, community channel has nothing special yet.",
"realmIsPublic": "Public Realm",
"realmIsPublicDescription": "The realm is public, anyone can join.",
"realmIsCommunity": "Community Realm",
"realmIsCommunityDescription": "Community realm will be displayed on the discover page.",
"realmLeave": "Leave Realm",
"realmLeaveDescription": "Leave the current realm and delete the realm's identity.",
"checkInResultTier1": "Worst",
"checkInResultTier2": "Worse",
"checkInResultTier3": "Normal",
"checkInResultTier4": "Better",
"checkInResultTier5": "Best",
"flagPostAction": "Flag the Post",
"flagPost": "Flag objectionable content",
"flagPostDescription": "If flagged users takes 50% or more of the views, the post will be collapsed. You cannot revoke the action.",
"flaggedPost": "Post has been flagged.",
"postViews": {
"zero": "No views",
"one": "{} view",
"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."
} }

View File

@ -331,6 +331,7 @@
"addAttachmentFromRandomId": "通过访问 ID 链接", "addAttachmentFromRandomId": "通过访问 ID 链接",
"attachmentDetailInfo": "附件详细信息", "attachmentDetailInfo": "附件详细信息",
"attachmentPastedImage": "粘贴的图片", "attachmentPastedImage": "粘贴的图片",
"attachmentInsertedImage": "插入的图片",
"attachmentInsertLink": "插入连接", "attachmentInsertLink": "插入连接",
"attachmentSetAsPostThumbnail": "设置为帖子缩略图", "attachmentSetAsPostThumbnail": "设置为帖子缩略图",
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图", "attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
@ -545,6 +546,7 @@
"termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。", "termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。",
"unauthorized": "未登陆", "unauthorized": "未登陆",
"unauthorizedDescription": "登陆以探索整个 Solar Network。", "unauthorizedDescription": "登陆以探索整个 Solar Network。",
"projectDetail": "项目详情",
"serviceStatus": "服务状态", "serviceStatus": "服务状态",
"termRelated": "相关条款", "termRelated": "相关条款",
"appDetails": "应用程序详情", "appDetails": "应用程序详情",
@ -637,5 +639,34 @@
"pollVotes": { "pollVotes": {
"one": "{} 票", "one": "{} 票",
"other": "{} 票" "other": "{} 票"
} },
"publisherDelete": "删除发布者 {}",
"publisherDeleteDescription": "你确定要删除这个发布者吗?该操作不可撤销。",
"channelIsPublic": "公开频道",
"channelIsPublicDescription": "该频道是公开的,任何人都可以加入。",
"channelIsCommunity": "社区频道",
"channelIsCommunityDescription": "目前来说,社区频道还没有什么特别之处。",
"realmIsPublic": "公开领域",
"realmIsPublicDescription": "该领域是公开的,任何人都可以加入。",
"realmIsCommunity": "社区领域",
"realmIsCommunityDescription": "社区领域会显示在发现页面上。",
"realmLeave": "离开领域",
"realmLeaveDescription": "离开当前领域,并且删除领域中的身份。",
"checkInResultTier1": "大凶",
"checkInResultTier2": "凶",
"checkInResultTier3": "中平",
"checkInResultTier4": "吉",
"checkInResultTier5": "大吉",
"flagPostAction": "吹哨",
"flagPost": "吹哨不良内容",
"flagPostDescription": "吹哨不良内容,如果吹哨用户占浏览量的 50% 或以上,则帖子会被折叠。吹哨后不可撤销。",
"flaggedPost": "哨子已经吹响。",
"postViews": {
"zero": "{} 次浏览",
"one": "{} 次浏览",
"other": "{} 次浏览"
},
"attachmentBillingUploaded": "已占用的字节数",
"attachmentBillingDiscount": "免费的字节数",
"attachmentBillingHint": "滑动窗口计价®\n在24小时内上传的文件大小超出免费空间才会适用扣费。"
} }

View File

@ -331,6 +331,7 @@
"addAttachmentFromRandomId": "通過訪問 ID 鏈接", "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
"attachmentDetailInfo": "附件詳細信息", "attachmentDetailInfo": "附件詳細信息",
"attachmentPastedImage": "粘貼的圖片", "attachmentPastedImage": "粘貼的圖片",
"attachmentInsertedImage": "插入的圖片",
"attachmentInsertLink": "插入連接", "attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖", "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖", "attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
@ -545,6 +546,7 @@
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。", "termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
"unauthorized": "未登陸", "unauthorized": "未登陸",
"unauthorizedDescription": "登陸以探索整個 Solar Network。", "unauthorizedDescription": "登陸以探索整個 Solar Network。",
"projectDetail": "項目詳情",
"serviceStatus": "服務狀態", "serviceStatus": "服務狀態",
"termRelated": "相關條款", "termRelated": "相關條款",
"appDetails": "應用程序詳情", "appDetails": "應用程序詳情",
@ -624,5 +626,47 @@
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。", "realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道", "realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
"realmJoined": "已加入領域 {}。", "realmJoined": "已加入領域 {}。",
"join": "加入" "join": "加入",
"pollEditorNew": "新投票",
"pollEditorEdit": "編輯投票",
"pollEditorDelete": "刪除投票",
"pollEditorDeleteDescription": "你確定要刪除這個投票嗎?該操作不可撤銷。",
"pollEditorUnlink": "解除鏈接",
"pollOptionAdd": "添加選項",
"pollOptionName": "選項名稱",
"pollLinkExisting": "鏈接現有投票",
"pollAnswered": "答案已經反饋。",
"pollVotes": {
"one": "{} 票",
"other": "{} 票"
},
"publisherDelete": "刪除發佈者 {}",
"publisherDeleteDescription": "你確定要刪除這個發佈者嗎?該操作不可撤銷。",
"channelIsPublic": "公開頻道",
"channelIsPublicDescription": "該頻道是公開的,任何人都可以加入。",
"channelIsCommunity": "社區頻道",
"channelIsCommunityDescription": "目前來説,社區頻道還沒有什麼特別之處。",
"realmIsPublic": "公開領域",
"realmIsPublicDescription": "該領域是公開的,任何人都可以加入。",
"realmIsCommunity": "社區領域",
"realmIsCommunityDescription": "社區領域會顯示在發現頁面上。",
"realmLeave": "離開領域",
"realmLeaveDescription": "離開當前領域,並且刪除領域中的身份。",
"checkInResultTier1": "大凶",
"checkInResultTier2": "兇",
"checkInResultTier3": "中平",
"checkInResultTier4": "吉",
"checkInResultTier5": "大吉",
"flagPostAction": "吹哨",
"flagPost": "吹哨不良內容",
"flagPostDescription": "吹哨不良內容,如果吹哨用户佔瀏覽量的 50% 或以上,則帖子會被摺疊。吹哨後不可撤銷。",
"flaggedPost": "哨子已經吹響。",
"postViews": {
"zero": "{} 次瀏覽",
"one": "{} 次瀏覽",
"other": "{} 次瀏覽"
},
"attachmentBillingUploaded": "已佔用的字節數",
"attachmentBillingDiscount": "免費的字節數",
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。"
} }

View File

@ -331,6 +331,7 @@
"addAttachmentFromRandomId": "通過訪問 ID 鏈接", "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
"attachmentDetailInfo": "附件詳細信息", "attachmentDetailInfo": "附件詳細信息",
"attachmentPastedImage": "粘貼的圖片", "attachmentPastedImage": "粘貼的圖片",
"attachmentInsertedImage": "插入的圖片",
"attachmentInsertLink": "插入連接", "attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖", "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖", "attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
@ -545,6 +546,7 @@
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。", "termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
"unauthorized": "未登陸", "unauthorized": "未登陸",
"unauthorizedDescription": "登陸以探索整個 Solar Network。", "unauthorizedDescription": "登陸以探索整個 Solar Network。",
"projectDetail": "項目詳情",
"serviceStatus": "服務狀態", "serviceStatus": "服務狀態",
"termRelated": "相關條款", "termRelated": "相關條款",
"appDetails": "應用程序詳情", "appDetails": "應用程序詳情",
@ -624,5 +626,47 @@
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。", "realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道", "realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
"realmJoined": "已加入領域 {}。", "realmJoined": "已加入領域 {}。",
"join": "加入" "join": "加入",
"pollEditorNew": "新投票",
"pollEditorEdit": "編輯投票",
"pollEditorDelete": "刪除投票",
"pollEditorDeleteDescription": "你確定要刪除這個投票嗎?該操作不可撤銷。",
"pollEditorUnlink": "解除鏈接",
"pollOptionAdd": "添加選項",
"pollOptionName": "選項名稱",
"pollLinkExisting": "鏈接現有投票",
"pollAnswered": "答案已經反饋。",
"pollVotes": {
"one": "{} 票",
"other": "{} 票"
},
"publisherDelete": "刪除發佈者 {}",
"publisherDeleteDescription": "你確定要刪除這個發佈者嗎?該操作不可撤銷。",
"channelIsPublic": "公開頻道",
"channelIsPublicDescription": "該頻道是公開的,任何人都可以加入。",
"channelIsCommunity": "社區頻道",
"channelIsCommunityDescription": "目前來說,社區頻道還沒有什麼特別之處。",
"realmIsPublic": "公開領域",
"realmIsPublicDescription": "該領域是公開的,任何人都可以加入。",
"realmIsCommunity": "社區領域",
"realmIsCommunityDescription": "社區領域會顯示在發現頁面上。",
"realmLeave": "離開領域",
"realmLeaveDescription": "離開當前領域,並且刪除領域中的身份。",
"checkInResultTier1": "大凶",
"checkInResultTier2": "兇",
"checkInResultTier3": "中平",
"checkInResultTier4": "吉",
"checkInResultTier5": "大吉",
"flagPostAction": "吹哨",
"flagPost": "吹哨不良內容",
"flagPostDescription": "吹哨不良內容,如果吹哨用戶佔瀏覽量的 50% 或以上,則帖子會被摺疊。吹哨後不可撤銷。",
"flaggedPost": "哨子已經吹響。",
"postViews": {
"zero": "{} 次瀏覽",
"one": "{} 次瀏覽",
"other": "{} 次瀏覽"
},
"attachmentBillingUploaded": "已佔用的字節數",
"attachmentBillingDiscount": "免費的字節數",
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。"
} }

14
debian/debian.yml vendored Normal file
View File

@ -0,0 +1,14 @@
flutter_app:
command: surface
arch: x64
parent: /usr/local/lib
nonInteractive: false
control:
Package: solian
Version: 2.3.2
Architecture: amd64
Priority: optional
Depends: mpv keybinder-3.0
Maintainer: Solsynth LLC
Description: The Solar Network Desktop Application

9
debian/gui/surface.desktop vendored Normal file
View File

@ -0,0 +1,9 @@
[Desktop Entry]
Version=2.3.2
Name=Solian
GenericName=Solian
Comment=The Solar Network Desktop Application
Terminal=false
Type=Application
Categories=Social Networking
Keywords=social;social network;chat;solar network

23
debian/gui/surface.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 232 KiB

View File

@ -123,25 +123,35 @@ class NotificationService: UNNotificationServiceExtension {
} }
if let imageIdentifier = metadata["image"] as? String { if let imageIdentifier = metadata["image"] as? String {
attachMedia(to: content, withIdentifier: imageIdentifier, fileType: UTType.jpeg, doScaleDown: true) attachMedia(to: content, withIdentifier: [imageIdentifier], fileType: UTType.jpeg, doScaleDown: true)
} else if let avatarIdentifier = metadata["avatar"] as? String { } else if let avatarIdentifier = metadata["avatar"] as? String {
attachMedia(to: content, withIdentifier: avatarIdentifier, fileType: UTType.jpeg, doScaleDown: true) attachMedia(to: content, withIdentifier: [avatarIdentifier], fileType: UTType.jpeg, doScaleDown: true)
} else if let imagesIdentifier = metadata["images"] as? Array<String> {
attachMedia(to: content, withIdentifier: imagesIdentifier, fileType: UTType.jpeg, doScaleDown: true)
} else { } else {
contentHandler?(content) contentHandler?(content)
} }
} }
private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: String, fileType type: UTType?, doScaleDown scaleDown: Bool = false) { private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: Array<String>, fileType type: UTType?, doScaleDown scaleDown: Bool = false) {
let attachmentUrl = getAttachmentUrl(for: identifier) let attachmentUrls = identifier.compactMap { element in
return getAttachmentUrl(for: element)
}
guard let remoteUrl = URL(string: attachmentUrl) else { guard !attachmentUrls.isEmpty else {
print("Invalid URL for attachment: \(attachmentUrl)") print("Invalid URLs for attachments: \(attachmentUrls)")
return return
} }
let targetSize = 800 let targetSize = 800
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit) let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
for attachmentUrl in attachmentUrls {
guard let remoteUrl = URL(string: attachmentUrl) else {
print("Invalid URL for attachment: \(attachmentUrl)")
continue // Skip this URL and move to the next one
}
KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [ KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [
.processor(scaleProcessor) .processor(scaleProcessor)
] : nil) { [weak self] result in ] : nil) { [weak self] result in
@ -151,12 +161,12 @@ class NotificationService: UNNotificationServiceExtension {
case .success(let retrievalResult): case .success(let retrievalResult):
// The image is either retrieved from cache or downloaded // The image is either retrieved from cache or downloaded
let tempDirectory = FileManager.default.temporaryDirectory let tempDirectory = FileManager.default.temporaryDirectory
let cachedFileUrl = tempDirectory.appendingPathComponent(identifier) let cachedFileUrl = tempDirectory.appendingPathComponent(UUID().uuidString) // Unique identifier for each file
do { do {
// Write the image data to a temporary file for UNNotificationAttachment // Write the image data to a temporary file for UNNotificationAttachment
try retrievalResult.image.pngData()?.write(to: cachedFileUrl) try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: identifier) self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: attachmentUrl)
} catch { } catch {
print("Failed to write media to temporary file: \(error.localizedDescription)") print("Failed to write media to temporary file: \(error.localizedDescription)")
self.contentHandler?(content) self.contentHandler?(content)
@ -168,6 +178,7 @@ class NotificationService: UNNotificationServiceExtension {
} }
} }
} }
}
private func attachLocalMedia(to content: UNMutableNotificationContent, fileType type: String?, from localUrl: URL, withIdentifier identifier: String) { private func attachLocalMedia(to content: UNMutableNotificationContent, fileType type: String?, from localUrl: URL, withIdentifier identifier: String) {
do { do {

View File

@ -55,7 +55,7 @@ struct CheckInEntry: TimelineEntry {
struct CheckInWidgetEntryView : View { struct CheckInWidgetEntryView : View {
var entry: CheckInProvider.Entry var entry: CheckInProvider.Entry
private let resultTierSymbols: [String] = ["大凶", "", "中平", "", "大吉"] private let resultTierSymbols: [String] = ["Bad", "Poor", "Medium", "Good", "Great"]
func checkIn() -> Void {} func checkIn() -> Void {}
@ -91,7 +91,7 @@ struct CheckInWidgetEntryView : View {
} else { } else {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Check In").font(.system(size: 19, weight: .bold)) Text("Check In").font(.system(size: 19, weight: .bold))
Text("You haven't check in today").font(.system(size: 15)) Text("You haven't divined today").font(.system(size: 15))
}.padding(.horizontal, 4) }.padding(.horizontal, 4)
Spacer() Spacer()

View File

@ -158,6 +158,14 @@ class PostWriteController extends ChangeNotifier {
final TextEditingController aliasController = TextEditingController(); final TextEditingController aliasController = TextEditingController();
final TextEditingController rewardController = TextEditingController(); final TextEditingController rewardController = TextEditingController();
ContentInsertionConfiguration get contentInsertionConfiguration => ContentInsertionConfiguration(
onContentInserted: (KeyboardInsertedContent content) {
if (content.hasData) {
addAttachments([PostWriteMedia.fromBytes(content.data!, 'attachmentInsertedImage'.tr(), SnMediaType.image)]);
}
},
);
bool _temporarySaveActive = false; bool _temporarySaveActive = false;
PostWriteController({bool doLoadFromTemporary = true}) { PostWriteController({bool doLoadFromTemporary = true}) {

View File

@ -47,6 +47,8 @@ import 'package:tray_manager/tray_manager.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:workmanager/workmanager.dart'; import 'package:workmanager/workmanager.dart';
import 'package:in_app_review/in_app_review.dart'; import 'package:in_app_review/in_app_review.dart';
import 'package:image_picker_android/image_picker_android.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
@pragma('vm:entry-point') @pragma('vm:entry-point')
void appBackgroundDispatcher() { void appBackgroundDispatcher() {
@ -67,20 +69,6 @@ void appBackgroundDispatcher() {
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
await Hive.initFlutter();
Hive.registerAdapter(SnChannelImplAdapter());
Hive.registerAdapter(SnRealmImplAdapter());
Hive.registerAdapter(SnChannelMemberImplAdapter());
Hive.registerAdapter(SnChatMessageImplAdapter());
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
GoRouter.optionURLReflectsImperativeAPIs = true;
usePathUrlStrategy();
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
doWhenWindowReady(() { doWhenWindowReady(() {
@ -91,6 +79,23 @@ void main() async {
}); });
} }
await EasyLocalization.ensureInitialized();
await Hive.initFlutter();
Hive.registerAdapter(SnChannelImplAdapter());
Hive.registerAdapter(SnRealmImplAdapter());
Hive.registerAdapter(SnChannelMemberImplAdapter());
Hive.registerAdapter(SnChatMessageImplAdapter());
if (!kIsWeb && !Platform.isLinux) {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
}
GoRouter.optionURLReflectsImperativeAPIs = true;
usePathUrlStrategy();
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Workmanager().initialize( Workmanager().initialize(
appBackgroundDispatcher, appBackgroundDispatcher,
@ -107,6 +112,13 @@ void main() async {
} }
} }
if (!kIsWeb && Platform.isAndroid) {
final ImagePickerPlatform imagePickerImplementation = ImagePickerPlatform.instance;
if (imagePickerImplementation is ImagePickerAndroid) {
imagePickerImplementation.useAndroidPhotoPicker = true;
}
}
runApp(const SolianApp()); runApp(const SolianApp());
} }
@ -160,8 +172,8 @@ class SolianApp extends StatelessWidget {
), ),
), ),
breakpoints: [ breakpoints: [
const Breakpoint(start: 0, end: 450, name: MOBILE), const Breakpoint(start: 0, end: 600, name: MOBILE),
const Breakpoint(start: 451, end: 800, name: TABLET), const Breakpoint(start: 601, end: 800, name: TABLET),
const Breakpoint(start: 801, end: 1920, name: DESKTOP), const Breakpoint(start: 801, end: 1920, name: DESKTOP),
], ],
); );
@ -415,8 +427,16 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
}); });
return false; return false;
}, },
child: SizeChangedLayoutNotifier( child: OrientationBuilder(
builder: (context, orientation) {
final cfg = context.read<ConfigProvider>();
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
return SizeChangedLayoutNotifier(
child: widget.child, child: widget.child,
);
},
), ),
); );
} }

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,
@ -45,8 +46,8 @@ class ConfigProvider extends ChangeNotifier {
bool newDrawerIsCollapsed = false; bool newDrawerIsCollapsed = false;
bool newDrawerIsExpanded = false; bool newDrawerIsExpanded = false;
if (withMediaQuery) { if (withMediaQuery) {
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450; newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600;
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451; newDrawerIsExpanded = MediaQuery.of(context).size.width >= 601;
} else { } else {
final rpb = ResponsiveBreakpoints.of(context); final rpb = ResponsiveBreakpoints.of(context);
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE); newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
@ -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

@ -14,9 +14,32 @@ class UserDirectoryProvider {
final Map<int, SnAccount> _cache = {}; final Map<int, SnAccount> _cache = {};
Future<List<SnAccount?>> listAccount(Iterable<dynamic> id) async { Future<List<SnAccount?>> listAccount(Iterable<dynamic> id) async {
final out = await Future.wait( final out = List<SnAccount?>.generate(id.length, (e) => null);
id.map((e) => getAccount(e)), final plannedQuery = <int>{};
); for (var idx = 0; idx < out.length; idx++) {
var item = id.elementAt(idx);
if (item is String && _idCache.containsKey(item)) {
item = _idCache[item];
}
if (_cache.containsKey(item)) {
out[idx] = _cache[item];
} else {
plannedQuery.add(item);
}
}
final resp = await _sn.client.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
final respDecoded = resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList();
var sideIdx = 0;
for (var idx = 0; idx < out.length; idx++) {
if (out[idx] != null) continue;
if (respDecoded.length <= sideIdx) {
break;
}
out[idx] = respDecoded[sideIdx];
_cache[respDecoded[sideIdx].id] = out[idx]!;
_idCache[respDecoded[sideIdx].name] = respDecoded[sideIdx].id;
sideIdx++;
}
return out; return out;
} }

View File

@ -42,13 +42,14 @@ class WebSocketProvider extends ChangeNotifier {
_connectCompleter = null; _connectCompleter = null;
} }
_connectCompleter = Completer<void>();
if (!_ua.isAuthorized) return; if (!_ua.isAuthorized) return;
if (isConnected || conn != null) { if (isConnected || conn != null) {
disconnect(); disconnect();
} }
try {
_connectCompleter = Completer<void>();
final atk = await _sn.getFreshAtk(); final atk = await _sn.getFreshAtk();
final uri = Uri.parse( final uri = Uri.parse(
'${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk', '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk',
@ -57,7 +58,6 @@ class WebSocketProvider extends ChangeNotifier {
isBusy = true; isBusy = true;
notifyListeners(); notifyListeners();
try {
conn = WebSocketChannel.connect(uri); conn = WebSocketChannel.connect(uri);
await conn!.ready; await conn!.ready;
_wsStream = conn!.stream.asBroadcastStream(); _wsStream = conn!.stream.asBroadcastStream();
@ -82,6 +82,7 @@ class WebSocketProvider extends ChangeNotifier {
isBusy = false; isBusy = false;
notifyListeners(); notifyListeners();
_connectCompleter!.complete(); _connectCompleter!.complete();
_connectCompleter = null;
} }
} }

View File

@ -74,7 +74,10 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
), ),
const Divider(height: 1), const Divider(height: 1),
if (_isBusy) if (_isBusy)
const CircularProgressIndicator().padding(all: 24).center() Padding(
padding: const EdgeInsets.all(24),
child: const CircularProgressIndicator(),
).center()
else else
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(

View File

@ -45,6 +45,33 @@ class _PublisherScreenState extends State<PublisherScreen> {
} }
} }
Future<void> _deletePublisher(SnPublisher publisher) async {
final confirm = await context.showConfirmDialog(
'publisherDelete'.tr(args: ['#${publisher.name}']),
'publisherDeleteDescription'.tr(),
);
if (!confirm) return;
if (!mounted) return;
setState(() => _isBusy = true);
try {
await context
.read<SnNetworkProvider>()
.client
.delete('/cgi/co/publishers/${publisher.name}');
if (!mounted) return;
context.showSnackbar('publisherDeleted'.tr(args: ['#${publisher.name}']));
_publishers.remove(publisher);
_fetchPublishers();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -118,6 +145,18 @@ class _PublisherScreenState extends State<PublisherScreen> {
}); });
}, },
), ),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deletePublisher(publisher);
},
),
], ],
), ),
); );

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,
@ -123,8 +183,10 @@ class _AlbumScreenState extends State<AlbumScreen> {
), ),
if (_isBusy) if (_isBusy)
SliverToBoxAdapter( SliverToBoxAdapter(
child: child: Padding(
const CircularProgressIndicator().padding(all: 24).center(), padding: const EdgeInsets.all(24),
child: const CircularProgressIndicator(),
).center(),
), ),
], ],
), ),

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}/members/me',
); );
if (!mounted) return; if (!mounted) return;
Navigator.pop(context, false); Navigator.pop(context, false);
@ -474,7 +474,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/im/channels/${widget.channel.keyPath}/members', queryParameters: { final resp = await sn.client.get('/cgi/im/channels/${widget.channel.keyPath}/members', queryParameters: {
'take': 10, 'take': 10,
'offset': 0, 'offset': _members.length,
}); });
final out = List<SnChannelMember>.from( final out = List<SnChannelMember>.from(
resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [], resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],

View File

@ -37,6 +37,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
SnChannel? _editingChannel; SnChannel? _editingChannel;
bool _isPublic = false;
bool _isCommunity = false;
Future<void> _fetchRealms() async { Future<void> _fetchRealms() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
try { try {
@ -67,6 +70,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
_aliasController.text = _editingChannel!.alias; _aliasController.text = _editingChannel!.alias;
_nameController.text = _editingChannel!.name; _nameController.text = _editingChannel!.name;
_descriptionController.text = _editingChannel!.description; _descriptionController.text = _editingChannel!.description;
_isPublic = _editingChannel!.isPublic;
_isCommunity = _editingChannel!.isCommunity;
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -88,6 +93,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
: uuid.v4().replaceAll('-', '').substring(0, 12), : uuid.v4().replaceAll('-', '').substring(0, 12),
'name': _nameController.text, 'name': _nameController.text,
'description': _descriptionController.text, 'description': _descriptionController.text,
'is_public': _isPublic,
'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 {
@ -164,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: [
@ -197,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: [
@ -271,6 +280,23 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), const Gap(12),
CheckboxListTile(
value: _isPublic,
title: Text('channelIsPublic'.tr()),
subtitle: Text('channelIsPublicDescription'.tr()),
onChanged: (value) {
setState(() => _isPublic = value ?? false);
},
),
CheckboxListTile(
value: _isCommunity,
title: Text('channelIsCommunity'.tr()),
subtitle: Text('channelIsCommunityDescription'.tr()),
onChanged: (value) {
setState(() => _isCommunity = value ?? false);
},
),
const Gap(12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [

View File

@ -131,6 +131,7 @@ class _HomeDashUpdateWidget extends StatelessWidget {
return Container( return Container(
padding: padding, padding: padding,
child: Card( child: Card(
margin: EdgeInsets.zero,
child: ListTile( child: ListTile(
leading: Icon(Symbols.update), leading: Icon(Symbols.update),
title: Text('updateAvailable').tr(), title: Text('updateAvailable').tr(),
@ -180,6 +181,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
return Column( return Column(
children: days.map((ele) { children: days.map((ele) {
return Card( return Card(
margin: EdgeInsets.zero,
child: ListTile( child: ListTile(
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24), leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']), title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
@ -203,6 +205,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
final progress = dayz.getSpecialDayProgress(lastOne.$2, date); final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
final diff = nextOne.$2.difference(DateTime.now()); final diff = nextOne.$2.difference(DateTime.now());
return Card( return Card(
margin: EdgeInsets.zero,
child: ListTile( child: ListTile(
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24), leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]), title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
@ -270,6 +273,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
margin: EdgeInsets.zero,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -469,6 +473,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
margin: EdgeInsets.zero,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -594,6 +599,7 @@ class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
margin: EdgeInsets.zero,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -657,23 +663,42 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
} }
} }
int _currentPage = 0;
final PageController _pageController = PageController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fetchRecommendationPosts(); _fetchRecommendationPosts();
_pageController.addListener(() {
setState(() {
_currentPage = _pageController.page?.round() ?? 0;
});
});
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isBusy) { if (_isBusy) {
return Card( return Card(
margin: EdgeInsets.zero,
child: CircularProgressIndicator().center(), child: CircularProgressIndicator().center(),
); );
} }
return Card( return Card(
margin: EdgeInsets.zero,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Row(
children: [ children: [
@ -682,11 +707,15 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
Text( Text(
'postRecommendation', 'postRecommendation',
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
).tr() ).tr(),
],
),
Text('${_currentPage + 1}/${_posts?.length ?? 0}', style: GoogleFonts.robotoMono())
], ],
).padding(horizontal: 18, top: 12, bottom: 8), ).padding(horizontal: 18, top: 12, bottom: 8),
Expanded( Expanded(
child: PageView.builder( child: PageView.builder(
controller: _pageController,
scrollBehavior: ScrollConfiguration.of(context).copyWith(dragDevices: { scrollBehavior: ScrollConfiguration.of(context).copyWith(dragDevices: {
PointerDeviceKind.mouse, PointerDeviceKind.mouse,
PointerDeviceKind.touch, PointerDeviceKind.touch,

View File

@ -3,6 +3,7 @@ import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.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:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
@ -59,10 +60,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
final resp = await sn.client.get('/cgi/id/notifications?take=10'); final resp = await sn.client.get('/cgi/id/notifications?take=10');
_totalCount = resp.data['count']; _totalCount = resp.data['count'];
_notifications.addAll( _notifications.addAll(
resp.data['data'] resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? [],
?.map((e) => SnNotification.fromJson(e))
.cast<SnNotification>() ??
[],
); );
nty.updateTray(); nty.updateTray();
} catch (err) { } catch (err) {
@ -188,8 +186,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
_fetchNotifications(); _fetchNotifications();
}, },
isLoading: _isBusy, isLoading: _isBusy,
hasReachedMax: _totalCount != null && hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!,
_notifications.length >= _totalCount!,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final nty = _notifications[idx]; final nty = _notifications[idx];
return Row( return Row(
@ -221,15 +218,13 @@ class _NotificationScreenState extends State<NotificationScreen> {
isAutoWarp: true, isAutoWarp: true,
), ),
), ),
if ([ if (['interactive.reply', 'interactive.feedback', 'interactive.subscription']
'interactive.feedback', .contains(nty.topic) &&
'interactive.subscription'
].contains(nty.topic) &&
nty.metadata['related_post'] != null) nty.metadata['related_post'] != null)
StyledWidget(Container( GestureDetector(
child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(Radius.circular(8)),
Radius.circular(8)),
border: Border.all( border: Border.all(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1, width: 1,
@ -243,7 +238,16 @@ class _NotificationScreenState extends State<NotificationScreen> {
showReactions: false, showReactions: false,
showMenu: false, showMenu: false,
), ),
)).padding(top: 8), ),
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {
'slug': nty.metadata['related_post']!['id'].toString(),
},
);
},
).padding(top: 8),
const Gap(8), const Gap(8),
Row( Row(
children: [ children: [
@ -268,10 +272,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
IconButton( IconButton(
icon: const Icon(Symbols.check), icon: const Icon(Symbols.check),
padding: EdgeInsets.all(0), padding: EdgeInsets.all(0),
visualDensity: visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
const VisualDensity(horizontal: -4, vertical: -4), onPressed: _isSubmitting ? null : () => _markOneAsRead(nty),
onPressed:
_isSubmitting ? null : () => _markOneAsRead(nty),
), ),
], ],
).padding(horizontal: 16); ).padding(horizontal: 16);

View File

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.dart'; import 'package:pasteboard/pasteboard.dart';
@ -110,7 +111,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
final HotKey _pasteHotKey = HotKey( final HotKey _pasteHotKey = HotKey(
key: PhysicalKeyboardKey.keyV, key: PhysicalKeyboardKey.keyV,
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control], modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
scope: HotKeyScope.inapp, scope: HotKeyScope.inapp,
); );
@ -136,6 +137,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
builder: (context) => _PostPublisherPopup( builder: (context) => _PostPublisherPopup(
controller: _writeController, controller: _writeController,
publishers: _publishers, publishers: _publishers,
onUpdate: () {
_fetchPublishers();
},
), ),
); );
} }
@ -160,7 +164,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
@override @override
void dispose() { void dispose() {
_writeController.dispose(); _writeController.dispose();
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey); if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
hotKeyManager.unregister(_pasteHotKey);
}
super.dispose(); super.dispose();
} }
@ -250,6 +256,62 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
], ],
), ),
), ),
if (_writeController.replyingPost != null)
Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.reply, size: 16),
const Gap(10),
Text('@${_writeController.replyingPost!.publisher.name}').bold(),
const Gap(4),
Expanded(
child: Text(
_writeController.replyingPost!.body['content'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
if (_writeController.repostingPost != null)
Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.forward, size: 16),
const Gap(10),
Text('@${_writeController.repostingPost!.publisher.name}').bold(),
const Gap(4),
Expanded(
child: Text(
_writeController.repostingPost!.body['content'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Expanded( Expanded(
child: Stack( child: Stack(
children: [ children: [
@ -434,8 +496,9 @@ class _PostEditorActionScrollBehavior extends MaterialScrollBehavior {
class _PostPublisherPopup extends StatelessWidget { class _PostPublisherPopup extends StatelessWidget {
final PostWriteController controller; final PostWriteController controller;
final List<SnPublisher>? publishers; final List<SnPublisher>? publishers;
final Function onUpdate;
const _PostPublisherPopup({required this.controller, this.publishers}); const _PostPublisherPopup({required this.controller, this.publishers, required this.onUpdate});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -450,6 +513,20 @@ class _PostPublisherPopup extends StatelessWidget {
Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(), Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(),
], ],
).padding(horizontal: 20, top: 16, bottom: 12), ).padding(horizontal: 20, top: 16, bottom: 12),
ListTile(
leading: const Icon(Symbols.add),
title: Text('publishersNew').tr(),
subtitle: Text('publisherNewSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () {
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
if (value == true) {
onUpdate();
}
});
},
),
const Divider(height: 1),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: publishers?.length ?? 0, itemCount: publishers?.length ?? 0,
@ -525,6 +602,7 @@ class _PostStoryEditor extends StatelessWidget {
border: InputBorder.none, border: InputBorder.none,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
), ),
], ],
), ),
@ -588,6 +666,7 @@ class _PostArticleEditor extends StatelessWidget {
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
).padding(horizontal: 16), ).padding(horizontal: 16),
const Gap(4), const Gap(4),
]; ];
@ -615,6 +694,7 @@ class _PostArticleEditor extends StatelessWidget {
border: InputBorder.none, border: InputBorder.none,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
), ),
), ),
const Gap(8), const Gap(8),
@ -649,6 +729,7 @@ class _PostArticleEditor extends StatelessWidget {
border: InputBorder.none, border: InputBorder.none,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
), ),
), ),
], ],
@ -720,6 +801,7 @@ class _PostQuestionEditor extends StatelessWidget {
border: InputBorder.none, border: InputBorder.none,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
), ),
], ],
), ),

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,21 +134,12 @@ 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( return RealmItemWidget(
contentPadding: const EdgeInsets.symmetric(horizontal: 16), showPopularity: false,
leading: AccountImage( item: realm,
content: realm.avatar, isListView: _isCompactView,
fallbackWidget: const Icon(Symbols.group, size: 20), actionListView: [
),
title: Text(realm.name),
subtitle: Text(
realm.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: PopupMenuButton(
itemBuilder: (BuildContext context) => [
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@ -181,75 +172,9 @@ class _RealmScreenState extends State<RealmScreen> {
}, },
), ),
], ],
), onUpdate: _fetchRealms,
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': realm.alias},
); );
}, },
);
}
return Container(
constraints: BoxConstraints(maxWidth: 640),
child: Card(
margin: const EdgeInsets.all(12),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: (realm.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoResizeUniversalImage(
sn.getAttachmentUrl(realm.banner!),
fit: BoxFit.cover,
),
),
),
Positioned(
bottom: -30,
left: 18,
child: AccountImage(
content: realm.avatar,
radius: 24,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
),
],
),
),
const Gap(20 + 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': realm.alias},
);
},
),
),
).center();
},
), ),
), ),
), ),

View File

@ -50,6 +50,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
_aliasController.text = out.alias; _aliasController.text = out.alias;
_nameController.text = out.name; _nameController.text = out.name;
_descriptionController.text = out.description; _descriptionController.text = out.description;
_isPublic = out.isPublic;
_isCommunity = out.isCommunity;
} catch (err) { } catch (err) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
if (context.mounted) context.showErrorDialog(err); if (context.mounted) context.showErrorDialog(err);
@ -67,6 +69,9 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
final _imagePicker = ImagePicker(); final _imagePicker = ImagePicker();
bool _isPublic = false;
bool _isCommunity = false;
Future<void> _updateImage(String place) async { Future<void> _updateImage(String place) 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;
@ -138,6 +143,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
'description': _descriptionController.text, 'description': _descriptionController.text,
'avatar': _avatar, 'avatar': _avatar,
'banner': _banner, 'banner': _banner,
'is_public': _isPublic,
'is_community': _isCommunity,
}; };
try { try {
@ -293,6 +300,23 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), const Gap(12),
CheckboxListTile(
value: _isPublic,
title: Text('realmIsPublic'.tr()),
subtitle: Text('realmIsPublicDescription'.tr()),
onChanged: (value) {
setState(() => _isPublic = value ?? false);
},
),
CheckboxListTile(
value: _isCommunity,
title: Text('realmIsCommunity'.tr()),
subtitle: Text('realmIsCommunityDescription'.tr()),
onChanged: (value) {
setState(() => _isCommunity = value ?? false);
},
),
const Gap(12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [

View File

@ -189,7 +189,7 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: { final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: {
'take': 10, 'take': 10,
'offset': 0, 'offset': _members.length,
}); });
final out = List<SnRealmMember>.from( final out = List<SnRealmMember>.from(
@ -343,12 +343,31 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
try { try {
await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}'); await sn.client.delete('/cgi/id/realms/${widget.realm!.id}');
if (!mounted) return;
Navigator.pop(context, true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _leaveRealm() async {
final confirm = await context.showConfirmDialog(
'realmLeave'.tr(),
'realmLeaveDescription'.tr(),
);
if (!confirm) return;
if (!mounted) return;
final sn = context.read<SnNetworkProvider>();
try {
await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}/members/me');
if (!mounted) return; if (!mounted) return;
Navigator.pop(context, true); Navigator.pop(context, true);
context.showSnackbar('realmDeleted'.tr(args: [
'#${widget.realm!.alias}',
]));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -366,6 +385,15 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
return Column( return Column(
children: [ children: [
const Gap(8), const Gap(8),
ListTile(
leading: const Icon(Symbols.logout),
trailing: const Icon(Symbols.chevron_right),
title: Text('realmLeave').tr(),
subtitle: Text('realmLeaveDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: _isBusy ? null : () => _leaveRealm(),
),
if (isOwned)
ListTile( ListTile(
leading: const Icon(Symbols.edit), leading: const Icon(Symbols.edit),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),

View File

@ -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,6 +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/realm/realm_item.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
class RealmDiscoveryScreen extends StatefulWidget { class RealmDiscoveryScreen extends StatefulWidget {
@ -24,6 +26,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 +47,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 +78,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),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: (realm.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoResizeUniversalImage(
sn.getAttachmentUrl(realm.banner!),
fit: BoxFit.cover,
),
),
),
Positioned(
bottom: -30,
left: 18,
child: AccountImage(
content: realm.avatar,
radius: 24,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
),
],
),
),
const Gap(20 + 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
],
),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => _RealmJoinPopup(realm: realm), builder: (context) => _RealmJoinPopup(realm: realm),
); );
}, },
), );
),
).center();
}, },
), ),
), ),
@ -154,7 +118,7 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
try { try {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}'); final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
final out = List<SnChannel>.from( final out = List<SnChannel>.from(
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(), resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
); );
@ -235,6 +199,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

@ -57,7 +57,7 @@ Future<ThemeData> createAppTheme(
), ),
pageTransitionsTheme: PageTransitionsTheme( pageTransitionsTheme: PageTransitionsTheme(
builders: { builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.macOS: ZoomPageTransitionsBuilder(), TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(), TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),

View File

@ -20,7 +20,7 @@ class SnAccount with _$SnAccount {
required String description, required String description,
required String name, required String name,
required String nick, required String nick,
required Map<String, dynamic> permNodes, @Default({}) Map<String, dynamic> permNodes,
required String language, required String language,
required SnAccountProfile? profile, required SnAccountProfile? profile,
@Default([]) List<SnAccountBadge> badges, @Default([]) List<SnAccountBadge> badges,

View File

@ -385,7 +385,7 @@ class _$SnAccountImpl extends _SnAccount {
required this.description, required this.description,
required this.name, required this.name,
required this.nick, required this.nick,
required final Map<String, dynamic> permNodes, final Map<String, dynamic> permNodes = const {},
required this.language, required this.language,
required this.profile, required this.profile,
final List<SnAccountBadge> badges = const [], final List<SnAccountBadge> badges = const [],
@ -437,6 +437,7 @@ class _$SnAccountImpl extends _SnAccount {
final String nick; final String nick;
final Map<String, dynamic> _permNodes; final Map<String, dynamic> _permNodes;
@override @override
@JsonKey()
Map<String, dynamic> get permNodes { Map<String, dynamic> get permNodes {
if (_permNodes is EqualUnmodifiableMapView) return _permNodes; if (_permNodes is EqualUnmodifiableMapView) return _permNodes;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
@ -566,7 +567,7 @@ abstract class _SnAccount extends SnAccount {
required final String description, required final String description,
required final String name, required final String name,
required final String nick, required final String nick,
required final Map<String, dynamic> permNodes, final Map<String, dynamic> permNodes,
required final String language, required final String language,
required final SnAccountProfile? profile, required final SnAccountProfile? profile,
final List<SnAccountBadge> badges, final List<SnAccountBadge> badges,

View File

@ -25,7 +25,7 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
description: json['description'] as String, description: json['description'] as String,
name: json['name'] as String, name: json['name'] as String,
nick: json['nick'] as String, nick: json['nick'] as String,
permNodes: json['perm_nodes'] as Map<String, dynamic>, permNodes: json['perm_nodes'] as Map<String, dynamic>? ?? const {},
language: json['language'] as String, language: json['language'] as String,
profile: json['profile'] == null profile: json['profile'] == null
? null ? null

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,9 +1,17 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'check_in.freezed.dart'; part 'check_in.freezed.dart';
part 'check_in.g.dart'; part 'check_in.g.dart';
const List<String> kCheckInResultTierSymbols = ['大凶', '', '中平', '', '大吉']; final List<String> kCheckInResultTierSymbols = [
'checkInResultTier1',
'checkInResultTier2',
'checkInResultTier3',
'checkInResultTier4',
'checkInResultTier5'
].map((e) => e.tr()).toList();
@freezed @freezed
class SnCheckInRecord with _$SnCheckInRecord { class SnCheckInRecord with _$SnCheckInRecord {
@ -21,8 +29,7 @@ class SnCheckInRecord with _$SnCheckInRecord {
required int accountId, required int accountId,
}) = _SnCheckInRecord; }) = _SnCheckInRecord;
factory SnCheckInRecord.fromJson(Map<String, dynamic> json) => factory SnCheckInRecord.fromJson(Map<String, dynamic> json) => _$SnCheckInRecordFromJson(json);
_$SnCheckInRecordFromJson(json);
String get symbol => kCheckInResultTierSymbols[resultTier]; String get symbol => kCheckInResultTierSymbols[resultTier];
} }

View File

@ -16,8 +16,7 @@ class SnPoll with _$SnPoll {
required SnPollMetric metric, required SnPollMetric metric,
}) = _SnPoll; }) = _SnPoll;
factory SnPoll.fromJson(Map<String, Object?> json) => factory SnPoll.fromJson(Map<String, Object?> json) => _$SnPollFromJson(json);
_$SnPollFromJson(json);
} }
@freezed @freezed
@ -25,11 +24,11 @@ class SnPollMetric with _$SnPollMetric {
const factory SnPollMetric({ const factory SnPollMetric({
required int totalAnswer, required int totalAnswer,
@Default({}) Map<String, int> byOptions, @Default({}) Map<String, int> byOptions,
@Default({}) Map<String, int> byOptionsPercentage, @Default({}) Map<String, double> byOptionsPercentage,
}) = _SnPollMetric; }) = _SnPollMetric;
factory SnPollMetric.fromJson(Map<String, Object?> json) factory SnPollMetric.fromJson(Map<String, Object?> json) =>
=> _$SnPollMetricFromJson(json); _$SnPollMetricFromJson(json);
} }
@freezed @freezed
@ -41,6 +40,6 @@ class SnPollOption with _$SnPollOption {
required String description, required String description,
}) = _SnPollOption; }) = _SnPollOption;
factory SnPollOption.fromJson(Map<String, Object?> json) factory SnPollOption.fromJson(Map<String, Object?> json) =>
=> _$SnPollOptionFromJson(json); _$SnPollOptionFromJson(json);
} }

View File

@ -345,7 +345,7 @@ SnPollMetric _$SnPollMetricFromJson(Map<String, dynamic> json) {
mixin _$SnPollMetric { mixin _$SnPollMetric {
int get totalAnswer => throw _privateConstructorUsedError; int get totalAnswer => throw _privateConstructorUsedError;
Map<String, int> get byOptions => throw _privateConstructorUsedError; Map<String, int> get byOptions => throw _privateConstructorUsedError;
Map<String, int> get byOptionsPercentage => Map<String, double> get byOptionsPercentage =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
/// Serializes this SnPollMetric to a JSON map. /// Serializes this SnPollMetric to a JSON map.
@ -367,7 +367,7 @@ abstract class $SnPollMetricCopyWith<$Res> {
$Res call( $Res call(
{int totalAnswer, {int totalAnswer,
Map<String, int> byOptions, Map<String, int> byOptions,
Map<String, int> byOptionsPercentage}); Map<String, double> byOptionsPercentage});
} }
/// @nodoc /// @nodoc
@ -401,7 +401,7 @@ class _$SnPollMetricCopyWithImpl<$Res, $Val extends SnPollMetric>
byOptionsPercentage: null == byOptionsPercentage byOptionsPercentage: null == byOptionsPercentage
? _value.byOptionsPercentage ? _value.byOptionsPercentage
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable : byOptionsPercentage // ignore: cast_nullable_to_non_nullable
as Map<String, int>, as Map<String, double>,
) as $Val); ) as $Val);
} }
} }
@ -417,7 +417,7 @@ abstract class _$$SnPollMetricImplCopyWith<$Res>
$Res call( $Res call(
{int totalAnswer, {int totalAnswer,
Map<String, int> byOptions, Map<String, int> byOptions,
Map<String, int> byOptionsPercentage}); Map<String, double> byOptionsPercentage});
} }
/// @nodoc /// @nodoc
@ -449,7 +449,7 @@ class __$$SnPollMetricImplCopyWithImpl<$Res>
byOptionsPercentage: null == byOptionsPercentage byOptionsPercentage: null == byOptionsPercentage
? _value._byOptionsPercentage ? _value._byOptionsPercentage
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable : byOptionsPercentage // ignore: cast_nullable_to_non_nullable
as Map<String, int>, as Map<String, double>,
)); ));
} }
} }
@ -460,7 +460,7 @@ class _$SnPollMetricImpl implements _SnPollMetric {
const _$SnPollMetricImpl( const _$SnPollMetricImpl(
{required this.totalAnswer, {required this.totalAnswer,
final Map<String, int> byOptions = const {}, final Map<String, int> byOptions = const {},
final Map<String, int> byOptionsPercentage = const {}}) final Map<String, double> byOptionsPercentage = const {}})
: _byOptions = byOptions, : _byOptions = byOptions,
_byOptionsPercentage = byOptionsPercentage; _byOptionsPercentage = byOptionsPercentage;
@ -478,10 +478,10 @@ class _$SnPollMetricImpl implements _SnPollMetric {
return EqualUnmodifiableMapView(_byOptions); return EqualUnmodifiableMapView(_byOptions);
} }
final Map<String, int> _byOptionsPercentage; final Map<String, double> _byOptionsPercentage;
@override @override
@JsonKey() @JsonKey()
Map<String, int> get byOptionsPercentage { Map<String, double> get byOptionsPercentage {
if (_byOptionsPercentage is EqualUnmodifiableMapView) if (_byOptionsPercentage is EqualUnmodifiableMapView)
return _byOptionsPercentage; return _byOptionsPercentage;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
@ -534,7 +534,7 @@ abstract class _SnPollMetric implements SnPollMetric {
const factory _SnPollMetric( const factory _SnPollMetric(
{required final int totalAnswer, {required final int totalAnswer,
final Map<String, int> byOptions, final Map<String, int> byOptions,
final Map<String, int> byOptionsPercentage}) = _$SnPollMetricImpl; final Map<String, double> byOptionsPercentage}) = _$SnPollMetricImpl;
factory _SnPollMetric.fromJson(Map<String, dynamic> json) = factory _SnPollMetric.fromJson(Map<String, dynamic> json) =
_$SnPollMetricImpl.fromJson; _$SnPollMetricImpl.fromJson;
@ -544,7 +544,7 @@ abstract class _SnPollMetric implements SnPollMetric {
@override @override
Map<String, int> get byOptions; Map<String, int> get byOptions;
@override @override
Map<String, int> get byOptionsPercentage; Map<String, double> get byOptionsPercentage;
/// Create a copy of SnPollMetric /// Create a copy of SnPollMetric
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View File

@ -40,7 +40,7 @@ _$SnPollMetricImpl _$$SnPollMetricImplFromJson(Map<String, dynamic> json) =>
const {}, const {},
byOptionsPercentage: byOptionsPercentage:
(json['by_options_percentage'] as Map<String, dynamic>?)?.map( (json['by_options_percentage'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, (e as num).toInt()), (k, e) => MapEntry(k, (e as num).toDouble()),
) ?? ) ??
const {}, const {},
); );

View File

@ -37,6 +37,8 @@ class SnPost with _$SnPost {
required DateTime? publishedUntil, required DateTime? publishedUntil,
required int totalUpvote, required int totalUpvote,
required int totalDownvote, required int totalDownvote,
@Default(0) int totalViews,
@Default(0) int totalAggregatedViews,
required int publisherId, required int publisherId,
required int? pollId, required int? pollId,
required SnPublisher publisher, required SnPublisher publisher,

View File

@ -47,6 +47,8 @@ mixin _$SnPost {
DateTime? get publishedUntil => throw _privateConstructorUsedError; DateTime? get publishedUntil => throw _privateConstructorUsedError;
int get totalUpvote => throw _privateConstructorUsedError; int get totalUpvote => throw _privateConstructorUsedError;
int get totalDownvote => throw _privateConstructorUsedError; int get totalDownvote => throw _privateConstructorUsedError;
int get totalViews => throw _privateConstructorUsedError;
int get totalAggregatedViews => throw _privateConstructorUsedError;
int get publisherId => throw _privateConstructorUsedError; int get publisherId => throw _privateConstructorUsedError;
int? get pollId => throw _privateConstructorUsedError; int? get pollId => throw _privateConstructorUsedError;
SnPublisher get publisher => throw _privateConstructorUsedError; SnPublisher get publisher => throw _privateConstructorUsedError;
@ -95,6 +97,8 @@ abstract class $SnPostCopyWith<$Res> {
DateTime? publishedUntil, DateTime? publishedUntil,
int totalUpvote, int totalUpvote,
int totalDownvote, int totalDownvote,
int totalViews,
int totalAggregatedViews,
int publisherId, int publisherId,
int? pollId, int? pollId,
SnPublisher publisher, SnPublisher publisher,
@ -150,6 +154,8 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
Object? publishedUntil = freezed, Object? publishedUntil = freezed,
Object? totalUpvote = null, Object? totalUpvote = null,
Object? totalDownvote = null, Object? totalDownvote = null,
Object? totalViews = null,
Object? totalAggregatedViews = null,
Object? publisherId = null, Object? publisherId = null,
Object? pollId = freezed, Object? pollId = freezed,
Object? publisher = null, Object? publisher = null,
@ -265,6 +271,14 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
? _value.totalDownvote ? _value.totalDownvote
: totalDownvote // ignore: cast_nullable_to_non_nullable : totalDownvote // ignore: cast_nullable_to_non_nullable
as int, as int,
totalViews: null == totalViews
? _value.totalViews
: totalViews // ignore: cast_nullable_to_non_nullable
as int,
totalAggregatedViews: null == totalAggregatedViews
? _value.totalAggregatedViews
: totalAggregatedViews // ignore: cast_nullable_to_non_nullable
as int,
publisherId: null == publisherId publisherId: null == publisherId
? _value.publisherId ? _value.publisherId
: publisherId // ignore: cast_nullable_to_non_nullable : publisherId // ignore: cast_nullable_to_non_nullable
@ -386,6 +400,8 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
DateTime? publishedUntil, DateTime? publishedUntil,
int totalUpvote, int totalUpvote,
int totalDownvote, int totalDownvote,
int totalViews,
int totalAggregatedViews,
int publisherId, int publisherId,
int? pollId, int? pollId,
SnPublisher publisher, SnPublisher publisher,
@ -444,6 +460,8 @@ class __$$SnPostImplCopyWithImpl<$Res>
Object? publishedUntil = freezed, Object? publishedUntil = freezed,
Object? totalUpvote = null, Object? totalUpvote = null,
Object? totalDownvote = null, Object? totalDownvote = null,
Object? totalViews = null,
Object? totalAggregatedViews = null,
Object? publisherId = null, Object? publisherId = null,
Object? pollId = freezed, Object? pollId = freezed,
Object? publisher = null, Object? publisher = null,
@ -559,6 +577,14 @@ class __$$SnPostImplCopyWithImpl<$Res>
? _value.totalDownvote ? _value.totalDownvote
: totalDownvote // ignore: cast_nullable_to_non_nullable : totalDownvote // ignore: cast_nullable_to_non_nullable
as int, as int,
totalViews: null == totalViews
? _value.totalViews
: totalViews // ignore: cast_nullable_to_non_nullable
as int,
totalAggregatedViews: null == totalAggregatedViews
? _value.totalAggregatedViews
: totalAggregatedViews // ignore: cast_nullable_to_non_nullable
as int,
publisherId: null == publisherId publisherId: null == publisherId
? _value.publisherId ? _value.publisherId
: publisherId // ignore: cast_nullable_to_non_nullable : publisherId // ignore: cast_nullable_to_non_nullable
@ -614,6 +640,8 @@ class _$SnPostImpl extends _SnPost {
required this.publishedUntil, required this.publishedUntil,
required this.totalUpvote, required this.totalUpvote,
required this.totalDownvote, required this.totalDownvote,
this.totalViews = 0,
this.totalAggregatedViews = 0,
required this.publisherId, required this.publisherId,
required this.pollId, required this.pollId,
required this.publisher, required this.publisher,
@ -731,6 +759,12 @@ class _$SnPostImpl extends _SnPost {
@override @override
final int totalDownvote; final int totalDownvote;
@override @override
@JsonKey()
final int totalViews;
@override
@JsonKey()
final int totalAggregatedViews;
@override
final int publisherId; final int publisherId;
@override @override
final int? pollId; final int? pollId;
@ -743,7 +777,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, 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, 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
@ -796,6 +830,10 @@ class _$SnPostImpl extends _SnPost {
other.totalUpvote == totalUpvote) && other.totalUpvote == totalUpvote) &&
(identical(other.totalDownvote, totalDownvote) || (identical(other.totalDownvote, totalDownvote) ||
other.totalDownvote == totalDownvote) && other.totalDownvote == totalDownvote) &&
(identical(other.totalViews, totalViews) ||
other.totalViews == totalViews) &&
(identical(other.totalAggregatedViews, totalAggregatedViews) ||
other.totalAggregatedViews == totalAggregatedViews) &&
(identical(other.publisherId, publisherId) || (identical(other.publisherId, publisherId) ||
other.publisherId == publisherId) && other.publisherId == publisherId) &&
(identical(other.pollId, pollId) || other.pollId == pollId) && (identical(other.pollId, pollId) || other.pollId == pollId) &&
@ -836,6 +874,8 @@ class _$SnPostImpl extends _SnPost {
publishedUntil, publishedUntil,
totalUpvote, totalUpvote,
totalDownvote, totalDownvote,
totalViews,
totalAggregatedViews,
publisherId, publisherId,
pollId, pollId,
publisher, publisher,
@ -888,6 +928,8 @@ abstract class _SnPost extends SnPost {
required final DateTime? publishedUntil, required final DateTime? publishedUntil,
required final int totalUpvote, required final int totalUpvote,
required final int totalDownvote, required final int totalDownvote,
final int totalViews,
final int totalAggregatedViews,
required final int publisherId, required final int publisherId,
required final int? pollId, required final int? pollId,
required final SnPublisher publisher, required final SnPublisher publisher,
@ -952,6 +994,10 @@ abstract class _SnPost extends SnPost {
@override @override
int get totalDownvote; int get totalDownvote;
@override @override
int get totalViews;
@override
int get totalAggregatedViews;
@override
int get publisherId; int get publisherId;
@override @override
int? get pollId; int? get pollId;

View File

@ -62,6 +62,9 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
: DateTime.parse(json['published_until'] as String), : DateTime.parse(json['published_until'] as String),
totalUpvote: (json['total_upvote'] as num).toInt(), totalUpvote: (json['total_upvote'] as num).toInt(),
totalDownvote: (json['total_downvote'] as num).toInt(), totalDownvote: (json['total_downvote'] as num).toInt(),
totalViews: (json['total_views'] as num?)?.toInt() ?? 0,
totalAggregatedViews:
(json['total_aggregated_views'] as num?)?.toInt() ?? 0,
publisherId: (json['publisher_id'] as num).toInt(), publisherId: (json['publisher_id'] as num).toInt(),
pollId: (json['poll_id'] as num?)?.toInt(), pollId: (json['poll_id'] as num?)?.toInt(),
publisher: publisher:
@ -101,6 +104,8 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
'published_until': instance.publishedUntil?.toIso8601String(), 'published_until': instance.publishedUntil?.toIso8601String(),
'total_upvote': instance.totalUpvote, 'total_upvote': instance.totalUpvote,
'total_downvote': instance.totalDownvote, 'total_downvote': instance.totalDownvote,
'total_views': instance.totalViews,
'total_aggregated_views': instance.totalAggregatedViews,
'publisher_id': instance.publisherId, 'publisher_id': instance.publisherId,
'poll_id': instance.pollId, 'poll_id': instance.pollId,
'publisher': instance.publisher.toJson(), 'publisher': instance.publisher.toJson(),

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

@ -45,7 +45,12 @@ class ChatMessageInputState extends State<ChatMessageInput> {
final HotKey _pasteHotKey = HotKey( final HotKey _pasteHotKey = HotKey(
key: PhysicalKeyboardKey.keyV, key: PhysicalKeyboardKey.keyV,
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control], modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
scope: HotKeyScope.inapp,
);
final HotKey _newLineHotKey = HotKey(
key: PhysicalKeyboardKey.enter,
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
scope: HotKeyScope.inapp, scope: HotKeyScope.inapp,
); );
@ -61,6 +66,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
)); ));
setState(() {}); setState(() {});
}); });
hotKeyManager.register(_newLineHotKey, keyDownHandler: (_) async {
if (_contentController.text.isEmpty) return;
_contentController.text += '\n';
});
} }
@override @override
@ -112,6 +121,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
} }
Future<void> _sendMessage() async { Future<void> _sendMessage() async {
if (_contentController.text.isEmpty && _attachments.isEmpty) return;
if (_isBusy) return; if (_isBusy) return;
final attach = context.read<SnAttachmentProvider>(); final attach = context.read<SnAttachmentProvider>();
@ -204,7 +214,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
_contentController.dispose(); _contentController.dispose();
_focusNode.dispose(); _focusNode.dispose();
_dismissEmojiPicker(); _dismissEmojiPicker();
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey); if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
hotKeyManager.unregister(_pasteHotKey);
hotKeyManager.unregister(_newLineHotKey);
}
super.dispose(); super.dispose();
} }
@ -344,6 +357,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
_sendMessage(); _sendMessage();
_focusNode.requestFocus(); _focusNode.requestFocus();
}, },
maxLines: null,
), ),
), ),
const Gap(8), const Gap(8),

View File

@ -166,10 +166,12 @@ class AppRootScaffold extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start, mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [ children: [
Text( Expanded(
child: Text(
'Solar Network', 'Solar Network',
style: GoogleFonts.spaceGrotesk(), style: GoogleFonts.spaceGrotesk(),
).padding(horizontal: 12, vertical: 5), ).padding(horizontal: 12, vertical: 5),
),
if (!Platform.isMacOS) if (!Platform.isMacOS)
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@ -684,6 +684,15 @@ class _PostBottomAction extends StatelessWidget {
); );
}, },
), ),
InkWell(
child: Row(
children: [
Icon(Symbols.play_circle, size: 20, color: iconColor),
const Gap(8),
Text('postViews').plural(data.totalViews),
],
),
),
], ],
), ),
InkWell( InkWell(
@ -829,7 +838,6 @@ class _PostContentHeader extends StatelessWidget {
await sn.client.delete('/cgi/co/posts/${data.id}', queryParameters: { await sn.client.delete('/cgi/co/posts/${data.id}', queryParameters: {
'publisherId': data.publisherId, 'publisherId': data.publisherId,
}); });
if (!context.mounted) return; if (!context.mounted) return;
context.showSnackbar('postDeleted'.tr(args: ['#${data.id}'])); context.showSnackbar('postDeleted'.tr(args: ['#${data.id}']));
} catch (err) { } catch (err) {
@ -838,6 +846,25 @@ class _PostContentHeader extends StatelessWidget {
} }
} }
Future<void> _flagPost(BuildContext context) async {
final confirm = await context.showConfirmDialog(
'flagPost'.tr(),
'flagPostDescription'.tr(),
);
if (!confirm) return;
if (!context.mounted) return;
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/co/posts/${data.id}/flag');
if (!context.mounted) return;
context.showSnackbar('postFlagged'.tr());
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
@ -965,7 +992,7 @@ class _PostContentHeader extends StatelessWidget {
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postEditor', 'postEditor',
pathParameters: {'mode': data.typePlural}, pathParameters: {'mode': 'stories'},
queryParameters: {'replying': data.id.toString()}, queryParameters: {'replying': data.id.toString()},
); );
}, },
@ -981,7 +1008,7 @@ class _PostContentHeader extends StatelessWidget {
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postEditor', 'postEditor',
pathParameters: {'mode': data.typePlural}, pathParameters: {'mode': 'stories'},
queryParameters: {'reposting': data.id.toString()}, queryParameters: {'reposting': data.id.toString()},
); );
}, },
@ -1029,6 +1056,18 @@ class _PostContentHeader extends StatelessWidget {
children: [ children: [
const Icon(Symbols.flag), const Icon(Symbols.flag),
const Gap(16), const Gap(16),
Text('flagPostAction').tr(),
],
),
onTap: () {
_flagPost(context);
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.report),
const Gap(16),
Text('report').tr(), Text('report').tr(),
], ],
), ),

View File

@ -31,19 +31,27 @@ class _PostPollState extends State<PostPoll> {
String? _answeredChoice; String? _answeredChoice;
Future<void> _refreshPoll() async {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/polls/${widget.poll.id}');
if (!mounted) return;
setState(() => _poll = SnPoll.fromJson(resp.data!));
}
Future<void> _fetchAnswer() async { Future<void> _fetchAnswer() async {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) return; if (!ua.isAuthorized) return;
try { try {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/polls/${widget.poll.id}/answer'); final resp =
await sn.client.get('/cgi/co/polls/${widget.poll.id}/answer');
_answeredChoice = resp.data?['answer']; _answeredChoice = resp.data?['answer'];
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {});
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); // ignore because it may not found
} finally { } finally {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
@ -59,8 +67,9 @@ class _PostPollState extends State<PostPoll> {
'answer': option.id, 'answer': option.id,
}); });
if (!mounted) return; if (!mounted) return;
context.showSnackbar('pollAnswered'.tr());
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
_answeredChoice = option.id;
_refreshPoll();
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -78,15 +87,24 @@ class _PostPollState extends State<PostPoll> {
for (final option in _poll.options) for (final option in _poll.options)
Stack( Stack(
children: [ children: [
Container( ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
height: 60, height: 60,
width: MediaQuery.of(context).size.width * (_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble(), width: MediaQuery.of(context).size.width *
(_poll.metric.byOptionsPercentage[option.id] ?? 0)
.toDouble(),
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
), ),
),
ListTile( ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
minTileHeight: 60, minTileHeight: 60,
leading: _answeredChoice == option.id ? const Icon(Symbols.circle, fill: 1) : const Icon(Symbols.circle), leading: _answeredChoice == option.id
? const Icon(Symbols.circle, fill: 1)
: const Icon(Symbols.circle),
title: Text(option.name), title: Text(option.name),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -95,14 +113,18 @@ class _PostPollState extends State<PostPoll> {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text('pollVotes'.plural(_poll.metric.byOptions[option.id] ?? 0)), Text(
'pollVotes'
.plural(_poll.metric.byOptions[option.id] ?? 0),
),
Text(' · ').padding(horizontal: 4), Text(' · ').padding(horizontal: 4),
Text( Text(
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%', '${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%',
), ),
], ],
), ),
if (option.description.isNotEmpty) Text(option.description), if (option.description.isNotEmpty)
Text(option.description),
], ],
), ),
onTap: _isBusy ? null : () => _voteForOption(option), onTap: _isBusy ? null : () => _voteForOption(option),

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

@ -1,6 +1,5 @@
#include "my_application.h"
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h> #include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
#include "my_application.h"
#include <flutter_linux/flutter_linux.h> #include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11 #ifdef GDK_WINDOWING_X11
@ -42,15 +41,16 @@ static void my_application_activate(GApplication* application) {
if (use_header_bar) { if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar)); gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "Surface"); gtk_header_bar_set_title(header_bar, "bitsdojo_window_example");
gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else { } else {
gtk_window_set_title(window, "Surface"); gtk_window_set_title(window, "bitsdojo_window_example");
} }
auto bdw = bitsdojo_window_from(window); auto bdw = bitsdojo_window_from(window);
bdw->setCustomFrame(true); bdw->setCustomFrame(true);
//gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window)); gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new(); g_autoptr(FlDartProject) project = fl_dart_project_new();
@ -84,24 +84,6 @@ static gboolean my_application_local_command_line(GApplication* application, gch
return TRUE; return TRUE;
} }
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose. // Implements GObject::dispose.
static void my_application_dispose(GObject* object) { static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object); MyApplication* self = MY_APPLICATION(object);
@ -112,8 +94,6 @@ static void my_application_dispose(GObject* object) {
static void my_application_class_init(MyApplicationClass* klass) { static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose; G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
} }

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"
@ -354,10 +354,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dart_webrtc name: dart_webrtc
sha256: "3b3ff59c66cbc1577ed0f28d7005b5163555208fb1697a42207424ab8baa27c5" sha256: "03df5b41b23bc185ebcf4b0ffc92d002e295bf56287fb5f9d2c321ddaf7760cc"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "1.5.1"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:
@ -498,10 +498,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "3d57312a53746ed4eb8c843dc50372454bbda37dd0c01a4d40fedc83e2ce4921" sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.3.5" version: "8.3.7"
file_saver: file_saver:
dependency: "direct main" dependency: "direct main"
description: description:
@ -538,10 +538,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_windows name: file_selector_windows
sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3+3" version: "0.9.3+4"
firebase_analytics: firebase_analytics:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1043,7 +1043,7 @@ packages:
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
image_picker_android: image_picker_android:
dependency: transitive dependency: "direct main"
description: description:
name: image_picker_android name: image_picker_android
sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c
@ -1083,7 +1083,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"

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2.3.2+67 version: 2.3.2+70
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@ -120,6 +120,9 @@ dependencies:
xml: ^6.5.0 xml: ^6.5.0
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
cached_network_image_platform_interface: ^4.1.1
image_picker_platform_interface: ^2.10.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: