Compare commits

..

38 Commits

Author SHA1 Message Date
5ddd4fed2e 🐛 Fix missing new publisher button 2025-02-15 01:17:09 +08:00
48b6d5f6c1 🐛 Fix poll 2025-02-15 00:16:06 +08:00
b83b0b5efb 🚀 Launch 2.3.2+67 2025-02-13 22:54:30 +08:00
cb24bd953d Poll participate 2025-02-13 22:35:53 +08:00
4937dee182 Poll editor 2025-02-12 23:56:45 +08:00
d612097bb1 🐛 Fix publisher edit has no header 2025-02-12 19:48:49 +08:00
058d668b6b 💄 Optimize post video displaying 2025-02-12 19:13:08 +08:00
8b19462c3a 🐛 Fix post video bug 2025-02-12 16:56:36 +08:00
0a381ef09b 🚀 Launch 2.3.2+66 2025-02-11 21:59:01 +08:00
9b84e912b2 🐛 Fix post item width issue 2025-02-11 21:35:53 +08:00
b3254e0f2f Realm discovery 2025-02-11 21:31:53 +08:00
f0a3bbe023 🐛 Bug fixes 2025-02-10 18:00:15 +08:00
df81c84438 🐛 Bug fixes 2025-02-10 17:54:31 +08:00
8b12395fca 💄 Add more actions to video post editor 2025-02-10 11:51:42 +08:00
cb2b71d194 🚀 Launch 2.3.2+65 2025-02-10 00:52:09 +08:00
7ed508e2bb Video post 2025-02-10 00:44:52 +08:00
dad869967e 🚀 Launch 2.3.2+64 2025-02-08 15:01:41 +08:00
2d5b3b554e ♻️ Apply new OpenablePostItem to almost everywhere 2025-02-08 13:58:35 +08:00
74882116e3 🐛 Bug fixes on AI Insight 2025-02-08 13:41:39 +08:00
a97c3bce3a Select & Featured Answer 2025-02-08 13:27:53 +08:00
1aa70827dc Create questions & display questions 2025-02-08 01:35:27 +08:00
fe028860e9 💄 Optimize post editors 2025-02-07 22:35:04 +08:00
a2d2ce4d38 🐛 Trying to fix stream already listen 2025-02-07 21:33:39 +08:00
167c11b9eb ♻️ Optimize post editor architecture 2025-02-07 20:19:48 +08:00
8cb3933fcc 🐛 Bug fixes 2025-02-07 18:11:28 +08:00
3818328afe Cmd/Ctrl-V to paste image 2025-02-06 15:04:04 +08:00
11627e2455 💄 Clear tray number when click from it 2025-02-06 14:48:41 +08:00
3f82c06ff8 🐛 Fix use Cmd+Q quitting app 2025-02-06 14:08:57 +08:00
2350f59131 💄 Transparent app bar with real white 2025-02-06 13:22:34 +08:00
9fe7c9530a ♻️ Replace duplicate widgets with account select 2025-02-06 13:17:17 +08:00
52f1826e91 🚀 Launch 2.2.2+62 2025-02-04 22:56:45 +08:00
28a4c86dbf Optimize post editor 2025-02-04 22:04:50 +08:00
85e48ce03b ♻️ Refactor tray with manager 2025-02-04 16:11:25 +08:00
efef61a8ea Tray icon basis 2025-02-04 15:43:20 +08:00
10ead95af9 Emotes picker 2025-02-04 02:33:19 +08:00
838ee4d55d Click to zoom in sticker 2025-02-03 22:56:49 +08:00
13e42429a9 📝 Update API docs 2025-02-03 21:34:15 +08:00
c6ce3fe2b7 🐛 Patch websocket connection issue 2025-02-03 21:34:05 +08:00
65 changed files with 3979 additions and 987 deletions

View File

@ -12,9 +12,9 @@ post {
body:json {
{
"alias": "AteChip",
"name": "Cat ate chips",
"attachment_id": "d0b692cc64054463",
"pack_id": 2
"alias": "BaLoading",
"name": "BaLoading",
"attachment_id": "2JCI2uh21mKkfk9P",
"pack_id": 3
}
}

View File

@ -0,0 +1,11 @@
meta {
name: Get Sticker Packs
type: http
seq: 3
}
get {
url: {{endpoint}}/cgi/uc/stickers/packs
body: none
auth: none
}

View File

@ -0,0 +1,15 @@
meta {
name: Get Stickers
type: http
seq: 4
}
get {
url: {{endpoint}}/cgi/uc/stickers?take=10
body: none
auth: none
}
params:query {
take: 10
}

View File

@ -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
}
}

View File

@ -0,0 +1,20 @@
meta {
name: Create Order
type: http
seq: 1
}
post {
url: {{endpoint}}/cgi/wa/orders
body: json
auth: none
}
body:json {
{
"client_id": "highland-mc",
"client_secret": "(3^DLAvo3v",
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
"amount": 500
}
}

View File

@ -0,0 +1,21 @@
meta {
name: Create Transaction
type: http
seq: 3
}
post {
url: {{endpoint}}/cgi/wa/transactions
body: json
auth: none
}
body:json {
{
"client_id": "alphabot",
"client_secret": "_uR0sVnHTh",
"remark": "新年红包",
"amount": 9705,
"payee_id": 2
}
}

20
api/Wallet/Get Order.bru Normal file
View File

@ -0,0 +1,20 @@
meta {
name: Get Order
type: http
seq: 2
}
get {
url: {{endpoint}}/cgi/wa/orders/4
body: none
auth: none
}
body:json {
{
"client_id": "highland-mc",
"client_secret": "(3^DLAvo3v",
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
"amount": 500
}
}

View File

@ -0,0 +1,20 @@
meta {
name: Get Transaction
type: http
seq: 4
}
get {
url: {{endpoint}}/cgi/wa/transactions/67
body: none
auth: inherit
}
body:json {
{
"client_id": "highland-mc",
"client_secret": "(3^DLAvo3v",
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
"amount": 500
}
}

BIN
assets/icon/tray-icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/icon/tray-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View File

@ -27,6 +27,7 @@
"screenChatNew": "New Channel",
"screenRealm": "Realm",
"screenRealmManage": "Edit Realm",
"screenRealmDiscovery": "Realm Discovery",
"screenRealmNew": "New Realm",
"screenNotification": "Notification",
"screenPostSearch": "Search Posts",
@ -154,9 +155,12 @@
"fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
"writePostTypeStory": "Post a story",
"writePostTypeArticle": "Write an article",
"writePostTypeQuestion": "Ask a question",
"writePostTypeVideo": "Post a video",
"fieldPostPublisher": "Post publisher",
"fieldPostContent": "What happened?!",
"fieldPostTitle": "Title",
"fieldPostQuestionReward": "Answer Rewards (Source Points)",
"fieldPostDescription": "Description",
"fieldPostTags": "Tags",
"fieldPostCategories": "Categories",
@ -166,9 +170,9 @@
"postPosted": "Post has been posted.",
"postPublishedAt": "Published At",
"postPublishedUntil": "Published Until",
"postEditingNotice": "You're about to editing a post that posted {}.",
"postReplyingNotice": "You're about to reply to a post that posted {}.",
"postRepostingNotice": "You're about to repost a post that posted {}.",
"postEditingNotice": "You're about to editing a post that posted by {}.",
"postReplyingNotice": "You're about to reply to a post that posted by {}.",
"postRepostingNotice": "You're about to repost a post that posted by {}.",
"postReact": "React",
"postReactions": "Reactions of Post",
"postReactionUpvote": {
@ -609,5 +613,30 @@
"other": "{} Source Points"
},
"aiThinkingProcess": "AI Thinking Process",
"accountSettingsApplied": "Account settings have been applied."
"accountSettingsApplied": "Account settings have been applied.",
"trayMenuExit": "Exit",
"postQuestionUnanswered": "Unanswered Question",
"postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
"postQuestionAnswered": "Answered Question",
"postQuestionAnswerSelect": "Select as Answer",
"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"
}
}

View File

@ -25,6 +25,7 @@
"screenChatNew": "新建聊天频道",
"screenRealm": "领域",
"screenRealmManage": "编辑领域",
"screenRealmDiscovery": "发现领域",
"screenRealmNew": "新建领域",
"screenNotification": "通知",
"screenPostSearch": "搜索帖子",
@ -138,9 +139,12 @@
"fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
"writePostTypeStory": "发动态",
"writePostTypeArticle": "写文章",
"writePostTypeQuestion": "提问题",
"writePostTypeVideo": "发视频",
"fieldPostPublisher": "帖子发布者",
"fieldPostContent": "发生什么事了?!",
"fieldPostTitle": "标题",
"fieldPostQuestionReward": "回答奖励源点",
"fieldPostDescription": "描述",
"fieldPostTags": "标签",
"fieldPostCategories": "分类",
@ -607,5 +611,31 @@
"other": "{} 源点"
},
"aiThinkingProcess": "AI 思考过程",
"accountSettingsApplied": "帐号设置已应用。"
"accountSettingsApplied": "帐号设置已应用。",
"trayMenuExit": "退出",
"postQuestionUnanswered": "未解答的问题",
"postQuestionUnansweredWithReward": "未解答的问题,悬赏源点 {}",
"postQuestionAnswered": "已解答的问题",
"postQuestionAnswerTitle": "精选解答",
"postQuestionAnswerSelect": "选择解答",
"postQuestionAnswerSelected": "解答已选择,奖励已发放。",
"postVideoUpload": "上传视频",
"realmJoin": "加入领域",
"realmCommunityHint": "该领域是一个社区领域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "该领域包含的公共频道",
"realmJoined": "已加入领域 {}。",
"join": "加入",
"pollEditorNew": "新投票",
"pollEditorEdit": "编辑投票",
"pollEditorDelete": "删除投票",
"pollEditorDeleteDescription": "你确定要删除这个投票吗?该操作不可撤销。",
"pollEditorUnlink": "解除链接",
"pollOptionAdd": "添加选项",
"pollOptionName": "选项名称",
"pollLinkExisting": "链接现有投票",
"pollAnswered": "答案已经反馈。",
"pollVotes": {
"one": "{} 票",
"other": "{} 票"
}
}

View File

@ -25,6 +25,7 @@
"screenChatNew": "新建聊天頻道",
"screenRealm": "領域",
"screenRealmManage": "編輯領域",
"screenRealmDiscovery": "發現領域",
"screenRealmNew": "新建領域",
"screenNotification": "通知",
"screenPostSearch": "搜索帖子",
@ -138,9 +139,12 @@
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
"writePostTypeStory": "發動態",
"writePostTypeArticle": "寫文章",
"writePostTypeQuestion": "提問題",
"writePostTypeVideo": "發視頻",
"fieldPostPublisher": "帖子發佈者",
"fieldPostContent": "發生什麼事了?!",
"fieldPostTitle": "標題",
"fieldPostQuestionReward": "回答獎勵源點",
"fieldPostDescription": "描述",
"fieldPostTags": "標籤",
"fieldPostCategories": "分類",
@ -607,5 +611,18 @@
"other": "{} 源點"
},
"aiThinkingProcess": "AI 思考過程",
"accountSettingsApplied": "帳號設置已應用。"
"accountSettingsApplied": "帳號設置已應用。",
"trayMenuExit": "退出",
"postQuestionUnanswered": "未解答的問題",
"postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
"postQuestionAnswered": "已解答的問題",
"postQuestionAnswerTitle": "精選解答",
"postQuestionAnswerSelect": "選擇解答",
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
"postVideoUpload": "上傳視頻",
"realmJoin": "加入領域",
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
"realmJoined": "已加入領域 {}。",
"join": "加入"
}

View File

@ -25,6 +25,7 @@
"screenChatNew": "新建聊天頻道",
"screenRealm": "領域",
"screenRealmManage": "編輯領域",
"screenRealmDiscovery": "發現領域",
"screenRealmNew": "新建領域",
"screenNotification": "通知",
"screenPostSearch": "搜索帖子",
@ -138,9 +139,12 @@
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
"writePostTypeStory": "發動態",
"writePostTypeArticle": "寫文章",
"writePostTypeQuestion": "提問題",
"writePostTypeVideo": "發視頻",
"fieldPostPublisher": "帖子發佈者",
"fieldPostContent": "發生什麼事了?!",
"fieldPostTitle": "標題",
"fieldPostQuestionReward": "回答獎勵源點",
"fieldPostDescription": "描述",
"fieldPostTags": "標籤",
"fieldPostCategories": "分類",
@ -607,5 +611,18 @@
"other": "{} 源點"
},
"aiThinkingProcess": "AI 思考過程",
"accountSettingsApplied": "帳號設置已應用。"
"accountSettingsApplied": "帳號設置已應用。",
"trayMenuExit": "退出",
"postQuestionUnanswered": "未解答的問題",
"postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
"postQuestionAnswered": "已解答的問題",
"postQuestionAnswerTitle": "精選解答",
"postQuestionAnswerSelect": "選擇解答",
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
"postVideoUpload": "上傳視頻",
"realmJoin": "加入領域",
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
"realmJoined": "已加入領域 {}。",
"join": "加入"
}

View File

@ -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):
@ -43,58 +42,58 @@ PODS:
- Flutter
- file_saver (0.0.1):
- Flutter
- Firebase/Analytics (11.6.0):
- Firebase/Analytics (11.7.0):
- Firebase/Core
- Firebase/Core (11.6.0):
- Firebase/Core (11.7.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 11.6.0)
- Firebase/CoreOnly (11.6.0):
- FirebaseCore (~> 11.6.0)
- Firebase/Messaging (11.6.0):
- FirebaseAnalytics (~> 11.7.0)
- Firebase/CoreOnly (11.7.0):
- FirebaseCore (~> 11.7.0)
- Firebase/Messaging (11.7.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.6.0)
- firebase_analytics (11.4.1):
- Firebase/Analytics (= 11.6.0)
- FirebaseMessaging (~> 11.7.0)
- firebase_analytics (11.4.2):
- Firebase/Analytics (= 11.7.0)
- firebase_core
- Flutter
- firebase_core (3.10.1):
- Firebase/CoreOnly (= 11.6.0)
- firebase_core (3.11.0):
- Firebase/CoreOnly (= 11.7.0)
- Flutter
- firebase_messaging (15.2.1):
- Firebase/Messaging (= 11.6.0)
- firebase_messaging (15.2.2):
- Firebase/Messaging (= 11.7.0)
- firebase_core
- Flutter
- FirebaseAnalytics (11.6.0):
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
- FirebaseCore (~> 11.6.0)
- FirebaseAnalytics (11.7.0):
- FirebaseAnalytics/AdIdSupport (= 11.7.0)
- FirebaseCore (~> 11.7.0)
- FirebaseInstallations (~> 11.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/AdIdSupport (11.6.0):
- FirebaseCore (~> 11.6.0)
- FirebaseAnalytics/AdIdSupport (11.7.0):
- FirebaseCore (~> 11.7.0)
- FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.6.0)
- GoogleAppMeasurement (= 11.7.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseCore (11.6.0):
- FirebaseCoreInternal (~> 11.6.0)
- FirebaseCore (11.7.0):
- FirebaseCoreInternal (~> 11.7.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreInternal (11.6.0):
- FirebaseCoreInternal (11.7.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseInstallations (11.6.0):
- FirebaseCore (~> 11.6.0)
- FirebaseInstallations (11.7.0):
- FirebaseCore (~> 11.7.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.6.0):
- FirebaseCore (~> 11.6.0)
- FirebaseMessaging (11.7.0):
- FirebaseCore (~> 11.7.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -123,21 +122,21 @@ PODS:
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleAppMeasurement (11.6.0):
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
- GoogleAppMeasurement (11.7.0):
- GoogleAppMeasurement/AdIdSupport (= 11.7.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/AdIdSupport (11.6.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
- GoogleAppMeasurement/AdIdSupport (11.7.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
- GoogleAppMeasurement/WithoutAdIdSupport (11.7.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
@ -179,8 +178,8 @@ PODS:
- Flutter
- in_app_review (2.0.0):
- Flutter
- Kingfisher (8.1.3)
- livekit_client (2.3.5):
- Kingfisher (8.2.0)
- 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,22 +373,22 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e
firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b
firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4
firebase_analytics: 7236e6115c1b4e62c2270faa29c052a317e31107
firebase_core: aa979ae726f00b3ef4ccf59dfb96170af84efbd4
firebase_messaging: 3af84b6a90aeac4d7a67fbf4c43a91e7083bea1f
FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec
FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4
FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881
FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9
FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
@ -397,14 +396,14 @@ SPEC CHECKSUMS:
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
livekit_client: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e

View File

@ -71,7 +71,7 @@ class ChatMessageController extends ChangeNotifier {
resp.data as Map<String, dynamic>,
);
_wsSubscription = _ws.stream.stream.listen((event) {
_wsSubscription = _ws.pk.stream.listen((event) {
switch (event.method) {
case 'events.new':
if (event.payload?['channel_id'] != channel?.id) break;

View File

@ -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';
@ -144,6 +145,8 @@ class PostWriteController extends ChangeNotifier {
static const Map<String, String> kTitleMap = {
'stories': 'writePostTypeStory',
'articles': 'writePostTypeArticle',
'questions': 'writePostTypeQuestion',
'videos': 'writePostTypeVideo',
};
static const kAttachmentProgressWeight = 0.9;
@ -153,6 +156,7 @@ class PostWriteController extends ChangeNotifier {
final TextEditingController titleController = TextEditingController();
final TextEditingController descriptionController = TextEditingController();
final TextEditingController aliasController = TextEditingController();
final TextEditingController rewardController = TextEditingController();
bool _temporarySaveActive = false;
@ -168,6 +172,7 @@ class PostWriteController extends ChangeNotifier {
});
contentController.addListener(() {
_temporaryPlanSave();
notifyListeners();
});
if (doLoadFromTemporary) _temporaryLoad();
}
@ -194,6 +199,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, {
@ -214,6 +221,8 @@ class PostWriteController extends ChangeNotifier {
descriptionController.text = post.body['description'] ?? '';
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);
@ -222,6 +231,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);
@ -347,6 +357,7 @@ class PostWriteController extends ChangeNotifier {
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
if (titleController.text.isNotEmpty) 'title': titleController.text,
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
if (rewardController.text.isNotEmpty) 'reward': rewardController.text,
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
'attachments':
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
@ -359,6 +370,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(),
}),
);
});
@ -375,6 +387,7 @@ class PostWriteController extends ChangeNotifier {
aliasController.text = data['alias'] ?? '';
titleController.text = data['title'] ?? '';
descriptionController.text = data['description'] ?? '';
rewardController.text = data['reward']?.toString() ?? '';
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
attachments
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
@ -387,6 +400,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();
});
@ -473,6 +487,8 @@ class PostWriteController extends ChangeNotifier {
progress = kAttachmentProgressWeight;
notifyListeners();
final reward = double.tryParse(rewardController.text);
// Posting the content
try {
final baseProgressVal = progress!;
@ -498,6 +514,9 @@ class PostWriteController extends ChangeNotifier {
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
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);
@ -624,6 +643,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;

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:ui';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:croppy/croppy.dart';
@ -10,8 +11,10 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
@ -40,6 +43,7 @@ import 'package:surface/types/chat.dart';
import 'package:surface/types/realm.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
import 'package:surface/widgets/dialog.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:version/version.dart';
import 'package:workmanager/workmanager.dart';
import 'package:in_app_review/in_app_review.dart';
@ -206,7 +210,7 @@ class _AppSplashScreen extends StatefulWidget {
State<_AppSplashScreen> createState() => _AppSplashScreenState();
}
class _AppSplashScreenState extends State<_AppSplashScreen> {
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
void _tryRequestRating() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('first_boot_time')) {
@ -281,6 +285,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
final notify = context.read<NotificationProvider>();
notify.listen();
await notify.registerPushNotifications();
if (!mounted) return;
final sticker = context.read<SnStickerProvider>();
await sticker.listStickerEagerly();
} catch (err) {
if (!mounted) return;
await context.showErrorDialog(err);
@ -291,9 +298,62 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
await widgetUpdateRandomPost();
}
Future<void> _hotkeyInitialization() async {
if (kIsWeb) return;
if (Platform.isMacOS) {
HotKey quitHotKey = HotKey(
key: PhysicalKeyboardKey.keyQ,
modifiers: [HotKeyModifier.meta],
scope: HotKeyScope.inapp,
);
await hotKeyManager.register(quitHotKey, keyUpHandler: (_) {
_appLifecycleListener?.dispose();
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
});
}
}
Future<void> _trayInitialization() async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png';
final appVersion = await PackageInfo.fromPlatform();
trayManager.addListener(this);
await trayManager.setIcon(icon);
Menu menu = Menu(
items: [
MenuItem(
key: 'version_label',
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
disabled: true,
),
MenuItem.separator(),
MenuItem(
key: 'exit',
label: 'trayMenuExit'.tr(),
),
],
);
await trayManager.setContextMenu(menu);
}
AppLifecycleListener? _appLifecycleListener;
@override
void initState() {
super.initState();
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
_appLifecycleListener = AppLifecycleListener(
onExitRequested: _onExitRequested,
);
}
_trayInitialization();
_hotkeyInitialization();
_initialize().then((_) {
_postInitialization();
_tryRequestRating();
@ -301,6 +361,50 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
});
}
Future<AppExitResponse> _onExitRequested() async {
appWindow.hide();
return AppExitResponse.cancel;
}
@override
void onTrayIconMouseDown() {
if (Platform.isWindows) {
context.read<NotificationProvider>().clearTray();
appWindow.show();
} else {
trayManager.popUpContextMenu();
}
}
@override
void onTrayIconRightMouseDown() {
if (Platform.isWindows) {
trayManager.popUpContextMenu();
} else {
context.read<NotificationProvider>().clearTray();
appWindow.show();
}
}
@override
void onTrayMenuItemClick(MenuItem menuItem) {
switch (menuItem.key) {
case 'exit':
_appLifecycleListener?.dispose();
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
break;
}
}
@override
void dispose() {
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
trayManager.removeListener(this);
hotKeyManager.unregisterAll();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>();

View File

@ -12,6 +12,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/types/notification.dart';
import 'package:tray_manager/tray_manager.dart';
class NotificationProvider extends ChangeNotifier {
late final SnNetworkProvider _sn;
@ -72,28 +73,47 @@ class NotificationProvider extends ChangeNotifier {
}
int showingCount = 0;
int showingTrayCount = 0;
List<SnNotification> notifications = List.empty(growable: true);
void listen() {
_ws.stream.stream.listen((event) {
_ws.pk.stream.listen((event) {
if (event.method == 'notifications.new') {
final notification = SnNotification.fromJson(event.payload!);
if (showingCount < 0) showingCount = 0;
showingCount++;
showingTrayCount++;
notifications.add(notification);
Future.delayed(const Duration(seconds: 3), () {
if (showingCount >= 0) showingCount--;
notifyListeners();
});
notifyListeners();
updateTray();
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.mediumImpact();
}
});
}
void clearTray() {
showingTrayCount = 0;
updateTray();
}
void updateTray() {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
if (showingTrayCount == 0) {
trayManager.setTitle('');
} else {
trayManager.setTitle(' $showingTrayCount');
}
}
void clear() {
showingCount = 0;
notifications.clear();
updateTray();
notifyListeners();
}
}

View File

@ -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,
),
);

View File

@ -9,6 +9,10 @@ class SnStickerProvider {
late final SnNetworkProvider _sn;
final Map<String, SnSticker?> _cache = {};
final Map<int, List<SnSticker>> stickersByPack = {};
List<SnSticker> get stickers => _cache.values.where((ele) => ele != null).cast<SnSticker>().toList();
SnStickerProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
}
@ -17,6 +21,12 @@ class SnStickerProvider {
return _cache.containsKey(alias) && _cache[alias] == null;
}
void _cacheSticker(SnSticker sticker) {
_cache['${sticker.pack.prefix}:${sticker.alias}'] = sticker;
if (stickersByPack[sticker.pack.id] == null) stickersByPack[sticker.pack.id] = List.empty(growable: true);
if (!stickersByPack[sticker.pack.id]!.contains(sticker)) stickersByPack[sticker.pack.id]!.add(sticker);
}
Future<SnSticker?> lookupSticker(String alias) async {
if (_cache.containsKey(alias)) {
return _cache[alias];
@ -25,7 +35,7 @@ class SnStickerProvider {
try {
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
final sticker = SnSticker.fromJson(resp.data);
_cache[alias] = sticker;
_cacheSticker(sticker);
return sticker;
} catch (err) {
@ -35,4 +45,30 @@ class SnStickerProvider {
return null;
}
Future<void> listStickerEagerly() async {
var count = await listSticker();
for (var page = 1; count > 0; count -= 10) {
await listSticker(page: page);
page++;
}
}
Future<int> listSticker({int page = 0}) async {
try {
final resp = await _sn.client.get('/cgi/uc/stickers', queryParameters: {
'take': 10,
'offset': page * 10,
});
final data = resp.data;
final stickers = List.from(data['data']).map((ele) => SnSticker.fromJson(ele));
for (final sticker in stickers) {
_cacheSticker(sticker);
}
return data['count'] as int;
} catch (err) {
log('[Sticker] Failed to list stickers: $err');
rethrow;
}
}
}

View File

@ -18,7 +18,8 @@ class WebSocketProvider extends ChangeNotifier {
late final SnNetworkProvider _sn;
late final UserProvider _ua;
StreamController<WebSocketPackage> stream = StreamController.broadcast();
StreamController<WebSocketPackage> pk = StreamController.broadcast();
Stream<dynamic>? _wsStream;
WebSocketProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
@ -33,23 +34,33 @@ class WebSocketProvider extends ChangeNotifier {
await connect();
}
Completer<void>? _connectCompleter;
Future<void> connect({noRetry = false}) async {
if (_connectCompleter != null) {
await _connectCompleter!.future;
_connectCompleter = null;
}
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();
listen();
log('[WebSocket] Connected to server!');
isConnected = true;
@ -70,6 +81,8 @@ class WebSocketProvider extends ChangeNotifier {
} finally {
isBusy = false;
notifyListeners();
_connectCompleter!.complete();
_connectCompleter = null;
}
}
@ -83,11 +96,12 @@ class WebSocketProvider extends ChangeNotifier {
}
void listen() {
conn?.stream.listen(
if (_wsStream == null) return;
_wsStream!.listen(
(event) {
final packet = WebSocketPackage.fromJson(jsonDecode(event));
log('Websocket incoming message: ${packet.method} ${packet.message}');
stream.sink.add(packet);
pk.sink.add(packet);
},
onDone: () {
isConnected = false;

View File

@ -47,6 +47,7 @@ class HomeWidgetProvider {
}
Future<void> widgetUpdateRandomPost() async {
if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) return;
final snc = await SnNetworkProvider.createOffContextClient();
final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1');
final post = SnPost.fromJson(resp.data['data'][0]);

View File

@ -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: [

View File

@ -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: [

View File

@ -155,12 +155,16 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
text: TextSpan(children: [
TextSpan(
text: 'call'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
const TextSpan(text: '\n'),
TextSpan(
text: call.lastDuration.toString(),
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
]),
),

View File

@ -10,8 +10,10 @@ import 'package:surface/providers/channel.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/account.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_select.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
@ -20,6 +22,7 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class ChannelDetailScreen extends StatefulWidget {
final String scope;
final String alias;
const ChannelDetailScreen({
super.key,
required this.scope,
@ -55,8 +58,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client
.get('/cgi/im/channels/${_channel!.keyPath}/members/me');
final resp = await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/members/me');
_profile = SnChannelMember.fromJson(resp.data);
_notifyLevel = _profile!.notify;
if (!mounted) return;
@ -143,6 +145,25 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
}
}
Future<void> _addMember(SnAccount related) async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post(
'/cgi/im/channels/${_channel!.keyPath}/members',
data: {'related': related.name},
);
if (!mounted) return;
context.showSnackbar('channelMemberAdded'.tr());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
void _showChannelProfileDetail() {
showDialog(
context: context,
@ -166,13 +187,16 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
);
}
void _showMemberAdd() {
showModalBottomSheet(
void _showMemberAdd() async {
final user = await showModalBottomSheet<SnAccount?>(
context: context,
builder: (context) => _NewChannelMemberWidget(
channel: _channel!,
builder: (context) => AccountSelect(
title: 'channelMemberAdd'.tr(),
),
);
if (!mounted) return;
if (user == null) return;
_addMember(user);
}
@override
@ -221,11 +245,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('channelDetailPersonalRegion')
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
Text('channelDetailPersonalRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
ListTile(
leading: const Icon(Symbols.notifications),
trailing: DropdownButtonHideUnderline(
@ -264,8 +284,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
),
ListTile(
leading: AccountImage(
content:
ud.getAccountFromCache(_profile!.accountId)?.avatar,
content: ud.getAccountFromCache(_profile!.accountId)?.avatar,
radius: 18,
),
trailing: const Icon(Symbols.chevron_right),
@ -284,8 +303,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
trailing: const Icon(Symbols.chevron_right),
title: Text('channelActionLeave').tr(),
subtitle: Text('channelActionLeaveDescription').tr(),
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: _leaveChannel,
),
],
@ -293,11 +311,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('channelDetailMemberRegion')
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
Text('channelDetailMemberRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
ListTile(
leading: const Icon(Symbols.group),
trailing: const Icon(Symbols.chevron_right),
@ -319,11 +333,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('channelDetailAdminRegion')
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
Text('channelDetailAdminRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
ListTile(
leading: const Icon(Symbols.edit),
trailing: const Icon(Symbols.chevron_right),
@ -362,18 +372,17 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
class _ChannelProfileDetailDialog extends StatefulWidget {
final SnChannel channel;
final SnChannelMember current;
const _ChannelProfileDetailDialog({
required this.channel,
required this.current,
});
@override
State<_ChannelProfileDetailDialog> createState() =>
_ChannelProfileDetailDialogState();
State<_ChannelProfileDetailDialog> createState() => _ChannelProfileDetailDialogState();
}
class _ChannelProfileDetailDialogState
extends State<_ChannelProfileDetailDialog> {
class _ChannelProfileDetailDialogState extends State<_ChannelProfileDetailDialog> {
bool _isBusy = false;
final TextEditingController _nickController = TextEditingController();
@ -444,11 +453,11 @@ class _ChannelProfileDetailDialogState
class _ChannelMemberListWidget extends StatefulWidget {
final SnChannel channel;
const _ChannelMemberListWidget({required this.channel});
@override
State<_ChannelMemberListWidget> createState() =>
_ChannelMemberListWidgetState();
State<_ChannelMemberListWidget> createState() => _ChannelMemberListWidgetState();
}
class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
@ -463,12 +472,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
try {
final ud = context.read<UserDirectoryProvider>();
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get(
'/cgi/im/channels/${widget.channel.keyPath}/members',
queryParameters: {
'take': 10,
'offset': 0,
});
final resp = await sn.client.get('/cgi/im/channels/${widget.channel.keyPath}/members', queryParameters: {
'take': 10,
'offset': 0,
});
final out = List<SnChannelMember>.from(
resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
);
@ -526,9 +533,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
children: [
const Icon(Symbols.group, size: 24),
const Gap(16),
Text('channelMemberManage')
.tr()
.textStyle(Theme.of(context).textTheme.titleLarge!),
Text('channelMemberManage').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Expanded(
@ -539,8 +544,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
},
child: InfiniteList(
itemCount: _members.length,
hasReachedMax:
_totalCount != null && _members.length >= _totalCount!,
hasReachedMax: _totalCount != null && _members.length >= _totalCount!,
isLoading: _isBusy,
onFetchData: _fetchMembers,
itemBuilder: (context, index) {
@ -551,8 +555,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
content: ud.getAccountFromCache(member.accountId)?.avatar,
),
title: Text(
ud.getAccountFromCache(member.accountId)?.name ??
'unknown'.tr(),
ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(),
),
subtitle: Text(member.nick ?? 'unknown'.tr()),
trailing: SizedBox(
@ -562,8 +565,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed:
_isUpdating ? null : () => _deleteMember(member),
onPressed: _isUpdating ? null : () => _deleteMember(member),
icon: const Icon(Symbols.person_remove),
),
],
@ -578,83 +580,3 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
);
}
}
class _NewChannelMemberWidget extends StatefulWidget {
final SnChannel channel;
const _NewChannelMemberWidget({required this.channel});
@override
State<_NewChannelMemberWidget> createState() =>
_NewChannelMemberWidgetState();
}
class _NewChannelMemberWidgetState extends State<_NewChannelMemberWidget> {
bool _isBusy = false;
final TextEditingController _relatedController = TextEditingController();
Future<void> _performAction() async {
if (_relatedController.text.isEmpty) return;
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post(
'/cgi/im/channels/${widget.channel.keyPath}/members',
data: {
'related': _relatedController.text,
},
);
if (!mounted) return;
Navigator.pop(context, true);
context.showSnackbar('channelMemberAdded'.tr());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void dispose() {
super.dispose();
_relatedController.dispose();
}
@override
Widget build(BuildContext context) {
return StyledWidget(Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'channelMemberAdd',
style: Theme.of(context).textTheme.titleLarge,
).tr(),
const Gap(12),
TextField(
controller: _relatedController,
readOnly: _isBusy,
autocorrect: false,
autofocus: true,
textCapitalization: TextCapitalization.none,
decoration: InputDecoration(
labelText: 'fieldMemberRelatedName'.tr(),
suffix: SizedBox(
height: 24,
child: IconButton(
onPressed: _isBusy ? null : () => _performAction(),
icon: Icon(Symbols.send),
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
),
),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
)
],
)).padding(all: 24);
}
}

View File

@ -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,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
List<SnRealm>? _realms;
SnRealm? _belongToRealm;
SnChannel? _editingChannel;
Future<void> _fetchRealms() async {
setState(() => _isBusy = true);
try {
@ -41,6 +45,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 +55,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
}
}
SnChannel? _editingChannel;
Future<void> _fetchChannel() async {
setState(() => _isBusy = true);
@ -124,9 +129,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 +141,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 +164,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 +182,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 +197,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 +213,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 +229,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 +248,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 +257,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 +268,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
border: const UnderlineInputBorder(),
labelText: 'fieldChatDescription'.tr(),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
Row(

View File

@ -206,7 +206,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
});
final ws = context.read<WebSocketProvider>();
_wsSubscription = ws.stream.stream.listen((event) {
_wsSubscription = ws.pk.stream.listen((event) {
switch (event.method) {
case 'calls.new':
final payload = SnChatCall.fromJson(event.payload!);

View File

@ -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(
@ -166,6 +161,48 @@ class _ExploreScreenState extends State<ExploreScreen> {
),
],
),
Row(
children: [
Text('writePostTypeQuestion').tr(),
const Gap(20),
FloatingActionButton(
heroTag: null,
tooltip: 'writePostTypeQuestion'.tr(),
onPressed: () {
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
'mode': 'questions',
}).then((value) {
if (value == true) {
_refreshPosts();
}
});
_fabKey.currentState!.toggle();
},
child: const Icon(Symbols.question_answer),
),
],
),
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(
@ -224,36 +261,15 @@ class _ExploreScreenState extends State<ExploreScreen> {
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
onFetchData: _fetchPosts,
itemBuilder: (context, idx) {
return Center(
child: OpenContainer(
closedBuilder: (_, __) => Container(
constraints: const BoxConstraints(maxWidth: 640),
child: PostItem(
data: _posts[idx],
maxWidth: 640,
onChanged: (data) {
setState(() => _posts[idx] = data);
},
onDeleted: () {
_refreshPosts();
},
),
),
openBuilder: (_, close) => PostDetailScreen(
slug: _posts[idx].id.toString(),
preload: _posts[idx],
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)),
),
),
return OpenablePostItem(
data: _posts[idx],
maxWidth: 640,
onChanged: (data) {
setState(() => _posts[idx] = data);
},
onDeleted: () {
_refreshPosts();
},
);
},
separatorBuilder: (_, __) => const Gap(8),

View File

@ -6,15 +6,15 @@ import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/relationship.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/account.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_select.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import '../providers/userinfo.dart';
import '../widgets/unauthorized_hint.dart';
import 'package:surface/widgets/unauthorized_hint.dart';
const kFriendStatus = {
0: 'friendStatusPending',
@ -168,6 +168,24 @@ class _FriendScreenState extends State<FriendScreen> {
});
}
Future<void> _sendRequest(SnAccount user) async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/id/users/me/relations', data: {
'related': user.name,
});
if (!mounted) return;
context.showSnackbar('friendRequestSent'.tr());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
@ -199,11 +217,16 @@ class _FriendScreenState extends State<FriendScreen> {
),
floatingActionButton: FloatingActionButton(
child: const Icon(Symbols.add),
onPressed: () {
showModalBottomSheet(
onPressed: () async {
final user = await showModalBottomSheet<SnAccount?>(
context: context,
builder: (context) => _NewFriendWidget(),
builder: (context) => AccountSelect(
title: 'friendNew'.tr(),
),
);
if (!mounted) return;
if (user == null) return;
_sendRequest(user);
},
),
body: Column(
@ -231,8 +254,7 @@ class _FriendScreenState extends State<FriendScreen> {
trailing: const Icon(Symbols.chevron_right),
onTap: _showBlocks,
),
if (_requests.isNotEmpty || _blocks.isNotEmpty)
const Divider(height: 1),
if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1),
Expanded(
child: MediaQuery.removePadding(
context: context,
@ -264,16 +286,12 @@ class _FriendScreenState extends State<FriendScreen> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
InkWell(
onTap: _isUpdating
? null
: () => _changeRelation(relation, 2),
onTap: _isUpdating ? null : () => _changeRelation(relation, 2),
child: Text('friendBlock').tr(),
),
const Gap(8),
InkWell(
onTap: _isUpdating
? null
: () => _deleteRelation(relation),
onTap: _isUpdating ? null : () => _deleteRelation(relation),
child: Text('friendDeleteAction').tr(),
),
],
@ -293,83 +311,9 @@ class _FriendScreenState extends State<FriendScreen> {
}
}
class _NewFriendWidget extends StatefulWidget {
const _NewFriendWidget();
@override
State<_NewFriendWidget> createState() => _NewFriendWidgetState();
}
class _NewFriendWidgetState extends State<_NewFriendWidget> {
bool _isBusy = false;
final TextEditingController _relatedController = TextEditingController();
Future<void> _sendRequest() async {
if (_relatedController.text.isEmpty) return;
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/id/users/me/relations', data: {
'related': _relatedController.text,
});
if (!mounted) return;
Navigator.pop(context, true);
context.showSnackbar('friendRequestSent'.tr());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void dispose() {
super.dispose();
_relatedController.dispose();
}
@override
Widget build(BuildContext context) {
return StyledWidget(Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'friendNew',
style: Theme.of(context).textTheme.titleLarge,
).tr(),
const Gap(12),
TextField(
controller: _relatedController,
readOnly: _isBusy,
autocorrect: false,
autofocus: true,
textCapitalization: TextCapitalization.none,
decoration: InputDecoration(
labelText: 'fieldFriendRelatedName'.tr(),
suffix: SizedBox(
height: 24,
child: IconButton(
onPressed: _isBusy ? null : () => _sendRequest(),
icon: Icon(Symbols.send),
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
),
),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
)
],
)).padding(all: 24);
}
}
class _FriendshipListWidget extends StatefulWidget {
final List<SnRelationship> relations;
const _FriendshipListWidget({required this.relations});
@override
@ -476,9 +420,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(kFriendStatus[relation.status] ?? 'unknown')
.tr()
.opacity(0.75),
Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75),
if (relation.status == 0)
Row(
mainAxisAlignment: MainAxisAlignment.end,
@ -499,8 +441,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
InkWell(
onTap:
_isBusy ? null : () => _changeRelation(relation, 1),
onTap: _isBusy ? null : () => _changeRelation(relation, 1),
child: Text('friendUnblock').tr(),
),
const Gap(8),

View File

@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/notification.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/notification.dart';
import 'package:surface/types/post.dart';
@ -54,6 +55,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
try {
final sn = context.read<SnNetworkProvider>();
final nty = context.read<NotificationProvider>();
final resp = await sn.client.get('/cgi/id/notifications?take=10');
_totalCount = resp.data['count'];
_notifications.addAll(
@ -62,6 +64,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
.cast<SnNotification>() ??
[],
);
nty.updateTray();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -88,9 +91,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
try {
final sn = context.read<SnNetworkProvider>();
final nty = context.read<NotificationProvider>();
final resp = await sn.client.put('/cgi/id/notifications/read/all');
_notifications.clear();
_fetchNotifications();
nty.clear();
if (!mounted) return;
context.showSnackbar(

View File

@ -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,
parentPostId: _data!.id,
maxWidth: 640,
parentPost: _data!,
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)),
],
),
),

File diff suppressed because it is too large Load Diff

View File

@ -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';
@ -134,7 +133,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
body: Stack(
children: [
InfiniteList(
padding: const EdgeInsets.only(top: 100),
padding: const EdgeInsets.only(top: 100 + 8),
itemCount: _posts.length,
isLoading: _isBusy,
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
@ -142,27 +141,18 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
_fetchPosts();
},
itemBuilder: (context, idx) {
return GestureDetector(
child: PostItem(
data: _posts[idx],
maxWidth: 640,
onChanged: (data) {
setState(() => _posts[idx] = data);
},
onDeleted: () {
_refreshPosts();
},
),
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {'slug': _posts[idx].id.toString()},
extra: _posts[idx],
);
return OpenablePostItem(
data: _posts[idx],
maxWidth: 640,
onChanged: (data) {
setState(() => _posts[idx] = data);
},
onDeleted: () {
_refreshPosts();
},
);
},
separatorBuilder: (context, index) => const Divider(height: 1),
separatorBuilder: (_, __) => const Gap(8),
),
Positioned(
top: 16,

View File

@ -287,8 +287,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
Theme(
data: Theme.of(context).copyWith(
appBarTheme: Theme.of(context).appBarTheme.copyWith(
foregroundColor: Colors.white,
),
foregroundColor: Colors.white,
),
),
child: SliverAppBar(
expandedHeight: _appBarHeight,
@ -597,25 +597,16 @@ class _PublisherPostList extends StatelessWidget {
hasReachedMax: postCount != null && posts.length >= postCount!,
onFetchData: fetchPosts,
itemBuilder: (context, idx) {
return GestureDetector(
child: PostItem(
data: posts[idx],
maxWidth: 640,
onChanged: (data) {
onChanged(idx, data);
},
onDeleted: onDeleted,
),
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {'slug': posts[idx].id.toString()},
extra: posts[idx],
);
return OpenablePostItem(
data: posts[idx],
maxWidth: 640,
onChanged: (data) {
onChanged(idx, data);
},
onDeleted: onDeleted,
);
},
separatorBuilder: (context, index) => const Divider(height: 1),
separatorBuilder: (_, __) => const Gap(8),
);
}
}

View File

@ -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: () {

View File

@ -8,9 +8,11 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/account.dart';
import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_select.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -229,13 +231,35 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
}
}
void _showMemberAdd() {
showModalBottomSheet(
Future<void> _addMember(SnAccount related) async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post(
'/cgi/id/realms/${widget.realm!.alias}/members',
data: {'related': related.name},
);
if (!mounted) return;
context.showSnackbar('realmMemberAdded'.tr());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
void _showMemberAdd() async {
final user = await showModalBottomSheet<SnAccount?>(
context: context,
builder: (context) => _NewRealmMemberWidget(
realm: widget.realm!,
builder: (context) => AccountSelect(
title: 'realmMemberAdd'.tr(),
),
);
if (!mounted) return;
if (user == null) return;
_addMember(user);
}
@override
@ -293,85 +317,6 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
}
}
class _NewRealmMemberWidget extends StatefulWidget {
final SnRealm realm;
const _NewRealmMemberWidget({required this.realm});
@override
State<_NewRealmMemberWidget> createState() => _NewRealmMemberWidgetState();
}
class _NewRealmMemberWidgetState extends State<_NewRealmMemberWidget> {
bool _isBusy = false;
final TextEditingController _relatedController = TextEditingController();
Future<void> _performAction() async {
if (_relatedController.text.isEmpty) return;
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post(
'/cgi/id/realms/${widget.realm.alias}/members',
data: {
'related': _relatedController.text,
},
);
if (!mounted) return;
Navigator.pop(context, true);
context.showSnackbar('channelMemberAdded'.tr());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void dispose() {
super.dispose();
_relatedController.dispose();
}
@override
Widget build(BuildContext context) {
return StyledWidget(Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'realmMemberAdd',
style: Theme.of(context).textTheme.titleLarge,
).tr(),
const Gap(12),
TextField(
controller: _relatedController,
readOnly: _isBusy,
autocorrect: false,
autofocus: true,
textCapitalization: TextCapitalization.none,
decoration: InputDecoration(
labelText: 'fieldMemberRelatedName'.tr(),
suffix: SizedBox(
height: 24,
child: IconButton(
onPressed: _isBusy ? null : () => _performAction(),
icon: Icon(Symbols.send),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
),
),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
)
],
)).padding(all: 24);
}
}
class _RealmSettingsWidget extends StatefulWidget {
final SnRealm? realm;
final Function() onUpdate;

View 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}');
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));
}
},
);
},
),
),
],
);
}
}

View File

@ -33,7 +33,7 @@ Future<ThemeData> createAppTheme(
brightness: brightness,
);
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
return ThemeData(
@ -51,9 +51,9 @@ Future<ThemeData> createAppTheme(
),
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: hasAppBarBlurry ? 0 : null,
backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
elevation: hasAppBarTransparent ? 0 : null,
backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
),
pageTransitionsTheme: PageTransitionsTheme(
builders: {

45
lib/types/poll.dart Normal file
View 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
View 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
View 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,
};

View File

@ -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';
@ -37,6 +38,7 @@ class SnPost with _$SnPost {
required int totalUpvote,
required int totalDownvote,
required int publisherId,
required int? pollId,
required SnPublisher publisher,
required SnMetric metric,
SnPostPreload? preload,
@ -89,6 +91,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) =>

View File

@ -48,6 +48,7 @@ mixin _$SnPost {
int get totalUpvote => throw _privateConstructorUsedError;
int get totalDownvote => 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;
@ -95,6 +96,7 @@ abstract class $SnPostCopyWith<$Res> {
int totalUpvote,
int totalDownvote,
int publisherId,
int? pollId,
SnPublisher publisher,
SnMetric metric,
SnPostPreload? preload});
@ -149,6 +151,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
Object? totalUpvote = null,
Object? totalDownvote = null,
Object? publisherId = null,
Object? pollId = freezed,
Object? publisher = null,
Object? metric = null,
Object? preload = freezed,
@ -266,6 +269,10 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
? _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
@ -380,6 +387,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
int totalUpvote,
int totalDownvote,
int publisherId,
int? pollId,
SnPublisher publisher,
SnMetric metric,
SnPostPreload? preload});
@ -437,6 +445,7 @@ class __$$SnPostImplCopyWithImpl<$Res>
Object? totalUpvote = null,
Object? totalDownvote = null,
Object? publisherId = null,
Object? pollId = freezed,
Object? publisher = null,
Object? metric = null,
Object? preload = freezed,
@ -554,6 +563,10 @@ class __$$SnPostImplCopyWithImpl<$Res>
? _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
@ -602,6 +615,7 @@ class _$SnPostImpl extends _SnPost {
required this.totalUpvote,
required this.totalDownvote,
required this.publisherId,
required this.pollId,
required this.publisher,
required this.metric,
this.preload})
@ -719,6 +733,8 @@ class _$SnPostImpl extends _SnPost {
@override
final int publisherId;
@override
final int? pollId;
@override
final SnPublisher publisher;
@override
final SnMetric metric;
@ -727,7 +743,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, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)';
}
@override
@ -782,6 +798,7 @@ class _$SnPostImpl extends _SnPost {
other.totalDownvote == totalDownvote) &&
(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) &&
@ -820,6 +837,7 @@ class _$SnPostImpl extends _SnPost {
totalUpvote,
totalDownvote,
publisherId,
pollId,
publisher,
metric,
preload
@ -871,6 +889,7 @@ abstract class _SnPost extends SnPost {
required final int totalUpvote,
required final int totalDownvote,
required final int publisherId,
required final int? pollId,
required final SnPublisher publisher,
required final SnMetric metric,
final SnPostPreload? preload}) = _$SnPostImpl;
@ -935,6 +954,8 @@ abstract class _SnPost extends SnPost {
@override
int get publisherId;
@override
int? get pollId;
@override
SnPublisher get publisher;
@override
SnMetric get metric;
@ -1567,6 +1588,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 +1607,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 +1635,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 +1647,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 +1671,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 +1709,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 +1738,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 +1750,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 +1767,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 +1787,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 +1805,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 +1834,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 +1845,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.

View File

@ -63,6 +63,7 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
totalUpvote: (json['total_upvote'] as num).toInt(),
totalDownvote: (json['total_downvote'] as num).toInt(),
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>),
@ -101,6 +102,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
'total_upvote': instance.totalUpvote,
'total_downvote': instance.totalDownvote,
'publisher_id': instance.publisherId,
'poll_id': instance.pollId,
'publisher': instance.publisher.toJson(),
'metric': instance.metric.toJson(),
'preload': instance.preload?.toJson(),
@ -165,12 +167,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(

View File

@ -1,9 +1,12 @@
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/user_directory.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/account.dart';
import 'package:surface/widgets/account/account_image.dart';
@ -47,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>(),
);
});
}
@ -96,10 +108,14 @@ class _AccountSelectState extends State<AccountSelect> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: Theme.of(context).textTheme.headlineSmall,
).padding(left: 24, right: 24, top: 16, bottom: 16),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.group, size: 24),
const Gap(16),
Text(widget.title, style: Theme.of(context).textTheme.titleLarge),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Container(
color: Theme.of(context).colorScheme.secondaryContainer,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
@ -117,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),
@ -142,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 {

View File

@ -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(

View File

@ -1,17 +1,28 @@
import 'dart:io';
import 'dart:math' show min;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/post/post_media_pending_list.dart';
import 'package:surface/widgets/universal_image.dart';
class ChatMessageInput extends StatefulWidget {
final ChatMessageController controller;
@ -32,9 +43,30 @@ class ChatMessageInputState extends State<ChatMessageInput> {
final TextEditingController _contentController = TextEditingController();
final FocusNode _focusNode = FocusNode();
final HotKey _pasteHotKey = HotKey(
key: PhysicalKeyboardKey.keyV,
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control],
scope: HotKeyScope.inapp,
);
void _registerHotKey() {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
hotKeyManager.register(_pasteHotKey, keyDownHandler: (_) async {
final imageBytes = await Pasteboard.image;
if (imageBytes == null) return;
_attachments.add(PostWriteMedia.fromBytes(
imageBytes,
'attachmentPastedImage'.tr(),
SnMediaType.image,
));
setState(() {});
});
}
@override
void initState() {
super.initState();
_registerHotKey();
_contentController.addListener(() {
if (_contentController.text.isNotEmpty) {
widget.controller.pingTypingStatus();
@ -144,10 +176,35 @@ class ChatMessageInputState extends State<ChatMessageInput> {
final List<PostWriteMedia> _attachments = List.empty(growable: true);
OverlayEntry? _overlayEntry;
void _showEmojiPicker(BuildContext context) {
final overlay = Overlay.of(context);
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
bottom: 16 + MediaQuery.of(context).padding.bottom,
right: 16,
child: _StickerPicker(
originalText: _contentController.text,
onDismiss: () => _dismissEmojiPicker(),
onInsert: (str) => _contentController.text = str,
),
),
);
overlay.insert(_overlayEntry!);
}
void _dismissEmojiPicker() {
_overlayEntry?.remove();
}
@override
void dispose() {
_contentController.dispose();
_focusNode.dispose();
_dismissEmojiPicker();
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
super.dispose();
}
@ -280,6 +337,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
: 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]),
border: InputBorder.none,
),
textInputAction: TextInputAction.send,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) {
if (_isBusy) return;
@ -289,6 +347,18 @@ class ChatMessageInputState extends State<ChatMessageInput> {
),
),
const Gap(8),
IconButton(
icon: Icon(
Symbols.mood,
color: Theme.of(context).colorScheme.primary,
),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
_showEmojiPicker(context);
},
),
AddPostMediaButton(
onAdd: (items) {
setState(() {
@ -302,10 +372,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(),
),
],
),
@ -314,3 +383,107 @@ class ChatMessageInputState extends State<ChatMessageInput> {
);
}
}
class _StickerPicker extends StatelessWidget {
final String originalText;
final Function? onDismiss;
final Function(String)? onInsert;
const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert});
@override
Widget build(BuildContext context) {
final sticker = context.read<SnStickerProvider>();
return GestureDetector(
onTap: () {
onDismiss?.call();
},
child: Container(
constraints: BoxConstraints(maxWidth: min(360, MediaQuery.of(context).size.width), maxHeight: 240),
child: Material(
elevation: 8,
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: ListView(
padding: EdgeInsets.zero,
children: sticker.stickersByPack.entries
.map((e) {
return <Widget>[
Container(
margin: EdgeInsets.only(bottom: 8),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(e.value.first.pack.name).bold(),
Text(e.value.first.pack.description),
],
),
),
GridView.builder(
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 48,
childAspectRatio: 1.0,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: e.value.length,
itemBuilder: (context, index) {
final sn = context.read<SnNetworkProvider>();
final element = e.value[index];
return GestureDetector(
onTap: () {
final withSpace = originalText.isNotEmpty;
onInsert?.call(
'$originalText${withSpace ? ' ' : ''}:${element.pack.prefix}${element.alias}:');
onDismiss?.call();
},
child: Tooltip(
richMessage: TextSpan(
children: [
TextSpan(
text: ':${element.pack.prefix}${element.alias}:\n',
style: GoogleFonts.robotoMono()),
TextSpan(text: element.name).bold(),
],
),
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: UniversalImage(
sn.getAttachmentUrl(element.attachment.rid),
width: 48,
height: 48,
cacheHeight: 48,
cacheWidth: 48,
fit: BoxFit.contain,
),
),
),
),
);
},
),
];
})
.expand((ele) => ele)
.toList(),
),
),
),
),
);
}
}

View File

@ -129,14 +129,27 @@ class MarkdownTextContent extends StatelessWidget {
future: st.lookupSticker(alias),
builder: (context, snapshot) {
if (snapshot.hasData) {
return UniversalImage(
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
fit: BoxFit.cover,
width: size,
height: size,
cacheHeight: size,
cacheWidth: size,
);
return GestureDetector(
child: UniversalImage(
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
fit: BoxFit.contain,
width: size,
height: size,
cacheHeight: size,
cacheWidth: size,
),
onTap: () {
if (snapshot.data == null) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: [snapshot.data!.attachment],
initialIndex: 0,
heroTags: [const Uuid().v4()],
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
});
}
return const SizedBox.shrink();
},
@ -145,7 +158,7 @@ class MarkdownTextContent extends StatelessWidget {
);
case 'attachments':
final attachment = attachments?.firstWhere(
(ele) => ele?.rid == segments[1],
(ele) => ele?.rid == segments[1],
orElse: () => null,
);
if (attachment != null) {

View File

@ -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)),

View File

@ -4,21 +4,69 @@ 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';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/post/post_mini_editor.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class PostCommentSliverList extends StatefulWidget {
final int parentPostId;
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;
final Function(SnPost)? onSelectAnswer;
const PostCommentSliverList({
super.key,
required this.parentPostId,
required this.parentPost,
this.maxWidth,
this.onSelectAnswer,
});
@override
@ -37,7 +85,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
setState(() => _isBusy = true);
final pt = context.read<SnPostContentProvider>();
final result = await pt.listPostReplies(widget.parentPostId);
final result = await pt.listPostReplies(widget.parentPost.id);
final List<SnPost> out = result.$1;
if (!mounted) return;
@ -48,8 +96,24 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
if (mounted) setState(() => _isBusy = false);
}
Future<void> _selectAnswer(SnPost answer) async {
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
'publisher': answer.publisherId,
'answer_id': answer.id,
});
if (!mounted) return;
await refresh();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
Future<void> refresh() async {
_posts.clear();
_postCount = null;
_fetchPosts();
}
@ -71,6 +135,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
child: PostItem(
data: _posts[idx],
maxWidth: widget.maxWidth,
onSelectAnswer: widget.parentPost.type == 'question' ? () => _selectAnswer(_posts[idx]) : null,
onChanged: (data) {
setState(() => _posts[idx] = data);
},
@ -94,11 +159,12 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
}
class PostCommentListPopup extends StatefulWidget {
final int postId;
final SnPost post;
final int commentCount;
const PostCommentListPopup({
super.key,
required this.postId,
required this.post,
this.commentCount = 0,
});
@ -122,9 +188,7 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
children: [
const Icon(Symbols.comment, size: 24),
const Gap(16),
Text('postCommentsDetailed')
.plural(widget.commentCount)
.textStyle(Theme.of(context).textTheme.titleLarge!),
Text('postCommentsDetailed').plural(widget.commentCount).textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Expanded(
@ -143,7 +207,7 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
),
),
child: PostMiniEditor(
postReplyId: widget.postId,
postReplyId: widget.post.id,
onPost: () {
_childListKey.currentState!.refresh();
},
@ -151,8 +215,8 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
),
),
PostCommentSliverList(
parentPost: widget.post,
key: _childListKey,
parentPostId: widget.postId,
),
],
),

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:animations/animations.dart';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart';
@ -22,10 +23,12 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/post/post_detail.dart';
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';
@ -33,11 +36,73 @@ 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';
import 'package:xml/xml.dart';
class OpenablePostItem extends StatelessWidget {
final SnPost data;
final bool showReactions;
final bool showComments;
final bool showMenu;
final bool showFullPost;
final double? maxWidth;
final Function(SnPost data)? onChanged;
final Function()? onDeleted;
final Function()? onSelectAnswer;
const OpenablePostItem({
super.key,
required this.data,
this.showReactions = true,
this.showComments = true,
this.showMenu = true,
this.showFullPost = false,
this.maxWidth,
this.onChanged,
this.onDeleted,
this.onSelectAnswer,
});
@override
Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>();
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,
),
),
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)),
),
),
);
}
}
class PostItem extends StatelessWidget {
final SnPost data;
final bool showReactions;
@ -47,6 +112,7 @@ class PostItem extends StatelessWidget {
final double? maxWidth;
final Function(SnPost data)? onChanged;
final Function()? onDeleted;
final Function()? onSelectAnswer;
const PostItem({
super.key,
@ -58,6 +124,7 @@ class PostItem extends StatelessWidget {
this.maxWidth,
this.onChanged,
this.onDeleted,
this.onSelectAnswer,
});
void _onChanged(SnPost data) {
@ -129,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(
@ -142,10 +260,12 @@ class PostItem extends StatelessWidget {
isRelativeDate: !showFullPost,
onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context),
onSelectAnswer: onSelectAnswer,
onDeleted: () {
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),
@ -224,10 +344,13 @@ class PostItem extends StatelessWidget {
showMenu: showMenu,
onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context),
onSelectAnswer: onSelectAnswer,
onDeleted: () {
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(
data: data,
@ -267,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'],
@ -333,6 +457,7 @@ class PostShareImageWidget extends StatelessWidget {
showMenu: false,
isRelativeDate: false,
).padding(horizontal: 16, bottom: 8),
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
_PostHeadline(
data: data,
isEnlarge: data.type == 'article',
@ -438,6 +563,30 @@ class PostShareImageWidget extends StatelessWidget {
}
}
class _PostQuestionHint extends StatelessWidget {
final SnPost data;
const _PostQuestionHint({required this.data});
@override
Widget build(BuildContext context) {
return Row(
children: [
Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20),
const Gap(4),
if (data.body['answer'] == null && data.body['reward']?.toDouble() != null)
Text('postQuestionUnansweredWithReward'.tr(args: [
'${data.body['reward']}',
])).opacity(0.75)
else if (data.body['answer'] == null)
Text('postQuestionUnanswered'.tr()).opacity(0.75)
else
Text('postQuestionAnswered'.tr()).opacity(0.75),
],
).opacity(0.75);
}
}
class _PostBottomAction extends StatelessWidget {
final SnPost data;
final bool showComments;
@ -529,7 +678,7 @@ class _PostBottomAction extends StatelessWidget {
context: context,
useRootNavigator: true,
builder: (context) => PostCommentListPopup(
postId: data.id,
post: data,
commentCount: data.metric.replyCount,
),
);
@ -652,6 +801,7 @@ class _PostContentHeader extends StatelessWidget {
final bool showMenu;
final Function onDeleted;
final Function() onShare, onShareImage;
final Function()? onSelectAnswer;
const _PostContentHeader({
required this.data,
@ -662,6 +812,7 @@ class _PostContentHeader extends StatelessWidget {
required this.onDeleted,
required this.onShare,
required this.onShareImage,
this.onSelectAnswer,
});
Future<void> _deletePost(BuildContext context) async {
@ -760,6 +911,20 @@ class _PostContentHeader extends StatelessWidget {
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
if (isAuthor && onSelectAnswer != null)
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.check_circle),
const Gap(16),
Text('postQuestionAnswerSelect').tr(),
],
),
onTap: () {
onSelectAnswer?.call();
},
),
if (isAuthor && onSelectAnswer != null) PopupMenuDivider(),
if (isAuthor)
PopupMenuItem(
child: Row(
@ -833,7 +998,7 @@ class _PostContentHeader extends StatelessWidget {
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => _PostGetInsightSheet(postId: data.id),
builder: (context) => _PostGetInsightPopup(postId: data.id),
);
},
),
@ -1139,8 +1304,18 @@ class _PostFeaturedComment extends StatefulWidget {
class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
SnPost? _featuredComment;
bool _isAnswer = false;
Future<void> _fetchComments() async {
// If this is a answered question, fetch the answer instead
if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
_isAnswer = true;
setState(() => _featuredComment = SnPost.fromJson(resp.data));
return;
}
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
@ -1166,13 +1341,15 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
if (widget.data.metric.replyCount == 0) return const SizedBox.shrink();
if (_featuredComment == null) return const SizedBox.shrink();
final sn = context.read<SnNetworkProvider>();
return AnimateWidgetExtensions(Container(
constraints: BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
margin: const EdgeInsets.only(top: 8),
width: double.infinity,
child: Material(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
color: _isAnswer ? Colors.green.withOpacity(0.5) : Theme.of(context).colorScheme.surfaceContainerHigh,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
@ -1180,7 +1357,7 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
context: context,
useRootNavigator: true,
builder: (context) => PostCommentListPopup(
postId: widget.data.id,
post: widget.data,
commentCount: widget.data.metric.replyCount,
),
);
@ -1188,7 +1365,18 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('postFeaturedComment', style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 16)).tr(),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Gap(2),
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, size: 20),
const Gap(10),
Text(
_isAnswer ? 'postQuestionAnswerTitle' : 'postFeaturedComment',
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 15),
).tr(),
],
),
const Gap(4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
@ -1196,7 +1384,7 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
CircleAvatar(
radius: 12,
backgroundImage: UniversalImage.provider(
_featuredComment!.publisher.avatar,
sn.getAttachmentUrl(_featuredComment!.publisher.avatar),
),
),
const Gap(8),
@ -1292,16 +1480,16 @@ class _PostAbuseReportDialogState extends State<_PostAbuseReportDialog> {
}
}
class _PostGetInsightSheet extends StatefulWidget {
class _PostGetInsightPopup extends StatefulWidget {
final int postId;
const _PostGetInsightSheet({required this.postId});
const _PostGetInsightPopup({required this.postId});
@override
State<_PostGetInsightSheet> createState() => _PostGetInsightSheetState();
State<_PostGetInsightPopup> createState() => _PostGetInsightPopupState();
}
class _PostGetInsightSheetState extends State<_PostGetInsightSheet> {
class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
String? _response;
String? _thinkingProcess;
@ -1314,8 +1502,14 @@ class _PostGetInsightSheetState extends State<_PostGetInsightSheet> {
receiveTimeout: const Duration(minutes: 10),
));
final out = resp.data['response'] as String;
final document = XmlDocument.parse(out);
_thinkingProcess = document.getElement('think')?.innerText.trim();
try {
final document = XmlDocument.parse(out);
_thinkingProcess = document.getElement('think')?.innerText.trim();
} catch (_) {
// ignore
}
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
} catch (err) {
@ -1384,3 +1578,29 @@ class _PostGetInsightSheetState extends State<_PostGetInsightSheet> {
);
}
}
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}'),
),
),
);
}
}

View File

@ -95,6 +95,7 @@ class PostMediaPendingList extends StatelessWidget {
context: context,
builder: (context) => AttachmentInputDialog(
title: 'attachmentSetThumbnail'.tr(),
pool: 'interactive',
analyzeNow: true,
),
);
@ -292,7 +293,7 @@ class PostMediaPendingList extends StatelessWidget {
constraints: const BoxConstraints(maxHeight: 120),
child: Row(
children: [
const Gap(8),
const Gap(16),
if (thumbnail != null)
ContextMenuArea(
contextMenu: _createContextMenu(context, -1, thumbnail!),
@ -337,15 +338,10 @@ class _PostMediaPendingItem extends StatelessWidget {
final sn = context.read<SnNetworkProvider>();
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.surfaceContainer,
),
return Material(
elevation: 4,
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(8),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Row(

View File

@ -19,6 +19,7 @@ const Map<int, String> kPostVisibilityLevel = {
class PostMetaEditor extends StatelessWidget {
final PostWriteController controller;
const PostMetaEditor({super.key, required this.controller});
Future<DateTime?> _selectDate(
@ -87,28 +88,6 @@ class PostMetaEditor extends StatelessWidget {
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
child: Column(
children: [
TextField(
controller: controller.titleController,
decoration: InputDecoration(
labelText: 'fieldPostTitle'.tr(),
border: UnderlineInputBorder(),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 24),
if (controller.mode == 'articles') const Gap(4),
if (controller.mode == 'articles')
TextField(
controller: controller.descriptionController,
maxLines: null,
decoration: InputDecoration(
labelText: 'fieldPostDescription'.tr(),
border: UnderlineInputBorder(),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 24),
const Gap(4),
PostTagsField(
initialTags: controller.tags,
labelText: 'fieldPostTags'.tr(),
@ -133,8 +112,7 @@ class PostMetaEditor extends StatelessWidget {
helperMaxLines: 2,
border: UnderlineInputBorder(),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 24),
const Gap(12),
ListTile(
@ -182,8 +160,7 @@ class PostMetaEditor extends StatelessWidget {
leading: Icon(Symbols.person),
trailing: Icon(Symbols.chevron_right),
title: Text('postVisibleUsers').tr(),
subtitle: Text('postSelectedUsers')
.plural(controller.visibleUsers.length),
subtitle: Text('postSelectedUsers').plural(controller.visibleUsers.length),
onTap: () {
_selectVisibleUser(context);
},
@ -194,8 +171,7 @@ class PostMetaEditor extends StatelessWidget {
leading: Icon(Symbols.person),
trailing: Icon(Symbols.chevron_right),
title: Text('postInvisibleUsers').tr(),
subtitle: Text('postSelectedUsers')
.plural(controller.invisibleUsers.length),
subtitle: Text('postSelectedUsers').plural(controller.invisibleUsers.length),
onTap: () {
_selectInvisibleUser(context);
},
@ -204,9 +180,7 @@ class PostMetaEditor extends StatelessWidget {
leading: const Icon(Symbols.event_available),
title: Text('postPublishedAt').tr(),
subtitle: Text(
controller.publishedAt != null
? dateFormatter.format(controller.publishedAt!)
: 'unset'.tr(),
controller.publishedAt != null ? dateFormatter.format(controller.publishedAt!) : 'unset'.tr(),
),
trailing: controller.publishedAt != null
? IconButton(
@ -230,9 +204,7 @@ class PostMetaEditor extends StatelessWidget {
leading: const Icon(Symbols.event_busy),
title: Text('postPublishedUntil').tr(),
subtitle: Text(
controller.publishedUntil != null
? dateFormatter.format(controller.publishedUntil!)
: 'unset'.tr(),
controller.publishedUntil != null ? dateFormatter.format(controller.publishedUntil!) : 'unset'.tr(),
),
trailing: controller.publishedUntil != null
? IconButton(

View 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),
),
],
)
],
),
);
}
}

View 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()),
),
],
);
}
}

View File

@ -11,9 +11,11 @@
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_udid/flutter_udid_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <hotkey_manager_linux/hotkey_manager_linux_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
#include <pasteboard/pasteboard_plugin.h>
#include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
@ -32,6 +34,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar);
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
@ -41,6 +46,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@ -8,9 +8,11 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_udid
flutter_webrtc
hotkey_manager_linux
media_kit_libs_linux
media_kit_video
pasteboard
tray_manager
url_launcher_linux
)

View File

@ -18,6 +18,7 @@ import flutter_inappwebview_macos
import flutter_udid
import flutter_webrtc
import gal
import hotkey_manager_macos
import in_app_review
import livekit_client
import media_kit_libs_macos_video
@ -29,6 +30,7 @@ import screen_brightness_macos
import share_plus
import shared_preferences_foundation
import sqflite_darwin
import tray_manager
import url_launcher_macos
import video_compress
import wakelock_plus
@ -47,6 +49,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
@ -58,6 +61,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))

View File

@ -2,7 +2,6 @@ PODS:
- bitsdojo_window_macos (0.0.1):
- FlutterMacOS
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- croppy (0.0.1):
- FlutterMacOS
@ -14,59 +13,59 @@ PODS:
- FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- Firebase/Analytics (11.6.0):
- Firebase/Analytics (11.7.0):
- Firebase/Core
- Firebase/Core (11.6.0):
- Firebase/Core (11.7.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 11.6.0)
- Firebase/CoreOnly (11.6.0):
- FirebaseCore (~> 11.6.0)
- Firebase/Messaging (11.6.0):
- FirebaseAnalytics (~> 11.7.0)
- Firebase/CoreOnly (11.7.0):
- FirebaseCore (~> 11.7.0)
- Firebase/Messaging (11.7.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.6.0)
- firebase_analytics (11.4.1):
- Firebase/Analytics (= 11.6.0)
- FirebaseMessaging (~> 11.7.0)
- firebase_analytics (11.4.2):
- Firebase/Analytics (= 11.7.0)
- firebase_core
- FlutterMacOS
- firebase_core (3.10.1):
- Firebase/CoreOnly (~> 11.6.0)
- firebase_core (3.11.0):
- Firebase/CoreOnly (~> 11.7.0)
- FlutterMacOS
- firebase_messaging (15.2.1):
- Firebase/CoreOnly (~> 11.6.0)
- Firebase/Messaging (~> 11.6.0)
- firebase_messaging (15.2.2):
- Firebase/CoreOnly (~> 11.7.0)
- Firebase/Messaging (~> 11.7.0)
- firebase_core
- FlutterMacOS
- FirebaseAnalytics (11.6.0):
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
- FirebaseCore (~> 11.6.0)
- FirebaseAnalytics (11.7.0):
- FirebaseAnalytics/AdIdSupport (= 11.7.0)
- FirebaseCore (~> 11.7.0)
- FirebaseInstallations (~> 11.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/AdIdSupport (11.6.0):
- FirebaseCore (~> 11.6.0)
- FirebaseAnalytics/AdIdSupport (11.7.0):
- FirebaseCore (~> 11.7.0)
- FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.6.0)
- GoogleAppMeasurement (= 11.7.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseCore (11.6.0):
- FirebaseCoreInternal (~> 11.6.0)
- FirebaseCore (11.7.0):
- FirebaseCoreInternal (~> 11.7.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreInternal (11.6.0):
- FirebaseCoreInternal (11.7.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseInstallations (11.6.0):
- FirebaseCore (~> 11.6.0)
- FirebaseInstallations (11.7.0):
- FirebaseCore (~> 11.7.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.6.0):
- FirebaseCore (~> 11.6.0)
- FirebaseMessaging (11.7.0):
- FirebaseCore (~> 11.7.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -87,21 +86,21 @@ PODS:
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleAppMeasurement (11.6.0):
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
- GoogleAppMeasurement (11.7.0):
- GoogleAppMeasurement/AdIdSupport (= 11.7.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/AdIdSupport (11.6.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
- GoogleAppMeasurement/AdIdSupport (11.7.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
- GoogleAppMeasurement/WithoutAdIdSupport (11.7.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
@ -137,9 +136,13 @@ PODS:
- GoogleUtilities/UserDefaults (8.0.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- HotKey (0.2.1)
- hotkey_manager_macos (0.0.1):
- FlutterMacOS
- HotKey
- in_app_review (2.0.0):
- FlutterMacOS
- livekit_client (2.3.5):
- livekit_client (2.3.6):
- flutter_webrtc
- FlutterMacOS
- WebRTC-SDK (= 125.6422.06)
@ -174,6 +177,8 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- tray_manager (0.0.1):
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
- video_compress (0.3.0):
@ -184,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`)
@ -198,6 +203,7 @@ DEPENDENCIES:
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
@ -210,6 +216,7 @@ DEPENDENCIES:
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
@ -225,6 +232,7 @@ SPEC REPOS:
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
- HotKey
- nanopb
- OrderedSet
- PromisesObjC
@ -235,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:
@ -262,6 +270,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral
gal:
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
hotkey_manager_macos:
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
in_app_review:
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
livekit_client:
@ -286,6 +296,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
tray_manager:
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
video_compress:
@ -295,31 +307,33 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802
croppy: 25a638bd7d05411d8c697f481568f261037694fc
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
firebase_analytics: 91efc58e8e37964469fdd59ad11ba36bc97e75d6
firebase_core: 75e003524565fb5bd80c9960bc5892e8475821cd
firebase_messaging: 082a385eb98b5bb843a566cb30404859c4bd6e25
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4
firebase_analytics: 41d88c024a7756462a803e36236ba74f24cdc2c5
firebase_core: 751d3d919b95d4ae46ab049d0d64d42d4eec086b
firebase_messaging: cc174f19945e9541e140e3cb0118448e59b38c6c
FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec
FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4
FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881
FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9
FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
livekit_client: 0ad107154753a5a76802d2222c040223ad049499
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
@ -334,6 +348,7 @@ SPEC CHECKSUMS:
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: e4f2a7ef31b0ab2c89d2bde35ef3e6e6aff1dce5e66069c6540b0e9cfe33ee6b
sha256: e051259913915ea5bc8fe18664596bea08592fd123930605d562969cd7315fcd
url: "https://pub.dev"
source: hosted
version: "1.3.50"
version: "1.3.51"
_macros:
dependency: transitive
description: dart
@ -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: "3b3ff59c66cbc1577ed0f28d7005b5163555208fb1697a42207424ab8baa27c5"
url: "https://pub.dev"
source: hosted
version: "1.4.10"
version: "1.5.0"
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:
@ -538,34 +546,34 @@ packages:
dependency: "direct main"
description:
name: firebase_analytics
sha256: eac382bbcd5ae78c1d1ce5619d13f5a7424429f4bf55df9e3ad5110da34d1060
sha256: "47428047a0778f72af53a3c7cb5d556e1cb25e2327cc8aa40d544971dc6245b2"
url: "https://pub.dev"
source: hosted
version: "11.4.1"
version: "11.4.2"
firebase_analytics_platform_interface:
dependency: transitive
description:
name: firebase_analytics_platform_interface
sha256: a34db46c367265c4c961626e4b128bfb7b7e50958e7add4c27ba103f5f81b9b0
sha256: "1076f4b041f76143e14878c70f0758f17fe5910c0cd992db9e93bd3c3584512b"
url: "https://pub.dev"
source: hosted
version: "4.3.1"
version: "4.3.2"
firebase_analytics_web:
dependency: transitive
description:
name: firebase_analytics_web
sha256: b6b4cef08e45e4c7d48476d9fc49fe9577081809a59026fe95b1a1b1eea165fa
sha256: "8f6dd64ea6d28b7f5b9e739d183a9e1c7f17027794a3e9aba1879621d42426ef"
url: "https://pub.dev"
source: hosted
version: "0.5.10+7"
version: "0.5.10+8"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: d851c1ca98fd5a4c07c747f8c65dacc2edd84a4d9ac055d32a5f0342529069f5
sha256: "93dc4dd12f9b02c5767f235307f609e61ed9211047132d07f9e02c668f0bfc33"
url: "https://pub.dev"
source: hosted
version: "3.10.1"
version: "3.11.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -578,34 +586,34 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: fbc008cf390d909b823763064b63afefe9f02d8afdb13eb3f485b871afee956b
sha256: "0e13c80f0de8acaa5d0519cbe23c8b4cc138a2d5d508b5755c861bdfc9762678"
url: "https://pub.dev"
source: hosted
version: "2.19.0"
version: "2.20.0"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: e20ea2a0ecf9b0971575ab3ab42a6e285a94e50092c555b090c1a588a81b4d54
sha256: "3dee3b0cbfe719e64773cb7d1cad57c58b2235a8c136f5715fe733a54058c783"
url: "https://pub.dev"
source: hosted
version: "15.2.1"
version: "15.2.2"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: c57a92b5ae1857ef4fe4ae2e73452b44d32e984e15ab8b53415ea1bb514bdabd
sha256: e9ea726b9bb864fc6223bb66422bd9877b9973ae51967754a769b0d01e201c1e
url: "https://pub.dev"
source: hosted
version: "4.6.1"
version: "4.6.2"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "83694a990d8525d6b01039240b97757298369622ca0253ad0ebcfed221bf8ee0"
sha256: "5f7b40e8bf861a37f8b8196e347d8a919750421a45f0b45d1bb74e98fa72726e"
url: "https://pub.dev"
source: hosted
version: "3.10.1"
version: "3.10.2"
fixnum:
dependency: transitive
description:
@ -764,10 +772,10 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487
sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5
url: "https://pub.dev"
source: hosted
version: "0.7.5"
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,50 @@ 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:
name: hotkey_manager
sha256: "06f0655b76c8dd322fb7101dc615afbdbf39c3d3414df9e059c33892104479cd"
url: "https://pub.dev"
source: hosted
version: "0.2.3"
hotkey_manager_linux:
dependency: transitive
description:
name: hotkey_manager_linux
sha256: "83676bda8210a3377bc6f1977f193bc1dbdd4c46f1bdd02875f44b6eff9a8473"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
hotkey_manager_macos:
dependency: transitive
description:
name: hotkey_manager_macos
sha256: "03b5967e64357b9ac05188ea4a5df6fe4ed4205762cb80aaccf8916ee1713c96"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
hotkey_manager_platform_interface:
dependency: transitive
description:
name: hotkey_manager_platform_interface
sha256: "98ffca25b8cc9081552902747b2942e3bc37855389a4218c9d50ca316b653b13"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
hotkey_manager_windows:
dependency: transitive
description:
name: hotkey_manager_windows
sha256: "0d03ced9fe563ed0b68f0a0e1b22c9ffe26eb8053cb960e401f68a4f070e0117"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
html:
dependency: "direct main"
description:
@ -1150,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:
@ -1206,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:
@ -1282,6 +1330,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.5"
menu_base:
dependency: transitive
description:
name: menu_base
sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
meta:
dependency: transitive
description:
@ -1334,18 +1390,18 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: b15fad91c4d3d1f2b48c053dd41cb82da007c27407dc9ab5f9aa59881d0e39d4
sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
url: "https://pub.dev"
source: hosted
version: "8.1.4"
version: "8.2.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b
sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.1.0"
pasteboard:
dependency: "direct main"
description:
@ -1710,18 +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:
@ -1774,10 +1830,18 @@ 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:
name: shortid
sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb
url: "https://pub.dev"
source: hosted
version: "0.1.2"
sky_engine:
dependency: transitive
description: flutter
@ -1951,6 +2015,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
tray_manager:
dependency: "direct main"
description:
name: tray_manager
sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
typed_data:
dependency: transitive
description:
@ -1959,6 +2031,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uni_platform:
dependency: transitive
description:
name: uni_platform
sha256: e02213a7ee5352212412ca026afd41d269eb00d982faa552f419ffc2debfad84
url: "https://pub.dev"
source: hosted
version: "0.1.3"
universal_io:
dependency: transitive
description:
@ -2059,10 +2139,10 @@ packages:
dependency: transitive
description:
name: vector_graphics
sha256: "7ed22c21d7fdcc88dd6ba7860384af438cd220b251ad65dfc142ab722fabef61"
sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de"
url: "https://pub.dev"
source: hosted
version: "1.1.16"
version: "1.1.18"
vector_graphics_codec:
dependency: transitive
description:
@ -2107,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:
@ -2179,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:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 2.2.2+61
version: 2.3.2+68
environment:
sdk: ^3.5.4
@ -118,6 +118,8 @@ dependencies:
flutter_inappwebview: ^6.1.5
html: ^0.15.5
xml: ^6.5.0
tray_manager: ^0.3.2
hotkey_manager: ^0.2.3
dev_dependencies:
flutter_test:
@ -152,6 +154,8 @@ flutter:
- assets/icon/icon.png
- assets/icon/icon-dark.png
- assets/icon/icon-light-radius.png
- assets/icon/tray-icon.ico
- assets/icon/tray-icon.png
- assets/translations/
# An image asset can refer to one or more resolution-specific "variants", see

View File

@ -15,6 +15,7 @@
#include <flutter_udid/flutter_udid_plugin_c_api.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <gal/gal_plugin_c_api.h>
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
#include <livekit_client/live_kit_plugin.h>
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h>
@ -22,6 +23,7 @@
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_brightness_windows/screen_brightness_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@ -43,6 +45,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
GalPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GalPluginCApi"));
HotkeyManagerWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
LiveKitPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LiveKitPlugin"));
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
@ -57,6 +61,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
TrayManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_udid
flutter_webrtc
gal
hotkey_manager_windows
livekit_client
media_kit_libs_windows_video
media_kit_video
@ -19,6 +20,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows
screen_brightness_windows
share_plus
tray_manager
url_launcher_windows
)