Compare commits
46 Commits
Author | SHA1 | Date | |
---|---|---|---|
e2ecb573a2 | |||
8cb5dff498 | |||
a5629975ed | |||
972b304969 | |||
e8ded55055 | |||
04875eb164 | |||
54a59aa470 | |||
365f330629 | |||
a7829d15b2 | |||
a3868a4281 | |||
1d1d61d60c | |||
03c2491587 | |||
2c1adc988c | |||
c0fbee55e4 | |||
6e544c0b6c | |||
7d56c5ef31 | |||
c2df1af16d | |||
a8143c6453 | |||
04065061e0 | |||
226eb452e5 | |||
a6715b0872 | |||
43e3404dbb | |||
c91cf7c813 | |||
9cd1cad695 | |||
dde280833b | |||
42ac12b53e | |||
63567bf708 | |||
5d3cadefef | |||
251fbb2503 | |||
0b31d32217 | |||
5ddd4fed2e | |||
48b6d5f6c1 | |||
b83b0b5efb | |||
cb24bd953d | |||
4937dee182 | |||
d612097bb1 | |||
058d668b6b | |||
8b19462c3a | |||
0a381ef09b | |||
9b84e912b2 | |||
b3254e0f2f | |||
f0a3bbe023 | |||
df81c84438 | |||
8b12395fca | |||
cb2b71d194 | |||
7ed508e2bb |
25
.github/workflows/nightly.yml
vendored
25
.github/workflows/nightly.yml
vendored
@ -38,4 +38,27 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
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
27
README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Solar Network
|
||||
|
||||

|
||||
|
||||
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.
|
@ -17,7 +17,6 @@
|
||||
android:label="Solian"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
|
@ -54,7 +54,7 @@ class CheckInWidget : GlanceAppWidget() {
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
|
||||
.registerTypeAdapter(Instant::class.java, InstantAdapter())
|
||||
.create()
|
||||
val resultTierSymbols = listOf("大凶", "凶", "中平", "吉", "大吉")
|
||||
val resultTierSymbols = listOf("Bad", "Poor", "Medium", "Good", "Great")
|
||||
|
||||
val prefs = currentState.preferences
|
||||
val checkInRaw: String? = prefs.getString("pas_check_in_record", null)
|
||||
@ -120,7 +120,7 @@ class CheckInWidget : GlanceAppWidget() {
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "You haven't checked in today",
|
||||
text = "You haven't divined today",
|
||||
style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface)
|
||||
)
|
||||
}
|
||||
|
@ -12,9 +12,9 @@ post {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"alias": "Meltdown",
|
||||
"name": "Meltdown",
|
||||
"attachment_id": "IpDPHEbWDDCbBofX",
|
||||
"pack_id": 4
|
||||
"alias": "BaLoading",
|
||||
"name": "BaLoading",
|
||||
"attachment_id": "2JCI2uh21mKkfk9P",
|
||||
"pack_id": 3
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ meta {
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/id/dev/notify/1
|
||||
url: {{endpoint}}/cgi/id/dev/notify/122
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
@ -15,12 +15,9 @@ body:json {
|
||||
"client_id": "{{third_client_id}}",
|
||||
"client_secret":"{{third_client_tk}}",
|
||||
"type": "general",
|
||||
"subject": "测试",
|
||||
"subtitle": "Alphabot です",
|
||||
"content": "全新通知动画",
|
||||
"metadata": {
|
||||
"image": "D2EDbcrsTugs3xk5"
|
||||
},
|
||||
"subject": "处理该帐号 @solian 的决定",
|
||||
"subtitle": "违反用户协议",
|
||||
"content": "您的帐号违反了我们用户协议中关于冒充我们官方的行为,至此做出停权的决定。还请见谅。该决定是最终决定,不接受上诉。",
|
||||
"priority": 10
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ body:json {
|
||||
"client_id": "alphabot",
|
||||
"client_secret": "_uR0sVnHTh",
|
||||
"remark": "新年红包",
|
||||
"amount": 9705,
|
||||
"payee_id": 2
|
||||
"amount": 150,
|
||||
"payee_id": 18
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@
|
||||
"screenChatNew": "New Channel",
|
||||
"screenRealm": "Realm",
|
||||
"screenRealmManage": "Edit Realm",
|
||||
"screenRealmDiscovery": "Realm Discovery",
|
||||
"screenRealmNew": "New Realm",
|
||||
"screenNotification": "Notification",
|
||||
"screenPostSearch": "Search Posts",
|
||||
@ -155,6 +156,7 @@
|
||||
"writePostTypeStory": "Post a story",
|
||||
"writePostTypeArticle": "Write an article",
|
||||
"writePostTypeQuestion": "Ask a question",
|
||||
"writePostTypeVideo": "Post a video",
|
||||
"fieldPostPublisher": "Post publisher",
|
||||
"fieldPostContent": "What happened?!",
|
||||
"fieldPostTitle": "Title",
|
||||
@ -331,6 +333,7 @@
|
||||
"addAttachmentFromRandomId": "Link via RID",
|
||||
"attachmentDetailInfo": "Attachment details",
|
||||
"attachmentPastedImage": "Pasted Image",
|
||||
"attachmentInsertedImage": "Inserted Image",
|
||||
"attachmentInsertLink": "Insert Link",
|
||||
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
|
||||
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
||||
@ -417,7 +420,7 @@
|
||||
"callMessageEnded": "Call lasted {}",
|
||||
"callMessageStarted": "Call started",
|
||||
"dailyCheckIn": "Check In",
|
||||
"dailyCheckInNone": "You haven't checked in today",
|
||||
"dailyCheckInNone": "You haven't divined today",
|
||||
"dailyCheckAction": "Check in right now!",
|
||||
"dailyCheckDetail": "Can't understand the symbol? Master, help me understand it!",
|
||||
"dailyCheckDetailTitle": "{}'s fortune details",
|
||||
@ -617,5 +620,50 @@
|
||||
"postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
|
||||
"postQuestionAnswered": "Answered Question",
|
||||
"postQuestionAnswerSelect": "Select as Answer",
|
||||
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied."
|
||||
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
|
||||
"postVideoUpload": "Upload Video",
|
||||
"realmJoin": "Join Realm",
|
||||
"realmCommunityHint": "This realm is a community realm, you can freely join.",
|
||||
"realmCommunityPublicChannelsHint": "The public channels in this realm",
|
||||
"realmJoined": "Joined realm {}.",
|
||||
"join": "Join",
|
||||
"pollEditorNew": "New Poll",
|
||||
"pollEditorEdit": "Edit Poll",
|
||||
"pollEditorDelete": "Delete Poll",
|
||||
"pollEditorDeleteDescription": "Are you sure you want to delete this poll? This operation is irreversible.",
|
||||
"pollEditorUnlink": "Unlink Poll",
|
||||
"pollOptionAdd": "Add Option",
|
||||
"pollOptionName": "Option Name",
|
||||
"pollLinkExisting": "Link existing poll",
|
||||
"pollAnswered": "Answered the poll.",
|
||||
"pollVotes": {
|
||||
"one": "{} vote",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
"screenChatNew": "新建聊天频道",
|
||||
"screenRealm": "领域",
|
||||
"screenRealmManage": "编辑领域",
|
||||
"screenRealmDiscovery": "发现领域",
|
||||
"screenRealmNew": "新建领域",
|
||||
"screenNotification": "通知",
|
||||
"screenPostSearch": "搜索帖子",
|
||||
@ -139,6 +140,7 @@
|
||||
"writePostTypeStory": "发动态",
|
||||
"writePostTypeArticle": "写文章",
|
||||
"writePostTypeQuestion": "提问题",
|
||||
"writePostTypeVideo": "发视频",
|
||||
"fieldPostPublisher": "帖子发布者",
|
||||
"fieldPostContent": "发生什么事了?!",
|
||||
"fieldPostTitle": "标题",
|
||||
@ -329,6 +331,7 @@
|
||||
"addAttachmentFromRandomId": "通过访问 ID 链接",
|
||||
"attachmentDetailInfo": "附件详细信息",
|
||||
"attachmentPastedImage": "粘贴的图片",
|
||||
"attachmentInsertedImage": "插入的图片",
|
||||
"attachmentInsertLink": "插入连接",
|
||||
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
||||
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
||||
@ -616,5 +619,50 @@
|
||||
"postQuestionAnswered": "已解答的问题",
|
||||
"postQuestionAnswerTitle": "精选解答",
|
||||
"postQuestionAnswerSelect": "选择解答",
|
||||
"postQuestionAnswerSelected": "解答已选择,奖励已发放。"
|
||||
"postQuestionAnswerSelected": "解答已选择,奖励已发放。",
|
||||
"postVideoUpload": "上传视频",
|
||||
"realmJoin": "加入领域",
|
||||
"realmCommunityHint": "该领域是一个社区领域,你可以自由加入。",
|
||||
"realmCommunityPublicChannelsHint": "该领域包含的公共频道",
|
||||
"realmJoined": "已加入领域 {}。",
|
||||
"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": "{} 次浏览"
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
"screenChatNew": "新建聊天頻道",
|
||||
"screenRealm": "領域",
|
||||
"screenRealmManage": "編輯領域",
|
||||
"screenRealmDiscovery": "發現領域",
|
||||
"screenRealmNew": "新建領域",
|
||||
"screenNotification": "通知",
|
||||
"screenPostSearch": "搜索帖子",
|
||||
@ -139,6 +140,7 @@
|
||||
"writePostTypeStory": "發動態",
|
||||
"writePostTypeArticle": "寫文章",
|
||||
"writePostTypeQuestion": "提問題",
|
||||
"writePostTypeVideo": "發視頻",
|
||||
"fieldPostPublisher": "帖子發佈者",
|
||||
"fieldPostContent": "發生什麼事了?!",
|
||||
"fieldPostTitle": "標題",
|
||||
@ -329,6 +331,7 @@
|
||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||
"attachmentDetailInfo": "附件詳細信息",
|
||||
"attachmentPastedImage": "粘貼的圖片",
|
||||
"attachmentInsertedImage": "插入的圖片",
|
||||
"attachmentInsertLink": "插入連接",
|
||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||
@ -616,5 +619,50 @@
|
||||
"postQuestionAnswered": "已解答的問題",
|
||||
"postQuestionAnswerTitle": "精選解答",
|
||||
"postQuestionAnswerSelect": "選擇解答",
|
||||
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。"
|
||||
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
|
||||
"postVideoUpload": "上傳視頻",
|
||||
"realmJoin": "加入領域",
|
||||
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
|
||||
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
|
||||
"realmJoined": "已加入領域 {}。",
|
||||
"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": "{} 次瀏覽"
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
"screenChatNew": "新建聊天頻道",
|
||||
"screenRealm": "領域",
|
||||
"screenRealmManage": "編輯領域",
|
||||
"screenRealmDiscovery": "發現領域",
|
||||
"screenRealmNew": "新建領域",
|
||||
"screenNotification": "通知",
|
||||
"screenPostSearch": "搜索帖子",
|
||||
@ -139,6 +140,7 @@
|
||||
"writePostTypeStory": "發動態",
|
||||
"writePostTypeArticle": "寫文章",
|
||||
"writePostTypeQuestion": "提問題",
|
||||
"writePostTypeVideo": "發視頻",
|
||||
"fieldPostPublisher": "帖子發佈者",
|
||||
"fieldPostContent": "發生什麼事了?!",
|
||||
"fieldPostTitle": "標題",
|
||||
@ -329,6 +331,7 @@
|
||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||
"attachmentDetailInfo": "附件詳細信息",
|
||||
"attachmentPastedImage": "粘貼的圖片",
|
||||
"attachmentInsertedImage": "插入的圖片",
|
||||
"attachmentInsertLink": "插入連接",
|
||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||
@ -616,5 +619,50 @@
|
||||
"postQuestionAnswered": "已解答的問題",
|
||||
"postQuestionAnswerTitle": "精選解答",
|
||||
"postQuestionAnswerSelect": "選擇解答",
|
||||
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。"
|
||||
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
|
||||
"postVideoUpload": "上傳視頻",
|
||||
"realmJoin": "加入領域",
|
||||
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
|
||||
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
|
||||
"realmJoined": "已加入領域 {}。",
|
||||
"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": "{} 次瀏覽"
|
||||
}
|
||||
}
|
||||
|
14
debian/debian.yml
vendored
Normal file
14
debian/debian.yml
vendored
Normal 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
9
debian/gui/surface.desktop
vendored
Normal 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
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 |
@ -2,7 +2,6 @@ PODS:
|
||||
- Alamofire (5.10.2)
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- croppy (0.0.1):
|
||||
- Flutter
|
||||
- device_info_plus (0.0.1):
|
||||
@ -180,7 +179,7 @@ PODS:
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- Kingfisher (8.2.0)
|
||||
- livekit_client (2.3.5):
|
||||
- livekit_client (2.3.6):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@ -237,7 +236,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- Alamofire
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
@ -300,7 +299,7 @@ SPEC REPOS:
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
croppy:
|
||||
:path: ".symlinks/plugins/croppy/ios"
|
||||
device_info_plus:
|
||||
@ -374,7 +373,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
@ -404,7 +403,7 @@ SPEC CHECKSUMS:
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
|
||||
livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
|
||||
livekit_client: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
|
@ -123,48 +123,59 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
contentHandler?(content)
|
||||
}
|
||||
}
|
||||
|
||||
private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: String, fileType type: UTType?, doScaleDown scaleDown: Bool = false) {
|
||||
let attachmentUrl = getAttachmentUrl(for: identifier)
|
||||
|
||||
guard let remoteUrl = URL(string: attachmentUrl) else {
|
||||
print("Invalid URL for attachment: \(attachmentUrl)")
|
||||
private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: Array<String>, fileType type: UTType?, doScaleDown scaleDown: Bool = false) {
|
||||
let attachmentUrls = identifier.compactMap { element in
|
||||
return getAttachmentUrl(for: element)
|
||||
}
|
||||
|
||||
guard !attachmentUrls.isEmpty else {
|
||||
print("Invalid URLs for attachments: \(attachmentUrls)")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let targetSize = 800
|
||||
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
||||
|
||||
KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [
|
||||
.processor(scaleProcessor)
|
||||
] : nil) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .success(let retrievalResult):
|
||||
// The image is either retrieved from cache or downloaded
|
||||
let tempDirectory = FileManager.default.temporaryDirectory
|
||||
let cachedFileUrl = tempDirectory.appendingPathComponent(identifier)
|
||||
|
||||
do {
|
||||
// Write the image data to a temporary file for UNNotificationAttachment
|
||||
try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
|
||||
self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: identifier)
|
||||
} catch {
|
||||
print("Failed to write media to temporary file: \(error.localizedDescription)")
|
||||
|
||||
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 ? [
|
||||
.processor(scaleProcessor)
|
||||
] : nil) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .success(let retrievalResult):
|
||||
// The image is either retrieved from cache or downloaded
|
||||
let tempDirectory = FileManager.default.temporaryDirectory
|
||||
let cachedFileUrl = tempDirectory.appendingPathComponent(UUID().uuidString) // Unique identifier for each file
|
||||
|
||||
do {
|
||||
// Write the image data to a temporary file for UNNotificationAttachment
|
||||
try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
|
||||
self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: attachmentUrl)
|
||||
} catch {
|
||||
print("Failed to write media to temporary file: \(error.localizedDescription)")
|
||||
self.contentHandler?(content)
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
print("Failed to retrieve image: \(error.localizedDescription)")
|
||||
self.contentHandler?(content)
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
print("Failed to retrieve image: \(error.localizedDescription)")
|
||||
self.contentHandler?(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,14 +15,14 @@ struct CheckInProvider: TimelineProvider {
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) {
|
||||
let prefs = UserDefaults(suiteName: "group.solsynth.solian")
|
||||
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
|
||||
|
||||
|
||||
let jsonDecoder = JSONDecoder()
|
||||
jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)
|
||||
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
|
||||
let checkInRaw = prefs?.string(forKey: "pas_check_in_record")
|
||||
var checkIn: SolarCheckInRecord?
|
||||
if let checkInRaw = checkInRaw {
|
||||
@ -31,7 +31,7 @@ struct CheckInProvider: TimelineProvider {
|
||||
checkIn = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let entry = CheckInEntry(
|
||||
date: Date(),
|
||||
checkIn: checkIn
|
||||
@ -54,11 +54,11 @@ struct CheckInEntry: TimelineEntry {
|
||||
|
||||
struct CheckInWidgetEntryView : View {
|
||||
var entry: CheckInProvider.Entry
|
||||
|
||||
private let resultTierSymbols: [String] = ["大凶", "凶", "中平", "吉", "大吉"]
|
||||
|
||||
|
||||
private let resultTierSymbols: [String] = ["Bad", "Poor", "Medium", "Good", "Great"]
|
||||
|
||||
func checkIn() -> Void {}
|
||||
|
||||
|
||||
func seeDetail() -> Void {}
|
||||
|
||||
var body: some View {
|
||||
@ -68,9 +68,9 @@ struct CheckInWidgetEntryView : View {
|
||||
Text(resultTierSymbols[checkIn.resultTier]).font(.system(size: 27, weight: .bold))
|
||||
Text("+\(checkIn.resultExperience) EXP").font(.system(size: 15, design: .monospaced))
|
||||
}.padding(.horizontal, 4)
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(
|
||||
@ -82,7 +82,7 @@ struct CheckInWidgetEntryView : View {
|
||||
format: .dateTime.day().month()
|
||||
).font(.system(size: 13))
|
||||
}.padding(.leading, 4)
|
||||
|
||||
|
||||
Button("See Detail", systemImage: "arrow.right", action: seeDetail)
|
||||
.labelStyle(.iconOnly)
|
||||
.buttonBorderShape(.circle)
|
||||
@ -91,11 +91,11 @@ struct CheckInWidgetEntryView : View {
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
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)
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
HStack(alignment: .bottom) {
|
||||
Button("Check In", systemImage: "checkmark", action: checkIn).labelStyle(.iconOnly).buttonBorderShape(.circle).frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
@ -145,6 +146,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
'stories': 'writePostTypeStory',
|
||||
'articles': 'writePostTypeArticle',
|
||||
'questions': 'writePostTypeQuestion',
|
||||
'videos': 'writePostTypeVideo',
|
||||
};
|
||||
|
||||
static const kAttachmentProgressWeight = 0.9;
|
||||
@ -156,6 +158,14 @@ class PostWriteController extends ChangeNotifier {
|
||||
final TextEditingController aliasController = 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;
|
||||
|
||||
PostWriteController({bool doLoadFromTemporary = true}) {
|
||||
@ -197,6 +207,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
PostWriteMedia? thumbnail;
|
||||
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||
DateTime? publishedAt, publishedUntil;
|
||||
SnAttachment? videoAttachment;
|
||||
SnPoll? poll;
|
||||
|
||||
Future<void> fetchRelatedPost(
|
||||
BuildContext context, {
|
||||
@ -218,6 +230,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
contentController.text = post.body['content'] ?? '';
|
||||
aliasController.text = post.alias ?? '';
|
||||
rewardController.text = post.body['reward']?.toString() ?? '';
|
||||
videoAttachment = post.preload?.video;
|
||||
publishedAt = post.publishedAt;
|
||||
publishedUntil = post.publishedUntil;
|
||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||
@ -226,6 +239,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
||||
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||
poll = post.preload?.poll;
|
||||
|
||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||
thumbnail = PostWriteMedia(post.preload!.thumbnail);
|
||||
@ -364,6 +378,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
||||
if (poll != null) 'poll': poll!.toJson(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -393,6 +408,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
||||
poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null;
|
||||
temporaryRestored = true;
|
||||
notifyListeners();
|
||||
});
|
||||
@ -507,6 +523,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
||||
if (reward != null) 'reward': reward,
|
||||
if (videoAttachment != null) 'video': videoAttachment!.rid,
|
||||
if (poll != null) 'poll': poll!.id,
|
||||
},
|
||||
onSendProgress: (count, total) {
|
||||
progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
||||
@ -633,6 +651,16 @@ class PostWriteController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVideoAttachment(SnAttachment? value) {
|
||||
videoAttachment = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPoll(SnPoll? value) {
|
||||
poll = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
publishedAt = null;
|
||||
publishedUntil = null;
|
||||
|
@ -47,6 +47,8 @@ import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
import 'package:image_picker_android/image_picker_android.dart';
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void appBackgroundDispatcher() {
|
||||
@ -67,20 +69,6 @@ void appBackgroundDispatcher() {
|
||||
|
||||
void main() async {
|
||||
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)) {
|
||||
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)) {
|
||||
Workmanager().initialize(
|
||||
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());
|
||||
}
|
||||
|
||||
@ -160,8 +172,8 @@ class SolianApp extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
breakpoints: [
|
||||
const Breakpoint(start: 0, end: 450, name: MOBILE),
|
||||
const Breakpoint(start: 451, end: 800, name: TABLET),
|
||||
const Breakpoint(start: 0, end: 600, name: MOBILE),
|
||||
const Breakpoint(start: 601, end: 800, name: TABLET),
|
||||
const Breakpoint(start: 801, end: 1920, name: DESKTOP),
|
||||
],
|
||||
);
|
||||
|
@ -45,8 +45,8 @@ class ConfigProvider extends ChangeNotifier {
|
||||
bool newDrawerIsCollapsed = false;
|
||||
bool newDrawerIsExpanded = false;
|
||||
if (withMediaQuery) {
|
||||
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450;
|
||||
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451;
|
||||
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600;
|
||||
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 601;
|
||||
} else {
|
||||
final rpb = ResponsiveBreakpoints.of(context);
|
||||
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
|
||||
|
@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
|
||||
class SnPostContentProvider {
|
||||
@ -16,6 +17,11 @@ class SnPostContentProvider {
|
||||
_attach = context.read<SnAttachmentProvider>();
|
||||
}
|
||||
|
||||
Future<SnPoll> _fetchPoll(int id) async {
|
||||
final resp = await _sn.client.get('/cgi/co/polls/$id');
|
||||
return SnPoll.fromJson(resp.data);
|
||||
}
|
||||
|
||||
Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
|
||||
Set<String> rids = {};
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
@ -23,6 +29,9 @@ class SnPostContentProvider {
|
||||
if (out[i].body['thumbnail'] != null) {
|
||||
rids.add(out[i].body['thumbnail']);
|
||||
}
|
||||
if (out[i].body['video'] != null) {
|
||||
rids.add(out[i].body['video']);
|
||||
}
|
||||
if (out[i].repostTo != null) {
|
||||
out[i] = out[i].copyWith(
|
||||
repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
|
||||
@ -32,10 +41,17 @@ class SnPostContentProvider {
|
||||
|
||||
final attachments = await _attach.getMultiple(rids.toList());
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
SnPoll? poll;
|
||||
if (out[i].pollId != null) {
|
||||
poll = await _fetchPoll(out[i].pollId!);
|
||||
}
|
||||
|
||||
out[i] = out[i].copyWith(
|
||||
preload: SnPostPreload(
|
||||
thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
|
||||
attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
||||
video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull,
|
||||
poll: poll,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -53,6 +69,9 @@ class SnPostContentProvider {
|
||||
if (out.body['thumbnail'] != null) {
|
||||
rids.add(out.body['thumbnail']);
|
||||
}
|
||||
if (out.body['video'] != null) {
|
||||
rids.add(out.body['video']);
|
||||
}
|
||||
if (out.repostTo != null) {
|
||||
out = out.copyWith(
|
||||
repostTo: await _preloadRelatedDataSingle(out.repostTo!),
|
||||
@ -60,10 +79,18 @@ class SnPostContentProvider {
|
||||
}
|
||||
|
||||
final attachments = await _attach.getMultiple(rids.toList());
|
||||
|
||||
SnPoll? poll;
|
||||
if (out.pollId != null) {
|
||||
poll = await _fetchPoll(out.pollId!);
|
||||
}
|
||||
|
||||
out = out.copyWith(
|
||||
preload: SnPostPreload(
|
||||
thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
|
||||
attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
||||
video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull,
|
||||
poll: poll,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -14,9 +14,32 @@ class UserDirectoryProvider {
|
||||
final Map<int, SnAccount> _cache = {};
|
||||
|
||||
Future<List<SnAccount?>> listAccount(Iterable<dynamic> id) async {
|
||||
final out = await Future.wait(
|
||||
id.map((e) => getAccount(e)),
|
||||
);
|
||||
final out = List<SnAccount?>.generate(id.length, (e) => null);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -42,22 +42,22 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
_connectCompleter = null;
|
||||
}
|
||||
|
||||
_connectCompleter = Completer<void>();
|
||||
|
||||
if (!_ua.isAuthorized) return;
|
||||
if (isConnected || conn != null) {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
final atk = await _sn.getFreshAtk();
|
||||
final uri = Uri.parse(
|
||||
'${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk',
|
||||
);
|
||||
|
||||
isBusy = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_connectCompleter = Completer<void>();
|
||||
|
||||
final atk = await _sn.getFreshAtk();
|
||||
final uri = Uri.parse(
|
||||
'${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk',
|
||||
);
|
||||
|
||||
isBusy = true;
|
||||
notifyListeners();
|
||||
|
||||
conn = WebSocketChannel.connect(uri);
|
||||
await conn!.ready;
|
||||
_wsStream = conn!.stream.asBroadcastStream();
|
||||
@ -82,6 +82,7 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
isBusy = false;
|
||||
notifyListeners();
|
||||
_connectCompleter!.complete();
|
||||
_connectCompleter = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ import 'package:surface/screens/post/post_search.dart';
|
||||
import 'package:surface/screens/realm.dart';
|
||||
import 'package:surface/screens/realm/manage.dart';
|
||||
import 'package:surface/screens/realm/realm_detail.dart';
|
||||
import 'package:surface/screens/realm/realm_discovery.dart';
|
||||
import 'package:surface/screens/settings.dart';
|
||||
import 'package:surface/screens/sharing.dart';
|
||||
import 'package:surface/screens/wallet.dart';
|
||||
@ -192,11 +193,6 @@ final _appRoutes = [
|
||||
child: const RealmScreen(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:alias',
|
||||
name: 'realmDetail',
|
||||
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manage',
|
||||
name: 'realmManage',
|
||||
@ -204,6 +200,16 @@ final _appRoutes = [
|
||||
editingRealmAlias: state.uri.queryParameters['editing'],
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/discovery',
|
||||
name: 'realmDiscovery',
|
||||
builder: (context, state) => const RealmDiscoveryScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:alias',
|
||||
name: 'realmDetail',
|
||||
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
|
||||
|
@ -74,7 +74,10 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (_isBusy)
|
||||
const CircularProgressIndicator().padding(all: 24).center()
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: const CircularProgressIndicator(),
|
||||
).center()
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
|
@ -178,6 +178,10 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountPublisherEdit').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
|
@ -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
|
||||
void 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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -123,8 +123,10 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
),
|
||||
if (_isBusy)
|
||||
SliverToBoxAdapter(
|
||||
child:
|
||||
const CircularProgressIndicator().padding(all: 24).center(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: const CircularProgressIndicator(),
|
||||
).center(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -474,7 +474,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/im/channels/${widget.channel.keyPath}/members', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': 0,
|
||||
'offset': _members.length,
|
||||
});
|
||||
final out = List<SnChannelMember>.from(
|
||||
resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -17,6 +18,7 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChatManageScreen extends StatefulWidget {
|
||||
final String? editingChannelAlias;
|
||||
|
||||
const ChatManageScreen({super.key, this.editingChannelAlias});
|
||||
|
||||
@override
|
||||
@ -33,6 +35,11 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
List<SnRealm>? _realms;
|
||||
SnRealm? _belongToRealm;
|
||||
|
||||
SnChannel? _editingChannel;
|
||||
|
||||
bool _isPublic = false;
|
||||
bool _isCommunity = false;
|
||||
|
||||
Future<void> _fetchRealms() async {
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
@ -41,6 +48,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
_realms = List<SnRealm>.from(
|
||||
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
||||
);
|
||||
if (_editingChannel != null) {
|
||||
_belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
@ -48,8 +58,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
SnChannel? _editingChannel;
|
||||
|
||||
Future<void> _fetchChannel() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
@ -62,6 +70,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
_aliasController.text = _editingChannel!.alias;
|
||||
_nameController.text = _editingChannel!.name;
|
||||
_descriptionController.text = _editingChannel!.description;
|
||||
_isPublic = _editingChannel!.isPublic;
|
||||
_isCommunity = _editingChannel!.isCommunity;
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -83,6 +93,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
: uuid.v4().replaceAll('-', '').substring(0, 12),
|
||||
'name': _nameController.text,
|
||||
'description': _descriptionController.text,
|
||||
'is_public': _isPublic,
|
||||
'is_community': _isCommunity,
|
||||
};
|
||||
|
||||
try {
|
||||
@ -124,9 +136,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.editingChannelAlias != null
|
||||
? Text('screenChatManage').tr()
|
||||
: Text('screenChatNew').tr(),
|
||||
title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
@ -138,8 +148,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
leadingPadding: const EdgeInsets.only(left: 10, right: 20),
|
||||
dividerColor: Colors.transparent,
|
||||
content: Text(
|
||||
'channelEditingNotice'
|
||||
.tr(args: ['#${_editingChannel!.alias}']),
|
||||
'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@ -162,6 +171,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
items: [
|
||||
...(_realms?.map(
|
||||
(SnRealm item) => DropdownMenuItem<SnRealm>(
|
||||
enabled: _editingChannel == null || _editingChannel?.realmId == item.id,
|
||||
value: item,
|
||||
child: Row(
|
||||
children: [
|
||||
@ -179,15 +189,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.name).textStyle(Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!),
|
||||
Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!),
|
||||
Text(
|
||||
item.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).textStyle(
|
||||
Theme.of(context).textTheme.bodySmall!),
|
||||
).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -197,14 +204,14 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
) ??
|
||||
[]),
|
||||
DropdownMenuItem<SnRealm>(
|
||||
enabled: _editingChannel == null,
|
||||
value: null,
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
child: const Icon(Symbols.clear),
|
||||
),
|
||||
const Gap(12),
|
||||
@ -213,9 +220,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('fieldChatBelongToRealmUnset')
|
||||
.tr()
|
||||
.textStyle(
|
||||
Text('fieldChatBelongToRealmUnset').tr().textStyle(
|
||||
Theme.of(context).textTheme.bodyMedium!,
|
||||
),
|
||||
],
|
||||
@ -231,10 +236,10 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
height: 60,
|
||||
height: 48,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 60,
|
||||
height: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -250,8 +255,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
helperText: 'fieldChatAliasHint'.tr(),
|
||||
helperMaxLines: 2,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
@ -260,8 +264,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldChatName'.tr(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
@ -272,8 +275,24 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldChatDescription'.tr(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
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(
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||
@ -7,10 +6,8 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/post/post_detail.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -97,8 +94,6 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: ExpandableFab(
|
||||
@ -187,6 +182,27 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('writePostTypeVideo').tr(),
|
||||
const Gap(20),
|
||||
FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: 'writePostTypeVideo'.tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||
'mode': 'videos',
|
||||
}).then((value) {
|
||||
if (value == true) {
|
||||
_refreshPosts();
|
||||
}
|
||||
});
|
||||
_fabKey.currentState!.toggle();
|
||||
},
|
||||
child: const Icon(Symbols.video_call),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
@ -245,17 +261,15 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
||||
onFetchData: _fetchPosts,
|
||||
itemBuilder: (context, idx) {
|
||||
return Center(
|
||||
child: OpenablePostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
_refreshPosts();
|
||||
},
|
||||
),
|
||||
return OpenablePostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
_refreshPosts();
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
|
@ -131,6 +131,7 @@ class _HomeDashUpdateWidget extends StatelessWidget {
|
||||
return Container(
|
||||
padding: padding,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
leading: Icon(Symbols.update),
|
||||
title: Text('updateAvailable').tr(),
|
||||
@ -180,6 +181,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
||||
return Column(
|
||||
children: days.map((ele) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
|
||||
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 diff = nextOne.$2.difference(DateTime.now());
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
||||
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
|
||||
@ -270,6 +273,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -469,6 +473,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -594,6 +599,7 @@ class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -657,36 +663,59 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
|
||||
}
|
||||
}
|
||||
|
||||
int _currentPage = 0;
|
||||
final PageController _pageController = PageController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchRecommendationPosts();
|
||||
_pageController.addListener(() {
|
||||
setState(() {
|
||||
_currentPage = _pageController.page?.round() ?? 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isBusy) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: CircularProgressIndicator().center(),
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Icon(Symbols.star),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'postRecommendation',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr()
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Symbols.star),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'postRecommendation',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
Text('${_currentPage + 1}/${_posts?.length ?? 0}', style: GoogleFonts.robotoMono())
|
||||
],
|
||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
scrollBehavior: ScrollConfiguration.of(context).copyWith(dragDevices: {
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.touch,
|
||||
|
@ -3,6 +3,7 @@ import 'dart:math' as math;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
@ -59,10 +60,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
final resp = await sn.client.get('/cgi/id/notifications?take=10');
|
||||
_totalCount = resp.data['count'];
|
||||
_notifications.addAll(
|
||||
resp.data['data']
|
||||
?.map((e) => SnNotification.fromJson(e))
|
||||
.cast<SnNotification>() ??
|
||||
[],
|
||||
resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? [],
|
||||
);
|
||||
nty.updateTray();
|
||||
} catch (err) {
|
||||
@ -188,8 +186,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
_fetchNotifications();
|
||||
},
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax: _totalCount != null &&
|
||||
_notifications.length >= _totalCount!,
|
||||
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!,
|
||||
itemBuilder: (context, idx) {
|
||||
final nty = _notifications[idx];
|
||||
return Row(
|
||||
@ -221,29 +218,36 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
isAutoWarp: true,
|
||||
),
|
||||
),
|
||||
if ([
|
||||
'interactive.feedback',
|
||||
'interactive.subscription'
|
||||
].contains(nty.topic) &&
|
||||
if (['interactive.reply', 'interactive.feedback', 'interactive.subscription']
|
||||
.contains(nty.topic) &&
|
||||
nty.metadata['related_post'] != null)
|
||||
StyledWidget(Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: PostItem(
|
||||
data: SnPost.fromJson(
|
||||
nty.metadata['related_post']!,
|
||||
),
|
||||
showComments: false,
|
||||
showReactions: false,
|
||||
showMenu: false,
|
||||
),
|
||||
),
|
||||
child: PostItem(
|
||||
data: SnPost.fromJson(
|
||||
nty.metadata['related_post']!,
|
||||
),
|
||||
showComments: false,
|
||||
showReactions: 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),
|
||||
Row(
|
||||
children: [
|
||||
@ -268,10 +272,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.check),
|
||||
padding: EdgeInsets.all(0),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed:
|
||||
_isSubmitting ? null : () => _markOneAsRead(nty),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16);
|
||||
|
@ -6,7 +6,6 @@ import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
@ -17,7 +16,6 @@ import 'package:surface/widgets/navigation/app_background.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
|
||||
class PostDetailScreen extends StatefulWidget {
|
||||
final String slug;
|
||||
@ -64,7 +62,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
|
||||
|
||||
return AppBackground(
|
||||
isRoot: widget.onBack != null,
|
||||
@ -114,7 +113,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
data: _data!,
|
||||
maxWidth: 640,
|
||||
maxWidth: maxWidth,
|
||||
showComments: false,
|
||||
showFullPost: true,
|
||||
onChanged: (data) {
|
||||
@ -125,11 +124,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||
if (_data != null)
|
||||
if (_data != null && _data!.type != 'video') const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||
if (_data != null && _data!.type != 'video')
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@ -142,51 +141,30 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
).padding(horizontal: 20, vertical: 12).center(),
|
||||
),
|
||||
),
|
||||
if (_data != null && ua.isAuthorized)
|
||||
if (_data != null && ua.isAuthorized && _data!.type != 'video')
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 240,
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
margin:
|
||||
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? const BorderRadius.all(Radius.circular(8))
|
||||
: BorderRadius.zero,
|
||||
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
)
|
||||
: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PostMiniEditor(
|
||||
postReplyId: _data!.id,
|
||||
onPost: () {
|
||||
setState(() {
|
||||
_data = _data!.copyWith(
|
||||
metric: _data!.metric.copyWith(
|
||||
replyCount: _data!.metric.replyCount + 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
),
|
||||
).center(),
|
||||
child: PostCommentQuickAction(
|
||||
parentPost: _data!,
|
||||
maxWidth: maxWidth,
|
||||
onPosted: () {
|
||||
setState(() {
|
||||
_data = _data!.copyWith(
|
||||
metric: _data!.metric.copyWith(
|
||||
replyCount: _data!.metric.replyCount + 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_data != null)
|
||||
if (_data != null && _data!.type != 'video')
|
||||
PostCommentSliverList(
|
||||
key: _childListKey,
|
||||
parentPost: _data!,
|
||||
maxWidth: 640,
|
||||
maxWidth: maxWidth,
|
||||
),
|
||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
if (_data != null && _data!.type == 'video') SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -6,7 +6,9 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
@ -14,10 +16,15 @@ import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_input.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
@ -25,6 +32,8 @@ import 'package:surface/widgets/post/post_media_pending_list.dart';
|
||||
import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/widgets/post/post_poll_editor.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class PostEditorExtra {
|
||||
final String? text;
|
||||
@ -102,7 +111,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
|
||||
final HotKey _pasteHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.keyV,
|
||||
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
|
||||
@ -128,14 +137,36 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
builder: (context) => _PostPublisherPopup(
|
||||
controller: _writeController,
|
||||
publishers: _publishers,
|
||||
onUpdate: () {
|
||||
_fetchPublishers();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPollEditorDialog() async {
|
||||
final poll = await showDialog<dynamic>(
|
||||
context: context,
|
||||
builder: (context) => PollEditorDialog(
|
||||
poll: _writeController.poll,
|
||||
),
|
||||
);
|
||||
if (poll == null) return;
|
||||
if (!mounted) return;
|
||||
|
||||
if (poll == false) {
|
||||
_writeController.setPoll(null);
|
||||
} else {
|
||||
_writeController.setPoll(poll);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_writeController.dispose();
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
|
||||
hotKeyManager.unregister(_pasteHotKey);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -225,12 +256,68 @@ 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(
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 160),
|
||||
child: switch (_writeController.mode) {
|
||||
child: StyledWidget(switch (_writeController.mode) {
|
||||
'stories' => _PostStoryEditor(
|
||||
controller: _writeController,
|
||||
onTapPublisher: _showPublisherPopup,
|
||||
@ -243,8 +330,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
controller: _writeController,
|
||||
onTapPublisher: _showPublisherPopup,
|
||||
),
|
||||
'videos' => _PostVideoEditor(
|
||||
controller: _writeController,
|
||||
onTapPublisher: _showPublisherPopup,
|
||||
),
|
||||
_ => const Placeholder(),
|
||||
},
|
||||
})
|
||||
.padding(top: 8),
|
||||
),
|
||||
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
|
||||
Positioned(
|
||||
@ -292,7 +384,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isLoading),
|
||||
if (_writeController.isBusy && _writeController.progress != null)
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: _writeController.progress),
|
||||
@ -301,6 +392,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
)
|
||||
else if (_writeController.isBusy)
|
||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||
LoadingIndicator(isActive: _isLoading),
|
||||
const Gap(4),
|
||||
Container(
|
||||
child: _writeController.temporaryRestored
|
||||
? Container(
|
||||
@ -348,6 +441,18 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_writeController.mode == 'stories')
|
||||
IconButton(
|
||||
icon: Icon(Symbols.poll, color: Theme.of(context).colorScheme.primary),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: _writeController.poll == null
|
||||
? null
|
||||
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
|
||||
),
|
||||
onPressed: () {
|
||||
_showPollEditorDialog();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -370,7 +475,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
],
|
||||
).padding(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||
top: 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -392,8 +496,9 @@ class _PostEditorActionScrollBehavior extends MaterialScrollBehavior {
|
||||
class _PostPublisherPopup extends StatelessWidget {
|
||||
final PostWriteController controller;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
@ -408,6 +513,20 @@ class _PostPublisherPopup extends StatelessWidget {
|
||||
Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
],
|
||||
).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(
|
||||
child: ListView.builder(
|
||||
itemCount: publishers?.length ?? 0,
|
||||
@ -483,6 +602,7 @@ class _PostStoryEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -546,6 +666,7 @@ class _PostArticleEditor extends StatelessWidget {
|
||||
keyboardType: TextInputType.multiline,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
).padding(horizontal: 16),
|
||||
const Gap(4),
|
||||
];
|
||||
@ -573,6 +694,7 @@ class _PostArticleEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
@ -607,6 +729,7 @@ class _PostArticleEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -678,6 +801,7 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -687,3 +811,226 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostVideoEditor extends StatelessWidget {
|
||||
final PostWriteController controller;
|
||||
final Function? onTapPublisher;
|
||||
|
||||
const _PostVideoEditor({required this.controller, this.onTapPublisher});
|
||||
|
||||
void _selectVideo(BuildContext context) async {
|
||||
final video = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
title: 'postVideoUpload'.tr(),
|
||||
pool: 'interactive',
|
||||
mediaType: SnMediaType.video,
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (video == null) return;
|
||||
controller.setVideoAttachment(video);
|
||||
}
|
||||
|
||||
void _setAlt(BuildContext context) async {
|
||||
if (controller.videoAttachment == null) return;
|
||||
|
||||
final result = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentAltDialog(media: PostWriteMedia(controller.videoAttachment)),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
controller.setVideoAttachment(result);
|
||||
}
|
||||
|
||||
Future<void> _createBoost(BuildContext context) async {
|
||||
if (controller.videoAttachment == null) return;
|
||||
|
||||
final result = await showDialog<SnAttachmentBoost?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentBoostDialog(media: PostWriteMedia(controller.videoAttachment)),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
final newAttach = controller.videoAttachment!.copyWith(
|
||||
boosts: [...controller.videoAttachment!.boosts, result],
|
||||
);
|
||||
|
||||
controller.setVideoAttachment(newAttach);
|
||||
}
|
||||
|
||||
void _setThumbnail(BuildContext context) async {
|
||||
if (controller.videoAttachment == null) return;
|
||||
|
||||
final thumbnail = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
title: 'attachmentSetThumbnail'.tr(),
|
||||
pool: 'interactive',
|
||||
analyzeNow: true,
|
||||
),
|
||||
);
|
||||
if (thumbnail == null) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
try {
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
final newAttach = await attach.updateOne(
|
||||
controller.videoAttachment!,
|
||||
thumbnailId: thumbnail.id,
|
||||
);
|
||||
controller.setVideoAttachment(newAttach);
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteAttachment(BuildContext context) async {
|
||||
if (controller.videoAttachment == null) return;
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
|
||||
controller.setVideoAttachment(null);
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
AccountImage(content: controller.publisher?.avatar, radius: 20),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(controller.publisher?.nick ?? 'loading'.tr()).bold(),
|
||||
Text('@${controller.publisher?.name}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12, vertical: 8),
|
||||
onTap: () {
|
||||
onTapPublisher?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: controller.titleController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostTitle'.tr(),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: controller.descriptionController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostDescription'.tr(),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(12),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
child: ContextMenuRegion(
|
||||
contextMenu: ContextMenu(
|
||||
entries: [
|
||||
MenuItem(
|
||||
label: 'attachmentSetAlt'.tr(),
|
||||
icon: Symbols.description,
|
||||
onSelected: () {
|
||||
_setAlt(context);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'attachmentBoost'.tr(),
|
||||
icon: Symbols.bolt,
|
||||
onSelected: () {
|
||||
_createBoost(context);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'attachmentSetThumbnail'.tr(),
|
||||
icon: Symbols.image,
|
||||
onSelected: () {
|
||||
_setThumbnail(context);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'attachmentCopyRandomId'.tr(),
|
||||
icon: Symbols.content_copy,
|
||||
onSelected: () {
|
||||
Clipboard.setData(ClipboardData(text: controller.videoAttachment!.rid));
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'delete'.tr(),
|
||||
icon: Symbols.delete,
|
||||
onSelected: () => _deleteAttachment(context),
|
||||
),
|
||||
MenuItem(
|
||||
label: 'unlink'.tr(),
|
||||
icon: Symbols.link_off,
|
||||
onSelected: () {
|
||||
controller.setVideoAttachment(null);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: controller.videoAttachment == null ? () => _selectVideo(context) : null,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: controller.videoAttachment == null
|
||||
? Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.add),
|
||||
const Gap(4),
|
||||
Text('postVideoUpload'.tr()),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AttachmentItem(
|
||||
data: controller.videoAttachment!,
|
||||
heroTag: const Uuid().v4(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
@ -100,6 +100,12 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenRealm').tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.globe),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('realmDiscovery');
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: !_isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
|
||||
onPressed: () {
|
||||
@ -180,7 +186,11 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmDetail',
|
||||
pathParameters: {'alias': realm.alias},
|
||||
);
|
||||
).then((value) {
|
||||
if (value == true) {
|
||||
_fetchRealms();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -238,7 +248,11 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmDetail',
|
||||
pathParameters: {'alias': realm.alias},
|
||||
);
|
||||
).then((value) {
|
||||
if (value == true) {
|
||||
_fetchRealms();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -50,6 +50,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
_aliasController.text = out.alias;
|
||||
_nameController.text = out.name;
|
||||
_descriptionController.text = out.description;
|
||||
_isPublic = out.isPublic;
|
||||
_isCommunity = out.isCommunity;
|
||||
} catch (err) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
@ -67,6 +69,9 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
|
||||
final _imagePicker = ImagePicker();
|
||||
|
||||
bool _isPublic = false;
|
||||
bool _isCommunity = false;
|
||||
|
||||
Future<void> _updateImage(String place) async {
|
||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
if (image == null) return;
|
||||
@ -138,6 +143,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
'description': _descriptionController.text,
|
||||
'avatar': _avatar,
|
||||
'banner': _banner,
|
||||
'is_public': _isPublic,
|
||||
'is_community': _isCommunity,
|
||||
};
|
||||
|
||||
try {
|
||||
@ -293,6 +300,23 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
|
@ -189,7 +189,7 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': 0,
|
||||
'offset': _members.length,
|
||||
});
|
||||
|
||||
final out = List<SnRealmMember>.from(
|
||||
@ -343,12 +343,31 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
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;
|
||||
Navigator.pop(context, true);
|
||||
context.showSnackbar('realmDeleted'.tr(args: [
|
||||
'#${widget.realm!.alias}',
|
||||
]));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -367,22 +386,31 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
||||
children: [
|
||||
const Gap(8),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.edit),
|
||||
leading: const Icon(Symbols.logout),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('realmEdit').tr(),
|
||||
subtitle: Text('realmEditDescription').tr(),
|
||||
title: Text('realmLeave').tr(),
|
||||
subtitle: Text('realmLeaveDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmManage',
|
||||
queryParameters: {'editing': widget.realm!.alias},
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
widget.onUpdate();
|
||||
}
|
||||
});
|
||||
},
|
||||
onTap: _isBusy ? null : () => _leaveRealm(),
|
||||
),
|
||||
if (isOwned)
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.edit),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('realmEdit').tr(),
|
||||
subtitle: Text('realmEditDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmManage',
|
||||
queryParameters: {'editing': widget.realm!.alias},
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
widget.onUpdate();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
if (isOwned)
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.delete),
|
||||
|
290
lib/screens/realm/realm_discovery.dart
Normal file
290
lib/screens/realm/realm_discovery.dart
Normal file
@ -0,0 +1,290 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class RealmDiscoveryScreen extends StatefulWidget {
|
||||
const RealmDiscoveryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RealmDiscoveryScreen> createState() => _RealmDiscoveryScreenState();
|
||||
}
|
||||
|
||||
class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
|
||||
List<SnRealm>? _realms;
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _fetchRealms() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/realms');
|
||||
_realms = List<SnRealm>.from(
|
||||
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
rethrow;
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchRealms();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('screenRealmDiscovery').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchRealms,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: _realms?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final realm = _realms![idx];
|
||||
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: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _RealmJoinPopup(realm: realm),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
).center();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RealmJoinPopup extends StatefulWidget {
|
||||
final SnRealm realm;
|
||||
|
||||
const _RealmJoinPopup({required this.realm});
|
||||
|
||||
@override
|
||||
State<_RealmJoinPopup> createState() => _RealmJoinPopupState();
|
||||
}
|
||||
|
||||
class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
||||
final List<String> _planJoinChannels = List.empty(growable: true);
|
||||
|
||||
List<SnChannel>? _channels;
|
||||
bool _isBusy = false;
|
||||
bool _isJoining = false;
|
||||
|
||||
Future<void> _fetchPublicChannels() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
|
||||
final out = List<SnChannel>.from(
|
||||
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
||||
);
|
||||
setState(() => _channels = out);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _joinRealm() async {
|
||||
try {
|
||||
setState(() => _isJoining = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client.post('/cgi/id/realms/${widget.realm.alias}/members', data: {
|
||||
'related': ua.user?.name,
|
||||
});
|
||||
await _joinSelectedChannels();
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('realmJoined'.tr(args: [widget.realm.name]));
|
||||
Navigator.pop(context);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isJoining = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _joinSelectedChannels() async {
|
||||
if (_planJoinChannels.isEmpty) return;
|
||||
for (final channel in _planJoinChannels) {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client.post('/cgi/im/channels/${widget.realm.alias}/$channel/members', data: {
|
||||
'related': ua.user?.name,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchPublicChannels();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.group_add, size: 24),
|
||||
const Gap(16),
|
||||
Text('realmJoin', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.realm.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Text(
|
||||
widget.realm.description,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _isJoining ? null : () => _joinRealm(),
|
||||
child: Text('join'.tr()),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, bottom: 12),
|
||||
const Divider(height: 1),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
|
||||
.padding(horizontal: 24, vertical: 8),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _channels?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final channel = _channels![index];
|
||||
return CheckboxListTile(
|
||||
value: _planJoinChannels.contains(channel.alias),
|
||||
title: Text(channel.name),
|
||||
subtitle: Text(
|
||||
channel.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
secondary: AccountImage(
|
||||
content: null,
|
||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||
),
|
||||
onChanged: (value) {
|
||||
value ??= false;
|
||||
if (value) {
|
||||
setState(() => _planJoinChannels.add(channel.alias));
|
||||
} else {
|
||||
setState(() => _planJoinChannels.remove(channel.alias));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -57,7 +57,7 @@ Future<ThemeData> createAppTheme(
|
||||
),
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
|
||||
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
|
||||
|
@ -20,7 +20,7 @@ class SnAccount with _$SnAccount {
|
||||
required String description,
|
||||
required String name,
|
||||
required String nick,
|
||||
required Map<String, dynamic> permNodes,
|
||||
@Default({}) Map<String, dynamic> permNodes,
|
||||
required String language,
|
||||
required SnAccountProfile? profile,
|
||||
@Default([]) List<SnAccountBadge> badges,
|
||||
|
@ -385,7 +385,7 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
required this.description,
|
||||
required this.name,
|
||||
required this.nick,
|
||||
required final Map<String, dynamic> permNodes,
|
||||
final Map<String, dynamic> permNodes = const {},
|
||||
required this.language,
|
||||
required this.profile,
|
||||
final List<SnAccountBadge> badges = const [],
|
||||
@ -437,6 +437,7 @@ class _$SnAccountImpl extends _SnAccount {
|
||||
final String nick;
|
||||
final Map<String, dynamic> _permNodes;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<String, dynamic> get permNodes {
|
||||
if (_permNodes is EqualUnmodifiableMapView) return _permNodes;
|
||||
// ignore: implicit_dynamic_type
|
||||
@ -566,7 +567,7 @@ abstract class _SnAccount extends SnAccount {
|
||||
required final String description,
|
||||
required final String name,
|
||||
required final String nick,
|
||||
required final Map<String, dynamic> permNodes,
|
||||
final Map<String, dynamic> permNodes,
|
||||
required final String language,
|
||||
required final SnAccountProfile? profile,
|
||||
final List<SnAccountBadge> badges,
|
||||
|
@ -25,7 +25,7 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
|
||||
description: json['description'] as String,
|
||||
name: json['name'] 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,
|
||||
profile: json['profile'] == null
|
||||
? null
|
||||
|
@ -1,9 +1,17 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'check_in.freezed.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
|
||||
class SnCheckInRecord with _$SnCheckInRecord {
|
||||
@ -21,8 +29,7 @@ class SnCheckInRecord with _$SnCheckInRecord {
|
||||
required int accountId,
|
||||
}) = _SnCheckInRecord;
|
||||
|
||||
factory SnCheckInRecord.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnCheckInRecordFromJson(json);
|
||||
factory SnCheckInRecord.fromJson(Map<String, dynamic> json) => _$SnCheckInRecordFromJson(json);
|
||||
|
||||
String get symbol => kCheckInResultTierSymbols[resultTier];
|
||||
}
|
||||
|
45
lib/types/poll.dart
Normal file
45
lib/types/poll.dart
Normal file
@ -0,0 +1,45 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'poll.freezed.dart';
|
||||
part 'poll.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnPoll with _$SnPoll {
|
||||
const factory SnPoll({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required dynamic deletedAt,
|
||||
required dynamic expiredAt,
|
||||
required List<SnPollOption> options,
|
||||
required int accountId,
|
||||
required SnPollMetric metric,
|
||||
}) = _SnPoll;
|
||||
|
||||
factory SnPoll.fromJson(Map<String, Object?> json) => _$SnPollFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPollMetric with _$SnPollMetric {
|
||||
const factory SnPollMetric({
|
||||
required int totalAnswer,
|
||||
@Default({}) Map<String, int> byOptions,
|
||||
@Default({}) Map<String, double> byOptionsPercentage,
|
||||
}) = _SnPollMetric;
|
||||
|
||||
factory SnPollMetric.fromJson(Map<String, Object?> json) =>
|
||||
_$SnPollMetricFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPollOption with _$SnPollOption {
|
||||
const factory SnPollOption({
|
||||
required String id,
|
||||
required String icon,
|
||||
required String name,
|
||||
required String description,
|
||||
}) = _SnPollOption;
|
||||
|
||||
factory SnPollOption.fromJson(Map<String, Object?> json) =>
|
||||
_$SnPollOptionFromJson(json);
|
||||
}
|
761
lib/types/poll.freezed.dart
Normal file
761
lib/types/poll.freezed.dart
Normal file
@ -0,0 +1,761 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'poll.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
SnPoll _$SnPollFromJson(Map<String, dynamic> json) {
|
||||
return _SnPoll.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnPoll {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
dynamic get deletedAt => throw _privateConstructorUsedError;
|
||||
dynamic get expiredAt => throw _privateConstructorUsedError;
|
||||
List<SnPollOption> get options => throw _privateConstructorUsedError;
|
||||
int get accountId => throw _privateConstructorUsedError;
|
||||
SnPollMetric get metric => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPoll to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnPollCopyWith<SnPoll> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnPollCopyWith<$Res> {
|
||||
factory $SnPollCopyWith(SnPoll value, $Res Function(SnPoll) then) =
|
||||
_$SnPollCopyWithImpl<$Res, SnPoll>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
dynamic expiredAt,
|
||||
List<SnPollOption> options,
|
||||
int accountId,
|
||||
SnPollMetric metric});
|
||||
|
||||
$SnPollMetricCopyWith<$Res> get metric;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnPollCopyWithImpl<$Res, $Val extends SnPoll>
|
||||
implements $SnPollCopyWith<$Res> {
|
||||
_$SnPollCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? expiredAt = freezed,
|
||||
Object? options = null,
|
||||
Object? accountId = null,
|
||||
Object? metric = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
expiredAt: freezed == expiredAt
|
||||
? _value.expiredAt
|
||||
: expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
options: null == options
|
||||
? _value.options
|
||||
: options // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPollOption>,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
metric: null == metric
|
||||
? _value.metric
|
||||
: metric // ignore: cast_nullable_to_non_nullable
|
||||
as SnPollMetric,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPollMetricCopyWith<$Res> get metric {
|
||||
return $SnPollMetricCopyWith<$Res>(_value.metric, (value) {
|
||||
return _then(_value.copyWith(metric: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnPollImplCopyWith<$Res> implements $SnPollCopyWith<$Res> {
|
||||
factory _$$SnPollImplCopyWith(
|
||||
_$SnPollImpl value, $Res Function(_$SnPollImpl) then) =
|
||||
__$$SnPollImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
dynamic expiredAt,
|
||||
List<SnPollOption> options,
|
||||
int accountId,
|
||||
SnPollMetric metric});
|
||||
|
||||
@override
|
||||
$SnPollMetricCopyWith<$Res> get metric;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnPollImplCopyWithImpl<$Res>
|
||||
extends _$SnPollCopyWithImpl<$Res, _$SnPollImpl>
|
||||
implements _$$SnPollImplCopyWith<$Res> {
|
||||
__$$SnPollImplCopyWithImpl(
|
||||
_$SnPollImpl _value, $Res Function(_$SnPollImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? expiredAt = freezed,
|
||||
Object? options = null,
|
||||
Object? accountId = null,
|
||||
Object? metric = null,
|
||||
}) {
|
||||
return _then(_$SnPollImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
expiredAt: freezed == expiredAt
|
||||
? _value.expiredAt
|
||||
: expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
options: null == options
|
||||
? _value._options
|
||||
: options // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPollOption>,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
metric: null == metric
|
||||
? _value.metric
|
||||
: metric // ignore: cast_nullable_to_non_nullable
|
||||
as SnPollMetric,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnPollImpl implements _SnPoll {
|
||||
const _$SnPollImpl(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.expiredAt,
|
||||
required final List<SnPollOption> options,
|
||||
required this.accountId,
|
||||
required this.metric})
|
||||
: _options = options;
|
||||
|
||||
factory _$SnPollImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnPollImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final dynamic deletedAt;
|
||||
@override
|
||||
final dynamic expiredAt;
|
||||
final List<SnPollOption> _options;
|
||||
@override
|
||||
List<SnPollOption> get options {
|
||||
if (_options is EqualUnmodifiableListView) return _options;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_options);
|
||||
}
|
||||
|
||||
@override
|
||||
final int accountId;
|
||||
@override
|
||||
final SnPollMetric metric;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPoll(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, expiredAt: $expiredAt, options: $options, accountId: $accountId, metric: $metric)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnPollImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
||||
const DeepCollectionEquality().equals(other.expiredAt, expiredAt) &&
|
||||
const DeepCollectionEquality().equals(other._options, _options) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId) &&
|
||||
(identical(other.metric, metric) || other.metric == metric));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
const DeepCollectionEquality().hash(deletedAt),
|
||||
const DeepCollectionEquality().hash(expiredAt),
|
||||
const DeepCollectionEquality().hash(_options),
|
||||
accountId,
|
||||
metric);
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnPollImplCopyWith<_$SnPollImpl> get copyWith =>
|
||||
__$$SnPollImplCopyWithImpl<_$SnPollImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnPollImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnPoll implements SnPoll {
|
||||
const factory _SnPoll(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final dynamic deletedAt,
|
||||
required final dynamic expiredAt,
|
||||
required final List<SnPollOption> options,
|
||||
required final int accountId,
|
||||
required final SnPollMetric metric}) = _$SnPollImpl;
|
||||
|
||||
factory _SnPoll.fromJson(Map<String, dynamic> json) = _$SnPollImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
dynamic get deletedAt;
|
||||
@override
|
||||
dynamic get expiredAt;
|
||||
@override
|
||||
List<SnPollOption> get options;
|
||||
@override
|
||||
int get accountId;
|
||||
@override
|
||||
SnPollMetric get metric;
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnPollImplCopyWith<_$SnPollImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnPollMetric _$SnPollMetricFromJson(Map<String, dynamic> json) {
|
||||
return _SnPollMetric.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnPollMetric {
|
||||
int get totalAnswer => throw _privateConstructorUsedError;
|
||||
Map<String, int> get byOptions => throw _privateConstructorUsedError;
|
||||
Map<String, double> get byOptionsPercentage =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPollMetric to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnPollMetricCopyWith<SnPollMetric> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnPollMetricCopyWith<$Res> {
|
||||
factory $SnPollMetricCopyWith(
|
||||
SnPollMetric value, $Res Function(SnPollMetric) then) =
|
||||
_$SnPollMetricCopyWithImpl<$Res, SnPollMetric>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int totalAnswer,
|
||||
Map<String, int> byOptions,
|
||||
Map<String, double> byOptionsPercentage});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnPollMetricCopyWithImpl<$Res, $Val extends SnPollMetric>
|
||||
implements $SnPollMetricCopyWith<$Res> {
|
||||
_$SnPollMetricCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? totalAnswer = null,
|
||||
Object? byOptions = null,
|
||||
Object? byOptionsPercentage = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
totalAnswer: null == totalAnswer
|
||||
? _value.totalAnswer
|
||||
: totalAnswer // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
byOptions: null == byOptions
|
||||
? _value.byOptions
|
||||
: byOptions // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, int>,
|
||||
byOptionsPercentage: null == byOptionsPercentage
|
||||
? _value.byOptionsPercentage
|
||||
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, double>,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnPollMetricImplCopyWith<$Res>
|
||||
implements $SnPollMetricCopyWith<$Res> {
|
||||
factory _$$SnPollMetricImplCopyWith(
|
||||
_$SnPollMetricImpl value, $Res Function(_$SnPollMetricImpl) then) =
|
||||
__$$SnPollMetricImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int totalAnswer,
|
||||
Map<String, int> byOptions,
|
||||
Map<String, double> byOptionsPercentage});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnPollMetricImplCopyWithImpl<$Res>
|
||||
extends _$SnPollMetricCopyWithImpl<$Res, _$SnPollMetricImpl>
|
||||
implements _$$SnPollMetricImplCopyWith<$Res> {
|
||||
__$$SnPollMetricImplCopyWithImpl(
|
||||
_$SnPollMetricImpl _value, $Res Function(_$SnPollMetricImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? totalAnswer = null,
|
||||
Object? byOptions = null,
|
||||
Object? byOptionsPercentage = null,
|
||||
}) {
|
||||
return _then(_$SnPollMetricImpl(
|
||||
totalAnswer: null == totalAnswer
|
||||
? _value.totalAnswer
|
||||
: totalAnswer // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
byOptions: null == byOptions
|
||||
? _value._byOptions
|
||||
: byOptions // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, int>,
|
||||
byOptionsPercentage: null == byOptionsPercentage
|
||||
? _value._byOptionsPercentage
|
||||
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, double>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnPollMetricImpl implements _SnPollMetric {
|
||||
const _$SnPollMetricImpl(
|
||||
{required this.totalAnswer,
|
||||
final Map<String, int> byOptions = const {},
|
||||
final Map<String, double> byOptionsPercentage = const {}})
|
||||
: _byOptions = byOptions,
|
||||
_byOptionsPercentage = byOptionsPercentage;
|
||||
|
||||
factory _$SnPollMetricImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnPollMetricImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int totalAnswer;
|
||||
final Map<String, int> _byOptions;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<String, int> get byOptions {
|
||||
if (_byOptions is EqualUnmodifiableMapView) return _byOptions;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_byOptions);
|
||||
}
|
||||
|
||||
final Map<String, double> _byOptionsPercentage;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<String, double> get byOptionsPercentage {
|
||||
if (_byOptionsPercentage is EqualUnmodifiableMapView)
|
||||
return _byOptionsPercentage;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_byOptionsPercentage);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPollMetric(totalAnswer: $totalAnswer, byOptions: $byOptions, byOptionsPercentage: $byOptionsPercentage)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnPollMetricImpl &&
|
||||
(identical(other.totalAnswer, totalAnswer) ||
|
||||
other.totalAnswer == totalAnswer) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._byOptions, _byOptions) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._byOptionsPercentage, _byOptionsPercentage));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
totalAnswer,
|
||||
const DeepCollectionEquality().hash(_byOptions),
|
||||
const DeepCollectionEquality().hash(_byOptionsPercentage));
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnPollMetricImplCopyWith<_$SnPollMetricImpl> get copyWith =>
|
||||
__$$SnPollMetricImplCopyWithImpl<_$SnPollMetricImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnPollMetricImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnPollMetric implements SnPollMetric {
|
||||
const factory _SnPollMetric(
|
||||
{required final int totalAnswer,
|
||||
final Map<String, int> byOptions,
|
||||
final Map<String, double> byOptionsPercentage}) = _$SnPollMetricImpl;
|
||||
|
||||
factory _SnPollMetric.fromJson(Map<String, dynamic> json) =
|
||||
_$SnPollMetricImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get totalAnswer;
|
||||
@override
|
||||
Map<String, int> get byOptions;
|
||||
@override
|
||||
Map<String, double> get byOptionsPercentage;
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnPollMetricImplCopyWith<_$SnPollMetricImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnPollOption _$SnPollOptionFromJson(Map<String, dynamic> json) {
|
||||
return _SnPollOption.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnPollOption {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
String get icon => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
String get description => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPollOption to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnPollOptionCopyWith<SnPollOption> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnPollOptionCopyWith<$Res> {
|
||||
factory $SnPollOptionCopyWith(
|
||||
SnPollOption value, $Res Function(SnPollOption) then) =
|
||||
_$SnPollOptionCopyWithImpl<$Res, SnPollOption>;
|
||||
@useResult
|
||||
$Res call({String id, String icon, String name, String description});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnPollOptionCopyWithImpl<$Res, $Val extends SnPollOption>
|
||||
implements $SnPollOptionCopyWith<$Res> {
|
||||
_$SnPollOptionCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? icon = null,
|
||||
Object? name = null,
|
||||
Object? description = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
icon: null == icon
|
||||
? _value.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnPollOptionImplCopyWith<$Res>
|
||||
implements $SnPollOptionCopyWith<$Res> {
|
||||
factory _$$SnPollOptionImplCopyWith(
|
||||
_$SnPollOptionImpl value, $Res Function(_$SnPollOptionImpl) then) =
|
||||
__$$SnPollOptionImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String id, String icon, String name, String description});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnPollOptionImplCopyWithImpl<$Res>
|
||||
extends _$SnPollOptionCopyWithImpl<$Res, _$SnPollOptionImpl>
|
||||
implements _$$SnPollOptionImplCopyWith<$Res> {
|
||||
__$$SnPollOptionImplCopyWithImpl(
|
||||
_$SnPollOptionImpl _value, $Res Function(_$SnPollOptionImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? icon = null,
|
||||
Object? name = null,
|
||||
Object? description = null,
|
||||
}) {
|
||||
return _then(_$SnPollOptionImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
icon: null == icon
|
||||
? _value.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnPollOptionImpl implements _SnPollOption {
|
||||
const _$SnPollOptionImpl(
|
||||
{required this.id,
|
||||
required this.icon,
|
||||
required this.name,
|
||||
required this.description});
|
||||
|
||||
factory _$SnPollOptionImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnPollOptionImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final String icon;
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final String description;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPollOption(id: $id, icon: $icon, name: $name, description: $description)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnPollOptionImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.icon, icon) || other.icon == icon) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.description, description) ||
|
||||
other.description == description));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, icon, name, description);
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnPollOptionImplCopyWith<_$SnPollOptionImpl> get copyWith =>
|
||||
__$$SnPollOptionImplCopyWithImpl<_$SnPollOptionImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnPollOptionImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnPollOption implements SnPollOption {
|
||||
const factory _SnPollOption(
|
||||
{required final String id,
|
||||
required final String icon,
|
||||
required final String name,
|
||||
required final String description}) = _$SnPollOptionImpl;
|
||||
|
||||
factory _SnPollOption.fromJson(Map<String, dynamic> json) =
|
||||
_$SnPollOptionImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get id;
|
||||
@override
|
||||
String get icon;
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
String get description;
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnPollOptionImplCopyWith<_$SnPollOptionImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
69
lib/types/poll.g.dart
Normal file
69
lib/types/poll.g.dart
Normal file
@ -0,0 +1,69 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'poll.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnPollImpl _$$SnPollImplFromJson(Map<String, dynamic> json) => _$SnPollImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'],
|
||||
expiredAt: json['expired_at'],
|
||||
options: (json['options'] as List<dynamic>)
|
||||
.map((e) => SnPollOption.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
metric: SnPollMetric.fromJson(json['metric'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPollImplToJson(_$SnPollImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt,
|
||||
'expired_at': instance.expiredAt,
|
||||
'options': instance.options.map((e) => e.toJson()).toList(),
|
||||
'account_id': instance.accountId,
|
||||
'metric': instance.metric.toJson(),
|
||||
};
|
||||
|
||||
_$SnPollMetricImpl _$$SnPollMetricImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPollMetricImpl(
|
||||
totalAnswer: (json['total_answer'] as num).toInt(),
|
||||
byOptions: (json['by_options'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toInt()),
|
||||
) ??
|
||||
const {},
|
||||
byOptionsPercentage:
|
||||
(json['by_options_percentage'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toDouble()),
|
||||
) ??
|
||||
const {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPollMetricImplToJson(_$SnPollMetricImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'total_answer': instance.totalAnswer,
|
||||
'by_options': instance.byOptions,
|
||||
'by_options_percentage': instance.byOptionsPercentage,
|
||||
};
|
||||
|
||||
_$SnPollOptionImpl _$$SnPollOptionImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPollOptionImpl(
|
||||
id: json['id'] as String,
|
||||
icon: json['icon'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPollOptionImplToJson(_$SnPollOptionImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'icon': instance.icon,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
|
||||
part 'post.freezed.dart';
|
||||
part 'post.g.dart';
|
||||
@ -36,7 +37,10 @@ class SnPost with _$SnPost {
|
||||
required DateTime? publishedUntil,
|
||||
required int totalUpvote,
|
||||
required int totalDownvote,
|
||||
@Default(0) int totalViews,
|
||||
@Default(0) int totalAggregatedViews,
|
||||
required int publisherId,
|
||||
required int? pollId,
|
||||
required SnPublisher publisher,
|
||||
required SnMetric metric,
|
||||
SnPostPreload? preload,
|
||||
@ -89,6 +93,8 @@ class SnPostPreload with _$SnPostPreload {
|
||||
const factory SnPostPreload({
|
||||
required SnAttachment? thumbnail,
|
||||
required List<SnAttachment?>? attachments,
|
||||
required SnAttachment? video,
|
||||
required SnPoll? poll,
|
||||
}) = _SnPostPreload;
|
||||
|
||||
factory SnPostPreload.fromJson(Map<String, Object?> json) =>
|
||||
|
@ -47,7 +47,10 @@ mixin _$SnPost {
|
||||
DateTime? get publishedUntil => throw _privateConstructorUsedError;
|
||||
int get totalUpvote => throw _privateConstructorUsedError;
|
||||
int get totalDownvote => throw _privateConstructorUsedError;
|
||||
int get totalViews => throw _privateConstructorUsedError;
|
||||
int get totalAggregatedViews => throw _privateConstructorUsedError;
|
||||
int get publisherId => throw _privateConstructorUsedError;
|
||||
int? get pollId => throw _privateConstructorUsedError;
|
||||
SnPublisher get publisher => throw _privateConstructorUsedError;
|
||||
SnMetric get metric => throw _privateConstructorUsedError;
|
||||
SnPostPreload? get preload => throw _privateConstructorUsedError;
|
||||
@ -94,7 +97,10 @@ abstract class $SnPostCopyWith<$Res> {
|
||||
DateTime? publishedUntil,
|
||||
int totalUpvote,
|
||||
int totalDownvote,
|
||||
int totalViews,
|
||||
int totalAggregatedViews,
|
||||
int publisherId,
|
||||
int? pollId,
|
||||
SnPublisher publisher,
|
||||
SnMetric metric,
|
||||
SnPostPreload? preload});
|
||||
@ -148,7 +154,10 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
Object? publishedUntil = freezed,
|
||||
Object? totalUpvote = null,
|
||||
Object? totalDownvote = null,
|
||||
Object? totalViews = null,
|
||||
Object? totalAggregatedViews = null,
|
||||
Object? publisherId = null,
|
||||
Object? pollId = freezed,
|
||||
Object? publisher = null,
|
||||
Object? metric = null,
|
||||
Object? preload = freezed,
|
||||
@ -262,10 +271,22 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
? _value.totalDownvote
|
||||
: totalDownvote // ignore: cast_nullable_to_non_nullable
|
||||
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
|
||||
? _value.publisherId
|
||||
: publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
pollId: freezed == pollId
|
||||
? _value.pollId
|
||||
: pollId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
publisher: null == publisher
|
||||
? _value.publisher
|
||||
: publisher // ignore: cast_nullable_to_non_nullable
|
||||
@ -379,7 +400,10 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
||||
DateTime? publishedUntil,
|
||||
int totalUpvote,
|
||||
int totalDownvote,
|
||||
int totalViews,
|
||||
int totalAggregatedViews,
|
||||
int publisherId,
|
||||
int? pollId,
|
||||
SnPublisher publisher,
|
||||
SnMetric metric,
|
||||
SnPostPreload? preload});
|
||||
@ -436,7 +460,10 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
||||
Object? publishedUntil = freezed,
|
||||
Object? totalUpvote = null,
|
||||
Object? totalDownvote = null,
|
||||
Object? totalViews = null,
|
||||
Object? totalAggregatedViews = null,
|
||||
Object? publisherId = null,
|
||||
Object? pollId = freezed,
|
||||
Object? publisher = null,
|
||||
Object? metric = null,
|
||||
Object? preload = freezed,
|
||||
@ -550,10 +577,22 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
||||
? _value.totalDownvote
|
||||
: totalDownvote // ignore: cast_nullable_to_non_nullable
|
||||
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
|
||||
? _value.publisherId
|
||||
: publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
pollId: freezed == pollId
|
||||
? _value.pollId
|
||||
: pollId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
publisher: null == publisher
|
||||
? _value.publisher
|
||||
: publisher // ignore: cast_nullable_to_non_nullable
|
||||
@ -601,7 +640,10 @@ class _$SnPostImpl extends _SnPost {
|
||||
required this.publishedUntil,
|
||||
required this.totalUpvote,
|
||||
required this.totalDownvote,
|
||||
this.totalViews = 0,
|
||||
this.totalAggregatedViews = 0,
|
||||
required this.publisherId,
|
||||
required this.pollId,
|
||||
required this.publisher,
|
||||
required this.metric,
|
||||
this.preload})
|
||||
@ -717,8 +759,16 @@ class _$SnPostImpl extends _SnPost {
|
||||
@override
|
||||
final int totalDownvote;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int totalViews;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int totalAggregatedViews;
|
||||
@override
|
||||
final int publisherId;
|
||||
@override
|
||||
final int? pollId;
|
||||
@override
|
||||
final SnPublisher publisher;
|
||||
@override
|
||||
final SnMetric metric;
|
||||
@ -727,7 +777,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
|
||||
@override
|
||||
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, 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
|
||||
@ -780,8 +830,13 @@ class _$SnPostImpl extends _SnPost {
|
||||
other.totalUpvote == totalUpvote) &&
|
||||
(identical(other.totalDownvote, totalDownvote) ||
|
||||
other.totalDownvote == totalDownvote) &&
|
||||
(identical(other.totalViews, totalViews) ||
|
||||
other.totalViews == totalViews) &&
|
||||
(identical(other.totalAggregatedViews, totalAggregatedViews) ||
|
||||
other.totalAggregatedViews == totalAggregatedViews) &&
|
||||
(identical(other.publisherId, publisherId) ||
|
||||
other.publisherId == publisherId) &&
|
||||
(identical(other.pollId, pollId) || other.pollId == pollId) &&
|
||||
(identical(other.publisher, publisher) ||
|
||||
other.publisher == publisher) &&
|
||||
(identical(other.metric, metric) || other.metric == metric) &&
|
||||
@ -819,7 +874,10 @@ class _$SnPostImpl extends _SnPost {
|
||||
publishedUntil,
|
||||
totalUpvote,
|
||||
totalDownvote,
|
||||
totalViews,
|
||||
totalAggregatedViews,
|
||||
publisherId,
|
||||
pollId,
|
||||
publisher,
|
||||
metric,
|
||||
preload
|
||||
@ -870,7 +928,10 @@ abstract class _SnPost extends SnPost {
|
||||
required final DateTime? publishedUntil,
|
||||
required final int totalUpvote,
|
||||
required final int totalDownvote,
|
||||
final int totalViews,
|
||||
final int totalAggregatedViews,
|
||||
required final int publisherId,
|
||||
required final int? pollId,
|
||||
required final SnPublisher publisher,
|
||||
required final SnMetric metric,
|
||||
final SnPostPreload? preload}) = _$SnPostImpl;
|
||||
@ -933,8 +994,14 @@ abstract class _SnPost extends SnPost {
|
||||
@override
|
||||
int get totalDownvote;
|
||||
@override
|
||||
int get totalViews;
|
||||
@override
|
||||
int get totalAggregatedViews;
|
||||
@override
|
||||
int get publisherId;
|
||||
@override
|
||||
int? get pollId;
|
||||
@override
|
||||
SnPublisher get publisher;
|
||||
@override
|
||||
SnMetric get metric;
|
||||
@ -1567,6 +1634,8 @@ SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) {
|
||||
mixin _$SnPostPreload {
|
||||
SnAttachment? get thumbnail => throw _privateConstructorUsedError;
|
||||
List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
|
||||
SnAttachment? get video => throw _privateConstructorUsedError;
|
||||
SnPoll? get poll => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPostPreload to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@ -1584,9 +1653,15 @@ abstract class $SnPostPreloadCopyWith<$Res> {
|
||||
SnPostPreload value, $Res Function(SnPostPreload) then) =
|
||||
_$SnPostPreloadCopyWithImpl<$Res, SnPostPreload>;
|
||||
@useResult
|
||||
$Res call({SnAttachment? thumbnail, List<SnAttachment?>? attachments});
|
||||
$Res call(
|
||||
{SnAttachment? thumbnail,
|
||||
List<SnAttachment?>? attachments,
|
||||
SnAttachment? video,
|
||||
SnPoll? poll});
|
||||
|
||||
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
||||
$SnAttachmentCopyWith<$Res>? get video;
|
||||
$SnPollCopyWith<$Res>? get poll;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -1606,6 +1681,8 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
|
||||
$Res call({
|
||||
Object? thumbnail = freezed,
|
||||
Object? attachments = freezed,
|
||||
Object? video = freezed,
|
||||
Object? poll = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
thumbnail: freezed == thumbnail
|
||||
@ -1616,6 +1693,14 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
|
||||
? _value.attachments
|
||||
: attachments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnAttachment?>?,
|
||||
video: freezed == video
|
||||
? _value.video
|
||||
: video // ignore: cast_nullable_to_non_nullable
|
||||
as SnAttachment?,
|
||||
poll: freezed == poll
|
||||
? _value.poll
|
||||
: poll // ignore: cast_nullable_to_non_nullable
|
||||
as SnPoll?,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@ -1632,6 +1717,34 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
|
||||
return _then(_value.copyWith(thumbnail: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAttachmentCopyWith<$Res>? get video {
|
||||
if (_value.video == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnAttachmentCopyWith<$Res>(_value.video!, (value) {
|
||||
return _then(_value.copyWith(video: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPollCopyWith<$Res>? get poll {
|
||||
if (_value.poll == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPollCopyWith<$Res>(_value.poll!, (value) {
|
||||
return _then(_value.copyWith(poll: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -1642,10 +1755,18 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res>
|
||||
__$$SnPostPreloadImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({SnAttachment? thumbnail, List<SnAttachment?>? attachments});
|
||||
$Res call(
|
||||
{SnAttachment? thumbnail,
|
||||
List<SnAttachment?>? attachments,
|
||||
SnAttachment? video,
|
||||
SnPoll? poll});
|
||||
|
||||
@override
|
||||
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
||||
@override
|
||||
$SnAttachmentCopyWith<$Res>? get video;
|
||||
@override
|
||||
$SnPollCopyWith<$Res>? get poll;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -1663,6 +1784,8 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
|
||||
$Res call({
|
||||
Object? thumbnail = freezed,
|
||||
Object? attachments = freezed,
|
||||
Object? video = freezed,
|
||||
Object? poll = freezed,
|
||||
}) {
|
||||
return _then(_$SnPostPreloadImpl(
|
||||
thumbnail: freezed == thumbnail
|
||||
@ -1673,6 +1796,14 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
|
||||
? _value._attachments
|
||||
: attachments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnAttachment?>?,
|
||||
video: freezed == video
|
||||
? _value.video
|
||||
: video // ignore: cast_nullable_to_non_nullable
|
||||
as SnAttachment?,
|
||||
poll: freezed == poll
|
||||
? _value.poll
|
||||
: poll // ignore: cast_nullable_to_non_nullable
|
||||
as SnPoll?,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -1682,7 +1813,9 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
|
||||
class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||
const _$SnPostPreloadImpl(
|
||||
{required this.thumbnail,
|
||||
required final List<SnAttachment?>? attachments})
|
||||
required final List<SnAttachment?>? attachments,
|
||||
required this.video,
|
||||
required this.poll})
|
||||
: _attachments = attachments;
|
||||
|
||||
factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) =>
|
||||
@ -1700,9 +1833,14 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
final SnAttachment? video;
|
||||
@override
|
||||
final SnPoll? poll;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments)';
|
||||
return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video, poll: $poll)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1713,13 +1851,15 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||
(identical(other.thumbnail, thumbnail) ||
|
||||
other.thumbnail == thumbnail) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._attachments, _attachments));
|
||||
.equals(other._attachments, _attachments) &&
|
||||
(identical(other.video, video) || other.video == video) &&
|
||||
(identical(other.poll, poll) || other.poll == poll));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, thumbnail,
|
||||
const DeepCollectionEquality().hash(_attachments));
|
||||
const DeepCollectionEquality().hash(_attachments), video, poll);
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -1740,7 +1880,9 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||
abstract class _SnPostPreload implements SnPostPreload {
|
||||
const factory _SnPostPreload(
|
||||
{required final SnAttachment? thumbnail,
|
||||
required final List<SnAttachment?>? attachments}) = _$SnPostPreloadImpl;
|
||||
required final List<SnAttachment?>? attachments,
|
||||
required final SnAttachment? video,
|
||||
required final SnPoll? poll}) = _$SnPostPreloadImpl;
|
||||
|
||||
factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
|
||||
_$SnPostPreloadImpl.fromJson;
|
||||
@ -1749,6 +1891,10 @@ abstract class _SnPostPreload implements SnPostPreload {
|
||||
SnAttachment? get thumbnail;
|
||||
@override
|
||||
List<SnAttachment?>? get attachments;
|
||||
@override
|
||||
SnAttachment? get video;
|
||||
@override
|
||||
SnPoll? get poll;
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
@ -62,7 +62,11 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
||||
: DateTime.parse(json['published_until'] as String),
|
||||
totalUpvote: (json['total_upvote'] 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(),
|
||||
pollId: (json['poll_id'] as num?)?.toInt(),
|
||||
publisher:
|
||||
SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
||||
metric: SnMetric.fromJson(json['metric'] as Map<String, dynamic>),
|
||||
@ -100,7 +104,10 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
||||
'published_until': instance.publishedUntil?.toIso8601String(),
|
||||
'total_upvote': instance.totalUpvote,
|
||||
'total_downvote': instance.totalDownvote,
|
||||
'total_views': instance.totalViews,
|
||||
'total_aggregated_views': instance.totalAggregatedViews,
|
||||
'publisher_id': instance.publisherId,
|
||||
'poll_id': instance.pollId,
|
||||
'publisher': instance.publisher.toJson(),
|
||||
'metric': instance.metric.toJson(),
|
||||
'preload': instance.preload?.toJson(),
|
||||
@ -165,12 +172,20 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
|
||||
? null
|
||||
: SnAttachment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
video: json['video'] == null
|
||||
? null
|
||||
: SnAttachment.fromJson(json['video'] as Map<String, dynamic>),
|
||||
poll: json['poll'] == null
|
||||
? null
|
||||
: SnPoll.fromJson(json['poll'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'thumbnail': instance.thumbnail?.toJson(),
|
||||
'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
|
||||
'video': instance.video?.toJson(),
|
||||
'poll': instance.poll?.toJson(),
|
||||
};
|
||||
|
||||
_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
|
||||
|
@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
|
||||
@ -49,10 +50,19 @@ class _AccountSelectState extends State<AccountSelect> {
|
||||
Future<void> _getFriends() async {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
|
||||
if (!mounted) return;
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
setState(() {
|
||||
_relativeUsers.addAll(
|
||||
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
|
||||
resp.data?.map((e) {
|
||||
final rel = SnRelationship.fromJson(e);
|
||||
if (rel.relatedId == ua.user?.id) {
|
||||
return rel.account!;
|
||||
} else {
|
||||
return rel.related!;
|
||||
}
|
||||
}).cast<SnAccount>(),
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -123,13 +133,9 @@ class _AccountSelectState extends State<AccountSelect> {
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _pendingUsers.isEmpty
|
||||
? _relativeUsers.length
|
||||
: _pendingUsers.length,
|
||||
itemCount: _pendingUsers.isEmpty ? _relativeUsers.length : _pendingUsers.length,
|
||||
itemBuilder: (context, index) {
|
||||
var user = _pendingUsers.isEmpty
|
||||
? _relativeUsers[index]
|
||||
: _pendingUsers[index];
|
||||
var user = _pendingUsers.isEmpty ? _relativeUsers[index] : _pendingUsers[index];
|
||||
return ListTile(
|
||||
title: Text(user.nick),
|
||||
subtitle: Text(user.name),
|
||||
@ -148,8 +154,7 @@ class _AccountSelectState extends State<AccountSelect> {
|
||||
}
|
||||
|
||||
setState(() {
|
||||
final idx = _selectedUsers
|
||||
.indexWhere((x) => x.id == user.id);
|
||||
final idx = _selectedUsers.indexWhere((x) => x.id == user.id);
|
||||
if (idx != -1) {
|
||||
_selectedUsers.removeAt(idx);
|
||||
} else {
|
||||
|
@ -6,12 +6,22 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class AttachmentInputDialog extends StatefulWidget {
|
||||
final String? title;
|
||||
final bool? analyzeNow;
|
||||
const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false});
|
||||
final bool? analyzeNow;
|
||||
final SnMediaType? mediaType;
|
||||
final String pool;
|
||||
|
||||
const AttachmentInputDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.pool,
|
||||
this.analyzeNow = false,
|
||||
this.mediaType = SnMediaType.image,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
|
||||
@ -20,13 +30,18 @@ final bool? analyzeNow;
|
||||
class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
final _randomIdController = TextEditingController();
|
||||
|
||||
XFile? _thumbnailFile;
|
||||
XFile? _file;
|
||||
double? _progress;
|
||||
|
||||
void _pickImage() async {
|
||||
void _pickMedia() async {
|
||||
final picker = ImagePicker();
|
||||
final result = await picker.pickImage(source: ImageSource.gallery);
|
||||
final result = switch (widget.mediaType) {
|
||||
SnMediaType.image => await picker.pickImage(source: ImageSource.gallery),
|
||||
SnMediaType.video => await picker.pickVideo(source: ImageSource.gallery),
|
||||
_ => await picker.pickMedia(),
|
||||
};
|
||||
if (result == null) return;
|
||||
setState(() => _thumbnailFile = result);
|
||||
setState(() => _file = result);
|
||||
}
|
||||
|
||||
bool _isBusy = false;
|
||||
@ -46,15 +61,20 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
} else if (_thumbnailFile != null) {
|
||||
} else if (_file != null) {
|
||||
try {
|
||||
final attachment = await attach.directUploadOne(
|
||||
(await _thumbnailFile!.readAsBytes()).buffer.asUint8List(),
|
||||
_thumbnailFile!.path,
|
||||
'interactive',
|
||||
null,
|
||||
final place = await attach.chunkedUploadInitialize(await _file!.length(), _file!.name, widget.pool, null);
|
||||
|
||||
final attachment = await attach.chunkedUploadParts(
|
||||
_file!,
|
||||
place.$1,
|
||||
place.$2,
|
||||
analyzeNow: widget.analyzeNow ?? false,
|
||||
onProgress: (value) {
|
||||
setState(() => _progress = value);
|
||||
},
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, attachment);
|
||||
} catch (err) {
|
||||
@ -67,7 +87,7 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.title ?? 'attachmentInputDialog').tr(),
|
||||
title: Text(widget.title ?? 'attachmentInputDialog'.tr()),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -86,24 +106,35 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
const Gap(24),
|
||||
Text('attachmentInputNew').tr().fontSize(14),
|
||||
Card(
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: const Icon(Symbols.add_photo_alternate),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('addAttachmentFromAlbum').tr(),
|
||||
subtitle: _thumbnailFile == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
|
||||
onTap: () {
|
||||
_pickImage();
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: const Icon(Symbols.add_photo_alternate),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('addAttachmentFromAlbum').tr(),
|
||||
subtitle: _file == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
|
||||
onTap: () {
|
||||
_pickMedia();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_isBusy)
|
||||
LinearProgressIndicator(
|
||||
value: _progress,
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
).padding(top: 16),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('dialogDismiss').tr(),
|
||||
),
|
||||
TextButton(
|
||||
|
@ -45,7 +45,12 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
|
||||
final HotKey _pasteHotKey = HotKey(
|
||||
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,
|
||||
);
|
||||
|
||||
@ -61,6 +66,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
));
|
||||
setState(() {});
|
||||
});
|
||||
hotKeyManager.register(_newLineHotKey, keyDownHandler: (_) async {
|
||||
if (_contentController.text.isEmpty) return;
|
||||
_contentController.text += '\n';
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -112,6 +121,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
if (_contentController.text.isEmpty && _attachments.isEmpty) return;
|
||||
if (_isBusy) return;
|
||||
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
@ -203,7 +213,11 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
void dispose() {
|
||||
_contentController.dispose();
|
||||
_focusNode.dispose();
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
|
||||
_dismissEmojiPicker();
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
|
||||
hotKeyManager.unregister(_pasteHotKey);
|
||||
hotKeyManager.unregister(_newLineHotKey);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -343,6 +357,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
_sendMessage();
|
||||
_focusNode.requestFocus();
|
||||
},
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
@ -351,10 +366,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
Symbols.mood,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
_showEmojiPicker(context);
|
||||
},
|
||||
@ -372,10 +386,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
Symbols.send,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -58,6 +58,7 @@ class AppScaffold extends StatelessWidget {
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: SizedBox.expand(
|
||||
child: AppBackground(
|
||||
isRoot: true,
|
||||
child: Column(
|
||||
children: [
|
||||
IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)),
|
||||
@ -165,10 +166,12 @@ class AppRootScaffold extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Solar Network',
|
||||
style: GoogleFonts.spaceGrotesk(),
|
||||
).padding(horizontal: 12, vertical: 5),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Solar Network',
|
||||
style: GoogleFonts.spaceGrotesk(),
|
||||
).padding(horizontal: 12, vertical: 5),
|
||||
),
|
||||
if (!Platform.isMacOS)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
@ -15,6 +16,47 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
import '../../providers/sn_network.dart';
|
||||
|
||||
class PostCommentQuickAction extends StatelessWidget {
|
||||
final double? maxWidth;
|
||||
final SnPost parentPost;
|
||||
final Function? onPosted;
|
||||
|
||||
const PostCommentQuickAction({super.key, this.maxWidth, required this.parentPost, this.onPosted});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
return Container(
|
||||
height: 240,
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.symmetric(vertical: 8) : EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? const BorderRadius.all(Radius.circular(8))
|
||||
: BorderRadius.zero,
|
||||
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
)
|
||||
: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PostMiniEditor(
|
||||
postReplyId: parentPost.id,
|
||||
onPost: () {
|
||||
onPosted?.call();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PostCommentSliverList extends StatefulWidget {
|
||||
final SnPost parentPost;
|
||||
final double? maxWidth;
|
||||
@ -71,6 +113,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
|
||||
Future<void> refresh() async {
|
||||
_posts.clear();
|
||||
_postCount = null;
|
||||
_fetchPosts();
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/reaction.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/link_preview.dart';
|
||||
@ -35,6 +36,7 @@ import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||
import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||
import 'package:surface/widgets/post/post_poll.dart';
|
||||
import 'package:surface/widgets/post/post_reaction.dart';
|
||||
import 'package:surface/widgets/post/publisher_popover.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
@ -68,32 +70,34 @@ class OpenablePostItem extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return OpenContainer(
|
||||
closedBuilder: (_, __) => Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: PostItem(
|
||||
data: data,
|
||||
maxWidth: maxWidth,
|
||||
showComments: showComments,
|
||||
showFullPost: showFullPost,
|
||||
onChanged: onChanged,
|
||||
onDeleted: onDeleted,
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
),
|
||||
),
|
||||
openBuilder: (_, close) => PostDetailScreen(
|
||||
slug: data.id.toString(),
|
||||
preload: data,
|
||||
onBack: close,
|
||||
),
|
||||
openColor: Colors.transparent,
|
||||
openElevation: 0,
|
||||
transitionType: ContainerTransitionType.fade,
|
||||
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
||||
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
||||
return Center(
|
||||
child: OpenContainer(
|
||||
closedBuilder: (_, __) => Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: PostItem(
|
||||
data: data,
|
||||
maxWidth: maxWidth,
|
||||
showComments: showComments,
|
||||
showFullPost: showFullPost,
|
||||
onChanged: onChanged,
|
||||
onDeleted: onDeleted,
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
),
|
||||
closedShape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
openBuilder: (_, close) => PostDetailScreen(
|
||||
slug: data.id.toString(),
|
||||
preload: data,
|
||||
onBack: close,
|
||||
),
|
||||
openColor: Colors.transparent,
|
||||
openElevation: 0,
|
||||
transitionType: ContainerTransitionType.fade,
|
||||
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
||||
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
||||
),
|
||||
closedShape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -192,6 +196,57 @@ class PostItem extends StatelessWidget {
|
||||
final ua = context.read<UserProvider>();
|
||||
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
|
||||
|
||||
// Video full view
|
||||
if (showFullPost && data.type == 'video' && ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PostContentHeader(
|
||||
data: data,
|
||||
isAuthor: isAuthor,
|
||||
isRelativeDate: !showFullPost,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
onDeleted: () {
|
||||
if (onDeleted != null) {}
|
||||
},
|
||||
).padding(bottom: 8),
|
||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(bottom: 8),
|
||||
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
|
||||
_PostFeaturedComment(data: data),
|
||||
_PostBottomAction(
|
||||
data: data,
|
||||
showComments: true,
|
||||
showReactions: showReactions,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onChanged: _onChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
SizedBox(
|
||||
width: 340,
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
PostCommentSliverList(
|
||||
parentPost: data,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Article headline preview
|
||||
if (!showFullPost && data.type == 'article') {
|
||||
return Container(
|
||||
@ -210,6 +265,7 @@ class PostItem extends StatelessWidget {
|
||||
if (onDeleted != null) {}
|
||||
},
|
||||
).padding(horizontal: 12, top: 8, bottom: 8),
|
||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
|
||||
@ -293,6 +349,7 @@ class PostItem extends StatelessWidget {
|
||||
if (onDeleted != null) onDeleted!();
|
||||
},
|
||||
).padding(horizontal: 12, vertical: 8),
|
||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
|
||||
if (data.body['title'] != null || data.body['description'] != null)
|
||||
_PostHeadline(
|
||||
@ -333,6 +390,7 @@ class PostItem extends StatelessWidget {
|
||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!).padding(horizontal: 12, vertical: 4),
|
||||
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
||||
LinkPreviewWidget(
|
||||
text: data.body['content'],
|
||||
@ -626,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(
|
||||
@ -771,7 +838,6 @@ class _PostContentHeader extends StatelessWidget {
|
||||
await sn.client.delete('/cgi/co/posts/${data.id}', queryParameters: {
|
||||
'publisherId': data.publisherId,
|
||||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
context.showSnackbar('postDeleted'.tr(args: ['#${data.id}']));
|
||||
} catch (err) {
|
||||
@ -780,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
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
@ -907,7 +992,7 @@ class _PostContentHeader extends StatelessWidget {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': data.typePlural},
|
||||
pathParameters: {'mode': 'stories'},
|
||||
queryParameters: {'replying': data.id.toString()},
|
||||
);
|
||||
},
|
||||
@ -923,7 +1008,7 @@ class _PostContentHeader extends StatelessWidget {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': data.typePlural},
|
||||
pathParameters: {'mode': 'stories'},
|
||||
queryParameters: {'reposting': data.id.toString()},
|
||||
);
|
||||
},
|
||||
@ -971,6 +1056,18 @@ class _PostContentHeader extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Symbols.flag),
|
||||
const Gap(16),
|
||||
Text('flagPostAction').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
_flagPost(context);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.report),
|
||||
const Gap(16),
|
||||
Text('report').tr(),
|
||||
],
|
||||
),
|
||||
@ -1520,3 +1617,29 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostVideoPlayer extends StatelessWidget {
|
||||
final SnPost data;
|
||||
|
||||
const _PostVideoPlayer({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +95,7 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
title: 'attachmentSetThumbnail'.tr(),
|
||||
pool: 'interactive',
|
||||
analyzeNow: true,
|
||||
),
|
||||
);
|
||||
|
138
lib/widgets/post/post_poll.dart
Normal file
138
lib/widgets/post/post_poll.dart
Normal file
@ -0,0 +1,138 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class PostPoll extends StatefulWidget {
|
||||
final SnPoll poll;
|
||||
|
||||
const PostPoll({super.key, required this.poll});
|
||||
|
||||
@override
|
||||
State<PostPoll> createState() => _PostPollState();
|
||||
}
|
||||
|
||||
class _PostPollState extends State<PostPoll> {
|
||||
bool _isBusy = false;
|
||||
late SnPoll _poll;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_poll = widget.poll;
|
||||
_fetchAnswer();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
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 {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) return;
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp =
|
||||
await sn.client.get('/cgi/co/polls/${widget.poll.id}/answer');
|
||||
_answeredChoice = resp.data?['answer'];
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
// ignore because it may not found
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _voteForOption(SnPollOption option) async {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) return;
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/co/polls/${widget.poll.id}/answer', data: {
|
||||
'answer': option.id,
|
||||
});
|
||||
if (!mounted) return;
|
||||
HapticFeedback.heavyImpact();
|
||||
_answeredChoice = option.id;
|
||||
_refreshPoll();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
for (final option in _poll.options)
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: MediaQuery.of(context).size.width *
|
||||
(_poll.metric.byOptionsPercentage[option.id] ?? 0)
|
||||
.toDouble(),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
minTileHeight: 60,
|
||||
leading: _answeredChoice == option.id
|
||||
? const Icon(Symbols.circle, fill: 1)
|
||||
: const Icon(Symbols.circle),
|
||||
title: Text(option.name),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'pollVotes'
|
||||
.plural(_poll.metric.byOptions[option.id] ?? 0),
|
||||
),
|
||||
Text(' · ').padding(horizontal: 4),
|
||||
Text(
|
||||
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (option.description.isNotEmpty)
|
||||
Text(option.description),
|
||||
],
|
||||
),
|
||||
onTap: _isBusy ? null : () => _voteForOption(option),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
201
lib/widgets/post/post_poll_editor.dart
Normal file
201
lib/widgets/post/post_poll_editor.dart
Normal file
@ -0,0 +1,201 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class PollEditorDialog extends StatefulWidget {
|
||||
final SnPoll? poll;
|
||||
|
||||
const PollEditorDialog({super.key, this.poll});
|
||||
|
||||
@override
|
||||
State<PollEditorDialog> createState() => _PollEditorDialogState();
|
||||
}
|
||||
|
||||
class _PollEditorDialogState extends State<PollEditorDialog> {
|
||||
final TextEditingController _linkController = TextEditingController();
|
||||
final List<SnPollOption> _pollOptions = List.empty(growable: true);
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _fetchPoll() async {
|
||||
if (_linkController.text.isEmpty) return;
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/polls/${_linkController.text}');
|
||||
final out = SnPoll.fromJson(resp.data);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, out);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _applyPost() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = widget.poll == null
|
||||
? await sn.client.post('/cgi/co/polls', data: {
|
||||
'options': _pollOptions.where((ele) => ele.name.isNotEmpty).toList(),
|
||||
})
|
||||
: await sn.client.put('/cgi/co/polls/${widget.poll!.id}', data: {
|
||||
'options': _pollOptions.where((ele) => ele.name.isNotEmpty).toList(),
|
||||
});
|
||||
final out = SnPoll.fromJson(resp.data);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, out);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deletePoll() async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'pollEditorDelete'.tr(),
|
||||
'pollEditorDeleteDescription'.tr(),
|
||||
);
|
||||
if (!confirm) return;
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/co/polls/${widget.poll!.id}');
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, false);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pollOptions.addAll(widget.poll?.options ?? []);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_linkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.poll == null ? 'pollEditorNew' : 'pollEditorEdit').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 16,
|
||||
children: [
|
||||
if (widget.poll == null)
|
||||
TextField(
|
||||
controller: _linkController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
labelText: 'pollLinkExisting'.tr(),
|
||||
prefixText: '#',
|
||||
suffixIcon: IconButton(
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _isBusy ? null : () => _fetchPoll(),
|
||||
icon: const Icon(Icons.keyboard_arrow_right),
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < _pollOptions.length; i++)
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.circle),
|
||||
title: TextFormField(
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'pollOptionName'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
initialValue: _pollOptions[i].name,
|
||||
onChanged: (value) {
|
||||
// Looks like we don't need set state here cuz it got internal updated.
|
||||
_pollOptions[i] = _pollOptions[i].copyWith(name: value);
|
||||
},
|
||||
),
|
||||
trailing: IconButton(
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
setState(() => _pollOptions.removeAt(i));
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('pollOptionAdd').tr(),
|
||||
onTap: () {
|
||||
setState(
|
||||
() => _pollOptions.add(
|
||||
SnPollOption(id: const Uuid().v4(), icon: '', name: '', description: ''),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.poll != null)
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.delete),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('pollEditorDelete').tr(),
|
||||
onTap: _isBusy ? null : () => _deletePoll(),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.link_off),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('pollEditorUnlink').tr(),
|
||||
onTap: _isBusy ? null : () => Navigator.pop(context, false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => Navigator.pop(context),
|
||||
child: Text('cancel'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _applyPost(),
|
||||
child: Text('dialogConfirm'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
#include "my_application.h"
|
||||
|
||||
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
||||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
@ -42,15 +41,16 @@ static void my_application_activate(GApplication* application) {
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
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_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "Surface");
|
||||
gtk_window_set_title(window, "bitsdojo_window_example");
|
||||
}
|
||||
|
||||
auto bdw = bitsdojo_window_from(window);
|
||||
bdw->setCustomFrame(true);
|
||||
//gtk_window_set_default_size(window, 1280, 720);
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
@ -84,24 +84,6 @@ static gboolean my_application_local_command_line(GApplication* application, gch
|
||||
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.
|
||||
static void my_application_dispose(GObject* 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) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ PODS:
|
||||
- bitsdojo_window_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- croppy (0.0.1):
|
||||
- FlutterMacOS
|
||||
@ -143,7 +142,7 @@ PODS:
|
||||
- HotKey
|
||||
- in_app_review (2.0.0):
|
||||
- FlutterMacOS
|
||||
- livekit_client (2.3.5):
|
||||
- livekit_client (2.3.6):
|
||||
- flutter_webrtc
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@ -190,7 +189,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
||||
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||
@ -244,7 +243,7 @@ EXTERNAL SOURCES:
|
||||
bitsdojo_window_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
|
||||
connectivity_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
|
||||
croppy:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
|
||||
device_info_plus:
|
||||
@ -308,7 +307,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
|
||||
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
|
||||
connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802
|
||||
croppy: 25a638bd7d05411d8c697f481568f261037694fc
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
|
||||
@ -334,7 +333,7 @@ SPEC CHECKSUMS:
|
||||
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
|
||||
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
|
||||
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
||||
livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
|
||||
livekit_client: 0ad107154753a5a76802d2222c040223ad049499
|
||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||
|
90
pubspec.lock
90
pubspec.lock
@ -138,26 +138,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948"
|
||||
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.3"
|
||||
version: "4.0.4"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e"
|
||||
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.4.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
|
||||
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.14"
|
||||
version: "2.4.15"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -214,6 +214,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
chalkdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: chalkdart
|
||||
sha256: "08c910ee341fcdd1e46f20ddce59b13c1d85f5d97f2fd2f12014c46ede670e40"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -266,10 +274,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
|
||||
sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
version: "6.1.3"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -346,10 +354,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dart_webrtc
|
||||
sha256: e65506edb452148220efab53d8d2f8bb9d827bd8bcd53cf3a3e6df70b27f3d86
|
||||
sha256: "03df5b41b23bc185ebcf4b0ffc92d002e295bf56287fb5f9d2c321ddaf7760cc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.10"
|
||||
version: "1.5.1"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -362,10 +370,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a
|
||||
sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.2.2"
|
||||
version: "11.3.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -490,10 +498,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc
|
||||
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.1"
|
||||
version: "8.3.7"
|
||||
file_saver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -530,10 +538,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4"
|
||||
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+3"
|
||||
version: "0.9.3+4"
|
||||
firebase_analytics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -764,10 +772,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: b3ff1ef5fb3924ee02b4d38b974ffae3969d50603e68787684ee9dd45f6f144a
|
||||
sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6+1"
|
||||
version: "0.7.6+2"
|
||||
flutter_native_splash:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -830,10 +838,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_webrtc
|
||||
sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03"
|
||||
sha256: "572df3de6c828e571db4b75b4a96a15c2f34fa3d420a84438f44a3158b22e81a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.7"
|
||||
version: "0.12.9"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -886,10 +894,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa"
|
||||
sha256: "04539267a740931c6d4479a10d466717ca5901c6fdfd3fcda09391bbb8ebd651"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.7.2"
|
||||
version: "14.8.0"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -934,10 +942,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: home_widget
|
||||
sha256: b313e3304c0429669fddf1286e1fbf61a64b873f38ba30b3eb890ef0d7560b12
|
||||
sha256: "7430f7549d42cef2e729bd3c779de748b93f1eb78b1abfe6bca8fffd1cfce3e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
version: "0.7.0+1"
|
||||
hotkey_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1035,7 +1043,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c
|
||||
@ -1190,10 +1198,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: livekit_client
|
||||
sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
|
||||
sha256: "0cfb2f48eff7a93ea8927696dc6f218aebd2fcd1fcc1b1a7b2f53ff3597fdb52"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
version: "2.3.6"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1246,10 +1254,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: material_symbols_icons
|
||||
sha256: "89aac72d25dd49303f71b3b1e70f8374791846729365b25bebc2a2531e5b86cd"
|
||||
sha256: "1403944c2a68dbdf934fca2b2115a372e70a4a185c136ab71a9d16e2108d0b14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2801.1"
|
||||
version: "4.2805.1"
|
||||
media_kit:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1382,10 +1390,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: c447a3c3e7be4addf129b8f9ab6a4bd5d166b78918223e223b61fddf4d07e254
|
||||
sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.0"
|
||||
version: "8.2.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1758,18 +1766,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a"
|
||||
sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.1"
|
||||
version: "2.5.2"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f"
|
||||
sha256: ea86be7b7114f9e94fddfbb52649e59a03d6627ccd2387ebddcd6624719e9f16
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "2.4.5"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1822,10 +1830,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.0"
|
||||
shortid:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2179,10 +2187,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_compress
|
||||
sha256: "5b42d89f3970c956bad7a86c29682b0892c11a4ddf95ae6e29897ee28788e377"
|
||||
sha256: "31bc5cdb9a02ba666456e5e1907393c28e6e0e972980d7d8d619a7beda0d4f20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
version: "3.1.4"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2251,10 +2259,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webrtc_interface
|
||||
sha256: abec3ab7956bd5ac539cf34a42fa0c82ea26675847c0966bb85160400eea9388
|
||||
sha256: "10fc6dc0ac16f909f5e434c18902415211d759313c87261f1e4ec5b4f6a04c26"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 2.3.2+64
|
||||
version: 2.3.2+70
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@ -120,6 +120,7 @@ dependencies:
|
||||
xml: ^6.5.0
|
||||
tray_manager: ^0.3.2
|
||||
hotkey_manager: ^0.2.3
|
||||
image_picker_android: ^0.8.12+20
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user