Compare commits

..

69 Commits

Author SHA1 Message Date
5bdd8e94fa 🐛 Bug hotfix launch 2.3.2+72 2025-02-23 18:48:26 +08:00
2a53031c9a 🚀 Relaunch 2.3.2+71 2025-02-23 15:41:18 +08:00
e8bc7261f3 Updater 2025-02-23 15:41:03 +08:00
997934f680 🚀 Launch 2.3.2+71 2025-02-23 14:51:15 +08:00
26e69d6264 Desktop local notification 2025-02-23 14:49:38 +08:00
153eabcbf2 💄 Enlarge emote when there is only one 2025-02-23 14:40:40 +08:00
6d0145c335 💄 Make attachment in chat aligned with message 2025-02-23 14:35:51 +08:00
81a79f9476 Stickers 2025-02-23 14:23:06 +08:00
537f404fe0 Delete sticker 2025-02-23 14:15:32 +08:00
eb29f76b9a Create new sticker to pack 2025-02-23 14:11:45 +08:00
56816dc060 More debug options in settings 2025-02-23 13:20:41 +08:00
899d5f3e5e Sticker page & add sticker 2025-02-23 13:14:16 +08:00
c8c455bb57 🐛 Make sure the send read event triggered before dispose chat message controller 2025-02-23 12:03:17 +08:00
5468fc0748 Two pane chat screen 2025-02-23 11:36:02 +08:00
78516abf2e Chat unread count 2025-02-23 01:49:07 +08:00
0424f98eb5 🐛 Fix title bar on macOS don't centered 2025-02-23 01:06:24 +08:00
2188b8b2e2 💄 Optimize (idk what i did) 2025-02-23 00:50:37 +08:00
0bf614a75c 🔀 Merge pull request '♻️ Use sqlite to replace hive' (#5) from refactor/sqlite into master
Reviewed-on: HyperNet/Surface#5
2025-02-22 12:49:51 +00:00
9f21f744a4 Remove Hive 2025-02-22 20:47:27 +08:00
b94cda6205 🗑️ Remove Hive related code 2025-02-22 20:46:47 +08:00
3c0e4046a4 ♻️ Refactor to replace Hive with Sqlite 2025-02-22 20:43:24 +08:00
338c22a606 Add sqlite3 dependency 2025-02-22 16:22:33 +08:00
25dd895e0d 💄 Optimize category selector 2025-02-22 14:58:20 +08:00
ea9ef9e82a ♻️ Refactored explore page 2025-02-22 14:52:58 +08:00
edd86eda77 Realm posts 2025-02-22 13:13:38 +08:00
671b857a79 💄 Optimize realm
 Post realm post
2025-02-22 12:25:56 +08:00
408fd0f35e 🐛 Bug fixes 2025-02-22 01:33:57 +08:00
30184d08b1 ♻️ Refactor the way to set thumbnail 2025-02-21 21:50:36 +08:00
95f257c47a Merge pull request #7 from I21b/master 2025-02-21 00:16:41 +08:00
92
41297c6712 📃 Add issue templates
en and zh
2025-02-21 00:49:21 +09:00
a8e0ade0c8 Realm Popularity 2025-02-20 23:44:28 +08:00
3338e699c4 💄 Memorable realm view style 2025-02-20 22:09:05 +08:00
e07da3efa5 Sliding window pricing of attachment billing info displaying 2025-02-20 21:19:23 +08:00
4f7f015250 ⬆️ I forgot what did I did last night 2025-02-20 20:41:41 +08:00
2a4c15d0dc 💄 Optimize About page 2025-02-20 20:41:25 +08:00
70ef894ec5 ♻️ Transferable chat channel 2025-02-18 23:34:59 +08:00
bb9179d5f9 🐛 Fix drawer remain when device rotate 2025-02-18 16:52:15 +08:00
e2ecb573a2 🚀 Launch 2.3.2+70 2025-02-18 00:52:07 +08:00
8cb5dff498 🌐 Update Traditional Chinese localization files 2025-02-18 00:43:57 +08:00
a5629975ed Content insert support 2025-02-18 00:43:12 +08:00
972b304969 Flag posts 2025-02-18 00:38:32 +08:00
e8ded55055 🐛 Fix some listing non offset bugs
 Optimize user listing speed
2025-02-18 00:07:48 +08:00
04875eb164 Support notifications with multiple attachments 2025-02-18 00:07:20 +08:00
54a59aa470 💄 Recommendation post indicator 2025-02-17 18:53:46 +08:00
365f330629 🐛 Realm related bug fixes 2025-02-16 19:50:34 +08:00
a7829d15b2 🐛 Remove android predictive back 2025-02-16 13:29:41 +08:00
a3868a4281 📝 Update README.md 2025-02-16 01:09:45 +08:00
1d1d61d60c Merge pull request #1 from I21b/master 2025-02-15 23:40:05 +08:00
03c2491587 🔨 Add debian build scripts 2025-02-15 23:04:47 +08:00
2c1adc988c 🐛 Fix desktop window title 2025-02-15 22:25:23 +08:00
c0fbee55e4 🐛 Fix linux running issue 2025-02-15 21:22:22 +08:00
6e544c0b6c 🚀 Launch 2.3.2+69 2025-02-15 19:58:11 +08:00
7d56c5ef31 🐛 Fix reply / forward post type will follow the target post 2025-02-15 19:45:33 +08:00
c2df1af16d Reply & repost indicator 2025-02-15 19:43:41 +08:00
a8143c6453 🐛 Fix publisher list did not update after created 2025-02-15 19:23:02 +08:00
04065061e0 Leave realm 2025-02-15 19:20:34 +08:00
226eb452e5 Community and public chat, realm 2025-02-15 19:16:54 +08:00
a6715b0872 Chat return new line 2025-02-15 19:08:40 +08:00
43e3404dbb Delete publisher 2025-02-15 18:43:06 +08:00
c91cf7c813 🐛 Fix send empty message 2025-02-15 18:12:35 +08:00
92
9cd1cad695 Merge branch 'Solsynth:master' into master 2025-02-15 15:00:07 +09:00
92
dde280833b idk what to say 2025-02-15 14:59:45 +09:00
42ac12b53e 🔨 Fix linux build script 2025-02-15 13:48:25 +08:00
63567bf708 🔨 Fix linux build missing deps 2025-02-15 13:43:21 +08:00
5d3cadefef 🔨 Add linux build pipeline 2025-02-15 13:39:00 +08:00
251fbb2503 🐛 Try to fix github action build error 2025-02-15 13:34:23 +08:00
0b31d32217 💄 Fix some designs issue
🐛 Fix web some pages error
2025-02-15 13:06:25 +08:00
5ddd4fed2e 🐛 Fix missing new publisher button 2025-02-15 01:17:09 +08:00
48b6d5f6c1 🐛 Fix poll 2025-02-15 00:16:06 +08:00
104 changed files with 20165 additions and 2115 deletions

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

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

View File

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

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

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

View File

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

View File

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

View File

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

27
README.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -333,6 +333,7 @@
"addAttachmentFromRandomId": "Link via RID",
"attachmentDetailInfo": "Attachment details",
"attachmentPastedImage": "Pasted Image",
"attachmentInsertedImage": "Inserted Image",
"attachmentInsertLink": "Insert Link",
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
@ -419,7 +420,7 @@
"callMessageEnded": "Call lasted {}",
"callMessageStarted": "Call started",
"dailyCheckIn": "Check In",
"dailyCheckInNone": "You haven't checked in today",
"dailyCheckInNone": "You haven't divined today",
"dailyCheckAction": "Check in right now!",
"dailyCheckDetail": "Can't understand the symbol? Master, help me understand it!",
"dailyCheckDetailTitle": "{}'s fortune details",
@ -547,6 +548,7 @@
"termAcceptNextWithAgree": "By clicking the \"Next\", it means you agree to our terms and its updates.",
"unauthorized": "Unauthorized",
"unauthorizedDescription": "Login to explore the entire Solar Network.",
"projectDetail": "Project Details",
"serviceStatus": "Service Status",
"termRelated": "Related Terms",
"appDetails": "App Details",
@ -582,6 +584,7 @@
"colorSchemeBlack": "Black",
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
"postFeaturedComment": "Featured Comment",
"postCategory": "Category",
"postCategoryTechnology": "Technology",
"postCategoryGaming": "Gaming",
"postCategoryLife": "Life",
@ -624,6 +627,7 @@
"realmJoin": "Join Realm",
"realmCommunityHint": "This realm is a community realm, you can freely join.",
"realmCommunityPublicChannelsHint": "The public channels in this realm",
"realmCommunityPublishersHint": "The publishers in this realm",
"realmJoined": "Joined realm {}.",
"join": "Join",
"pollEditorNew": "New Poll",
@ -638,5 +642,84 @@
"pollVotes": {
"one": "{} vote",
"other": "{} votes"
}
},
"publisherDelete": "Delete Publisher {}",
"publisherDeleteDescription": "Are you sure you want to delete this publisher? This operation is irreversible.",
"channelIsPublic": "Public Channel",
"channelIsPublicDescription": "The channel is public, anyone can join.",
"channelIsCommunity": "Community Channel",
"channelIsCommunityDescription": "Currently, community channel has nothing special yet.",
"realmIsPublic": "Public Realm",
"realmIsPublicDescription": "The realm is public, anyone can join.",
"realmIsCommunity": "Community Realm",
"realmIsCommunityDescription": "Community realm will be displayed on the discover page.",
"realmLeave": "Leave Realm",
"realmLeaveDescription": "Leave the current realm and delete the realm's identity.",
"checkInResultTier1": "Worst",
"checkInResultTier2": "Worse",
"checkInResultTier3": "Normal",
"checkInResultTier4": "Better",
"checkInResultTier5": "Best",
"flagPostAction": "Flag the Post",
"flagPost": "Flag objectionable content",
"flagPostDescription": "If flagged users takes 50% or more of the views, the post will be collapsed. You cannot revoke the action.",
"flaggedPost": "Post has been flagged.",
"postViews": {
"zero": "No views",
"one": "{} view",
"other": "{} views"
},
"attachmentBillingUploaded": "Used space",
"attachmentBillingDiscount": "Free space",
"attachmentBillingRatio": "Usage",
"attachmentBillingHint": "Sliding Window Pricing®\nFees will only apply if the size of the file uploaded within 24 hours exceeds the free space.",
"postThumbnail": "Post Thumbnail",
"accountRealms": "Realms",
"postInGlobal": "Global",
"postInGlobalDescription": "Do not link this post with any realm.",
"postChannelGlobal": "Global",
"postChannelFriends": "Friends",
"postChannelFollowing": "Following",
"postChannelRealm": "Realms",
"postFilterReset": "Reset Filter",
"postFilterResetDescription": "Clear filter and show all posts.",
"postFilterWithCategory": "Viewing posts in {}",
"databaseSize": "Database Size",
"databaseDelete": "Delete Database",
"databaseDeleteDescription": "Remove the database on your local disk, the content will be fetched from server again.",
"databaseDeleted": "The local database has been deleted.",
"settingsEnablePushNotifications": "Enable Push Notifications",
"settingsEnablePushNotificationsDescription": "Re-enable and request permission to receive push notifications. Just in case it didn't run automatically.",
"settingsEnabledPushNotifications": "Push notification has been enabled.",
"screenStickers": "Stickers",
"stickersDiscovery": "Discovery",
"stickersOwned": "Owned",
"stickersCreated": "Created",
"stickersAdd": "Add Sticker Pack",
"stickersAdded": "Sticker pack has been added.",
"add": "Add",
"stickersRemoved": "Sticker pack has been removed, you can add it again anytime.",
"stickersReload": "Reload Stickers",
"stickersReloadDescription": "Reload stickers from the server, update the sticker picker.",
"stickersReloaded": "Sticker packs has been reloaded.",
"stickersPackDelete": "Delete Pack {}",
"stickersPackDeleteDescription": "Are you sure you want to delete this sticker pack? This operation is irreversible.",
"stickersPackDeleted": "Sticker pack has been deleted.",
"stickersDelete": "Delete Sticker {}",
"stickersDeleteDescription": "Are you sure you want to delete this sticker? This operation is irreversible.",
"stickersDeleted": "Sticker has been deleted.",
"fieldStickerName": "Sticker Name",
"fieldStickerAlias": "Sticker Alias",
"fieldStickerAliasHint": "The unique sticker placeholder with the pack prefix.",
"fieldStickerPackName": "Name",
"fieldStickerPackDescription": "Description",
"fieldStickerPackPrefix": "Prefix",
"fieldStickerAttachment": "Attachment",
"stickersNew": "New Sticker",
"stickersNewDescription": "Create a new sticker belongs to this pack.",
"stickersPackNew": "New Sticker Pack",
"trayMenuShow": "Show",
"update": "Update",
"forceUpdate": "Force Update",
"forceUpdateDescription": "Force to show the application update popup, even the new version is not available."
}

View File

@ -331,6 +331,7 @@
"addAttachmentFromRandomId": "通过访问 ID 链接",
"attachmentDetailInfo": "附件详细信息",
"attachmentPastedImage": "粘贴的图片",
"attachmentInsertedImage": "插入的图片",
"attachmentInsertLink": "插入连接",
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
@ -545,6 +546,7 @@
"termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。",
"unauthorized": "未登陆",
"unauthorizedDescription": "登陆以探索整个 Solar Network。",
"projectDetail": "项目详情",
"serviceStatus": "服务状态",
"termRelated": "相关条款",
"appDetails": "应用程序详情",
@ -580,6 +582,7 @@
"colorSchemeBlack": "黑色",
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
"postFeaturedComment": "精选评论",
"postCategory": "分类",
"postCategoryTechnology": "技术",
"postCategoryGaming": "游戏",
"postCategoryLife": "生活",
@ -623,6 +626,7 @@
"realmJoin": "加入领域",
"realmCommunityHint": "该领域是一个社区领域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "该领域包含的公共频道",
"realmCommunityPublishersHint": "该领域的发布者",
"realmJoined": "已加入领域 {}。",
"join": "加入",
"pollEditorNew": "新投票",
@ -637,5 +641,83 @@
"pollVotes": {
"one": "{} 票",
"other": "{} 票"
}
},
"publisherDelete": "删除发布者 {}",
"publisherDeleteDescription": "你确定要删除这个发布者吗?该操作不可撤销。",
"channelIsPublic": "公开频道",
"channelIsPublicDescription": "该频道是公开的,任何人都可以加入。",
"channelIsCommunity": "社区频道",
"channelIsCommunityDescription": "目前来说,社区频道还没有什么特别之处。",
"realmIsPublic": "公开领域",
"realmIsPublicDescription": "该领域是公开的,任何人都可以加入。",
"realmIsCommunity": "社区领域",
"realmIsCommunityDescription": "社区领域会显示在发现页面上。",
"realmLeave": "离开领域",
"realmLeaveDescription": "离开当前领域,并且删除领域中的身份。",
"checkInResultTier1": "大凶",
"checkInResultTier2": "凶",
"checkInResultTier3": "中平",
"checkInResultTier4": "吉",
"checkInResultTier5": "大吉",
"flagPostAction": "吹哨",
"flagPost": "吹哨不良内容",
"flagPostDescription": "吹哨不良内容,如果吹哨用户占浏览量的 50% 或以上,则帖子会被折叠。吹哨后不可撤销。",
"flaggedPost": "哨子已经吹响。",
"postViews": {
"zero": "{} 次浏览",
"one": "{} 次浏览",
"other": "{} 次浏览"
},
"attachmentBillingUploaded": "已占用的字节数",
"attachmentBillingDiscount": "免费的字节数",
"attachmentBillingHint": "滑动窗口计价®\n在24小时内上传的文件大小超出免费空间才会适用扣费。",
"postThumbnail": "帖子缩略图",
"accountRealms": "领域",
"postInGlobal": "全站",
"postInGlobalDescription": "不关联此帖子与任何领域。",
"postChannelGlobal": "全站",
"postChannelFriends": "好友",
"postChannelFollowing": "关注",
"postChannelRealm": "领域",
"postFilterReset": "重置过滤器",
"postFilterResetDescription": "清除过滤器并显示所有帖子。",
"postFilterWithCategory": "查看{}区中的帖子",
"databaseSize": "数据库大小",
"databaseDelete": "删除数据库",
"databaseDeleteDescription": "删除本地数据库,内容将从服务器重新获取。",
"databaseDeleted": "本地数据库已被删除。",
"settingsEnablePushNotifications": "启用推送数据",
"settingsEnablePushNotificationsDescription": "重新启用并请求推送权限,以防自动激活失败。",
"settingsEnabledPushNotifications": "推送通知已经注册。",
"screenStickers": "贴图",
"stickersDiscovery": "发现",
"stickersOwned": "由我拥有",
"stickersCreated": "由我发布",
"stickersAdd": "添加贴图包",
"stickersAdded": "贴图包已添加。",
"add": "添加",
"stickersRemoved": "贴图包已被移除,你可以随时再次添加回来。",
"stickersReload": "重载贴图包",
"stickersReloadDescription": "从服务器重新加载添加过的贴图,更新贴图选择器。",
"stickersReloaded": "贴图包已重载。",
"stickersPackDelete": "删除贴图包 {}",
"stickersPackDeleteDescription": "你确定要删除这个贴图包吗?这个操作不可撤销。",
"stickersPackDeleted": "贴图包已被删除。",
"stickersDelete": "删除贴图 {}",
"stickersDeleteDescription": "你确定要删除这个贴图吗?这个操作不可撤销。",
"stickersDeleted": "贴图已被删除。",
"fieldStickerName": "贴图名称",
"fieldStickerAlias": "贴图别名",
"fieldStickerAliasHint": "和贴图包前缀组合成为本贴图的唯一占位符。",
"fieldStickerPackName": "名称",
"fieldStickerPackDescription": "描述",
"fieldStickerPackPrefix": "贴图包前缀",
"fieldStickerAttachment": "附件",
"stickersNew": "新建贴图",
"stickersNewDescription": "创建一个新的贴图。",
"stickersPackNew": "新建贴图包",
"trayMenuShow": "显示",
"update": "更新",
"forceUpdate": "强制更新",
"forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。"
}

View File

@ -331,6 +331,7 @@
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
"attachmentDetailInfo": "附件詳細信息",
"attachmentPastedImage": "粘貼的圖片",
"attachmentInsertedImage": "插入的圖片",
"attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
@ -545,6 +546,7 @@
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
"unauthorized": "未登陸",
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
"projectDetail": "項目詳情",
"serviceStatus": "服務狀態",
"termRelated": "相關條款",
"appDetails": "應用程序詳情",
@ -580,6 +582,7 @@
"colorSchemeBlack": "黑色",
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
"postFeaturedComment": "精選評論",
"postCategory": "分類",
"postCategoryTechnology": "技術",
"postCategoryGaming": "遊戲",
"postCategoryLife": "生活",
@ -623,6 +626,98 @@
"realmJoin": "加入領域",
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
"realmCommunityPublishersHint": "該領域的發佈者",
"realmJoined": "已加入領域 {}。",
"join": "加入"
"join": "加入",
"pollEditorNew": "新投票",
"pollEditorEdit": "編輯投票",
"pollEditorDelete": "刪除投票",
"pollEditorDeleteDescription": "你確定要刪除這個投票嗎?該操作不可撤銷。",
"pollEditorUnlink": "解除鏈接",
"pollOptionAdd": "添加選項",
"pollOptionName": "選項名稱",
"pollLinkExisting": "鏈接現有投票",
"pollAnswered": "答案已經反饋。",
"pollVotes": {
"one": "{} 票",
"other": "{} 票"
},
"publisherDelete": "刪除發佈者 {}",
"publisherDeleteDescription": "你確定要刪除這個發佈者嗎?該操作不可撤銷。",
"channelIsPublic": "公開頻道",
"channelIsPublicDescription": "該頻道是公開的,任何人都可以加入。",
"channelIsCommunity": "社區頻道",
"channelIsCommunityDescription": "目前來説,社區頻道還沒有什麼特別之處。",
"realmIsPublic": "公開領域",
"realmIsPublicDescription": "該領域是公開的,任何人都可以加入。",
"realmIsCommunity": "社區領域",
"realmIsCommunityDescription": "社區領域會顯示在發現頁面上。",
"realmLeave": "離開領域",
"realmLeaveDescription": "離開當前領域,並且刪除領域中的身份。",
"checkInResultTier1": "大凶",
"checkInResultTier2": "兇",
"checkInResultTier3": "中平",
"checkInResultTier4": "吉",
"checkInResultTier5": "大吉",
"flagPostAction": "吹哨",
"flagPost": "吹哨不良內容",
"flagPostDescription": "吹哨不良內容,如果吹哨用户佔瀏覽量的 50% 或以上,則帖子會被摺疊。吹哨後不可撤銷。",
"flaggedPost": "哨子已經吹響。",
"postViews": {
"zero": "{} 次瀏覽",
"one": "{} 次瀏覽",
"other": "{} 次瀏覽"
},
"attachmentBillingUploaded": "已佔用的字節數",
"attachmentBillingDiscount": "免費的字節數",
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。",
"postThumbnail": "帖子縮略圖",
"accountRealms": "領域",
"postInGlobal": "全站",
"postInGlobalDescription": "不關聯此帖子與任何領域。",
"postChannelGlobal": "全站",
"postChannelFriends": "好友",
"postChannelFollowing": "關注",
"postChannelRealm": "領域",
"postFilterReset": "重置過濾器",
"postFilterResetDescription": "清除過濾器並顯示所有帖子。",
"postFilterWithCategory": "查看{}區中的帖子",
"databaseSize": "數據庫大小",
"databaseDelete": "刪除數據庫",
"databaseDeleteDescription": "刪除本地數據庫,內容將從服務器重新獲取。",
"databaseDeleted": "本地數據庫已被刪除。",
"settingsEnablePushNotifications": "啓用推送數據",
"settingsEnablePushNotificationsDescription": "重新啓用並請求推送權限,以防自動激活失敗。",
"settingsEnabledPushNotifications": "推送通知已經註冊。",
"screenStickers": "貼圖",
"stickersDiscovery": "發現",
"stickersOwned": "由我擁有",
"stickersCreated": "由我發佈",
"stickersAdd": "添加貼圖包",
"stickersAdded": "貼圖包已添加。",
"add": "添加",
"stickersRemoved": "貼圖包已被移除,你可以隨時再次添加回來。",
"stickersReload": "重載貼圖包",
"stickersReloadDescription": "從服務器重新加載添加過的貼圖,更新貼圖選擇器。",
"stickersReloaded": "貼圖包已重載。",
"stickersPackDelete": "刪除貼圖包 {}",
"stickersPackDeleteDescription": "你確定要刪除這個貼圖包嗎?這個操作不可撤銷。",
"stickersPackDeleted": "貼圖包已被刪除。",
"stickersDelete": "刪除貼圖 {}",
"stickersDeleteDescription": "你確定要刪除這個貼圖嗎?這個操作不可撤銷。",
"stickersDeleted": "貼圖已被刪除。",
"fieldStickerName": "貼圖名稱",
"fieldStickerAlias": "貼圖別名",
"fieldStickerAliasHint": "和貼圖包前綴組合成為本貼圖的唯一佔位符。",
"fieldStickerPackName": "名稱",
"fieldStickerPackDescription": "描述",
"fieldStickerPackPrefix": "貼圖包前綴",
"fieldStickerAttachment": "附件",
"stickersNew": "新建貼圖",
"stickersNewDescription": "創建一個新的貼圖。",
"stickersPackNew": "新建貼圖包",
"trayMenuShow": "顯示",
"update": "更新",
"forceUpdate": "強制更新",
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。"
}

View File

@ -331,6 +331,7 @@
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
"attachmentDetailInfo": "附件詳細信息",
"attachmentPastedImage": "粘貼的圖片",
"attachmentInsertedImage": "插入的圖片",
"attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
@ -545,6 +546,7 @@
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
"unauthorized": "未登陸",
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
"projectDetail": "項目詳情",
"serviceStatus": "服務狀態",
"termRelated": "相關條款",
"appDetails": "應用程序詳情",
@ -580,6 +582,7 @@
"colorSchemeBlack": "黑色",
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
"postFeaturedComment": "精選評論",
"postCategory": "分類",
"postCategoryTechnology": "技術",
"postCategoryGaming": "遊戲",
"postCategoryLife": "生活",
@ -623,6 +626,98 @@
"realmJoin": "加入領域",
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
"realmCommunityPublishersHint": "該領域的發佈者",
"realmJoined": "已加入領域 {}。",
"join": "加入"
"join": "加入",
"pollEditorNew": "新投票",
"pollEditorEdit": "編輯投票",
"pollEditorDelete": "刪除投票",
"pollEditorDeleteDescription": "你確定要刪除這個投票嗎?該操作不可撤銷。",
"pollEditorUnlink": "解除鏈接",
"pollOptionAdd": "添加選項",
"pollOptionName": "選項名稱",
"pollLinkExisting": "鏈接現有投票",
"pollAnswered": "答案已經反饋。",
"pollVotes": {
"one": "{} 票",
"other": "{} 票"
},
"publisherDelete": "刪除發佈者 {}",
"publisherDeleteDescription": "你確定要刪除這個發佈者嗎?該操作不可撤銷。",
"channelIsPublic": "公開頻道",
"channelIsPublicDescription": "該頻道是公開的,任何人都可以加入。",
"channelIsCommunity": "社區頻道",
"channelIsCommunityDescription": "目前來說,社區頻道還沒有什麼特別之處。",
"realmIsPublic": "公開領域",
"realmIsPublicDescription": "該領域是公開的,任何人都可以加入。",
"realmIsCommunity": "社區領域",
"realmIsCommunityDescription": "社區領域會顯示在發現頁面上。",
"realmLeave": "離開領域",
"realmLeaveDescription": "離開當前領域,並且刪除領域中的身份。",
"checkInResultTier1": "大凶",
"checkInResultTier2": "兇",
"checkInResultTier3": "中平",
"checkInResultTier4": "吉",
"checkInResultTier5": "大吉",
"flagPostAction": "吹哨",
"flagPost": "吹哨不良內容",
"flagPostDescription": "吹哨不良內容,如果吹哨用戶佔瀏覽量的 50% 或以上,則帖子會被摺疊。吹哨後不可撤銷。",
"flaggedPost": "哨子已經吹響。",
"postViews": {
"zero": "{} 次瀏覽",
"one": "{} 次瀏覽",
"other": "{} 次瀏覽"
},
"attachmentBillingUploaded": "已佔用的字節數",
"attachmentBillingDiscount": "免費的字節數",
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。",
"postThumbnail": "帖子縮略圖",
"accountRealms": "領域",
"postInGlobal": "全站",
"postInGlobalDescription": "不關聯此帖子與任何領域。",
"postChannelGlobal": "全站",
"postChannelFriends": "好友",
"postChannelFollowing": "關注",
"postChannelRealm": "領域",
"postFilterReset": "重置過濾器",
"postFilterResetDescription": "清除過濾器並顯示所有帖子。",
"postFilterWithCategory": "查看{}區中的帖子",
"databaseSize": "數據庫大小",
"databaseDelete": "刪除數據庫",
"databaseDeleteDescription": "刪除本地數據庫,內容將從服務器重新獲取。",
"databaseDeleted": "本地數據庫已被刪除。",
"settingsEnablePushNotifications": "啟用推送數據",
"settingsEnablePushNotificationsDescription": "重新啟用並請求推送權限,以防自動激活失敗。",
"settingsEnabledPushNotifications": "推送通知已經註冊。",
"screenStickers": "貼圖",
"stickersDiscovery": "發現",
"stickersOwned": "由我擁有",
"stickersCreated": "由我發佈",
"stickersAdd": "添加貼圖包",
"stickersAdded": "貼圖包已添加。",
"add": "添加",
"stickersRemoved": "貼圖包已被移除,你可以隨時再次添加回來。",
"stickersReload": "重載貼圖包",
"stickersReloadDescription": "從服務器重新加載添加過的貼圖,更新貼圖選擇器。",
"stickersReloaded": "貼圖包已重載。",
"stickersPackDelete": "刪除貼圖包 {}",
"stickersPackDeleteDescription": "你確定要刪除這個貼圖包嗎?這個操作不可撤銷。",
"stickersPackDeleted": "貼圖包已被刪除。",
"stickersDelete": "刪除貼圖 {}",
"stickersDeleteDescription": "你確定要刪除這個貼圖嗎?這個操作不可撤銷。",
"stickersDeleted": "貼圖已被刪除。",
"fieldStickerName": "貼圖名稱",
"fieldStickerAlias": "貼圖別名",
"fieldStickerAliasHint": "和貼圖包前綴組合成為本貼圖的唯一佔位符。",
"fieldStickerPackName": "名稱",
"fieldStickerPackDescription": "描述",
"fieldStickerPackPrefix": "貼圖包前綴",
"fieldStickerAttachment": "附件",
"stickersNew": "新建貼圖",
"stickersNewDescription": "創建一個新的貼圖。",
"stickersPackNew": "新建貼圖包",
"trayMenuShow": "顯示",
"update": "更新",
"forceUpdate": "強制更新",
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。"
}

14
debian/debian.yml vendored Normal file
View File

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

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

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

23
debian/gui/surface.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 232 KiB

View File

@ -42,58 +42,58 @@ PODS:
- Flutter
- file_saver (0.0.1):
- Flutter
- Firebase/Analytics (11.7.0):
- Firebase/Analytics (11.8.0):
- Firebase/Core
- Firebase/Core (11.7.0):
- Firebase/Core (11.8.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 11.7.0)
- Firebase/CoreOnly (11.7.0):
- FirebaseCore (~> 11.7.0)
- Firebase/Messaging (11.7.0):
- FirebaseAnalytics (~> 11.8.0)
- Firebase/CoreOnly (11.8.0):
- FirebaseCore (~> 11.8.0)
- Firebase/Messaging (11.8.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.7.0)
- firebase_analytics (11.4.2):
- Firebase/Analytics (= 11.7.0)
- FirebaseMessaging (~> 11.8.0)
- firebase_analytics (11.4.3):
- Firebase/Analytics (= 11.8.0)
- firebase_core
- Flutter
- firebase_core (3.11.0):
- Firebase/CoreOnly (= 11.7.0)
- firebase_core (3.12.0):
- Firebase/CoreOnly (= 11.8.0)
- Flutter
- firebase_messaging (15.2.2):
- Firebase/Messaging (= 11.7.0)
- firebase_messaging (15.2.3):
- Firebase/Messaging (= 11.8.0)
- firebase_core
- Flutter
- FirebaseAnalytics (11.7.0):
- FirebaseAnalytics/AdIdSupport (= 11.7.0)
- FirebaseCore (~> 11.7.0)
- FirebaseAnalytics (11.8.0):
- FirebaseAnalytics/AdIdSupport (= 11.8.0)
- FirebaseCore (~> 11.8.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.7.0):
- FirebaseCore (~> 11.7.0)
- FirebaseAnalytics/AdIdSupport (11.8.0):
- FirebaseCore (~> 11.8.0)
- FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.7.0)
- GoogleAppMeasurement (= 11.8.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseCore (11.7.0):
- FirebaseCoreInternal (~> 11.7.0)
- FirebaseCore (11.8.1):
- FirebaseCoreInternal (~> 11.8.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreInternal (11.7.0):
- FirebaseCoreInternal (11.8.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseInstallations (11.7.0):
- FirebaseCore (~> 11.7.0)
- FirebaseInstallations (11.8.0):
- FirebaseCore (~> 11.8.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.7.0):
- FirebaseCore (~> 11.7.0)
- FirebaseMessaging (11.8.0):
- FirebaseCore (~> 11.8.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -122,21 +122,21 @@ PODS:
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleAppMeasurement (11.7.0):
- GoogleAppMeasurement/AdIdSupport (= 11.7.0)
- GoogleAppMeasurement (11.8.0):
- GoogleAppMeasurement/AdIdSupport (= 11.8.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.7.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.0)
- GoogleAppMeasurement/AdIdSupport (11.8.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.8.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.7.0):
- GoogleAppMeasurement/WithoutAdIdSupport (11.8.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
@ -179,7 +179,7 @@ PODS:
- in_app_review (2.0.0):
- Flutter
- Kingfisher (8.2.0)
- livekit_client (2.3.6):
- livekit_client (2.4.0):
- Flutter
- flutter_webrtc
- WebRTC-SDK (= 125.6422.06)
@ -210,9 +210,9 @@ PODS:
- SAMKeychain (1.5.3)
- screen_brightness_ios (0.1.0):
- Flutter
- SDWebImage (5.20.0):
- SDWebImage/Core (= 5.20.0)
- SDWebImage/Core (5.20.0)
- SDWebImage (5.20.1):
- SDWebImage/Core (= 5.20.1)
- SDWebImage/Core (5.20.1)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@ -221,6 +221,25 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.49.1):
- sqlite3/common (= 3.49.1)
- sqlite3/common (3.49.1)
- sqlite3/dbstatvtab (3.49.1):
- sqlite3/common
- sqlite3/fts5 (3.49.1):
- sqlite3/common
- sqlite3/perf-threadsafe (3.49.1):
- sqlite3/common
- sqlite3/rtree (3.49.1):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.49.0)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
- SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1):
- Flutter
@ -268,6 +287,7 @@ DEPENDENCIES:
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_compress (from `.symlinks/plugins/video_compress/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
@ -294,6 +314,7 @@ SPEC REPOS:
- PromisesObjC
- SAMKeychain
- SDWebImage
- sqlite3
- SwiftyGif
- WebRTC-SDK
@ -360,6 +381,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_compress:
@ -380,30 +403,30 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4
firebase_analytics: 7236e6115c1b4e62c2270faa29c052a317e31107
firebase_core: aa979ae726f00b3ef4ccf59dfb96170af84efbd4
firebase_messaging: 3af84b6a90aeac4d7a67fbf4c43a91e7083bea1f
FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec
FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4
FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881
FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9
FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
firebase_analytics: 7ec1166af61987fa968766eb11587c562a5650ee
firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682
firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d
FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
livekit_client: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81
livekit_client: 9819ebc8be8ef00ed0fae7d806bf8938ec689573
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
@ -417,10 +440,12 @@ SPEC CHECKSUMS:
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
SDWebImage: 33d0f23bddeb5d209ae959153883247be6703713
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe

View File

@ -59,6 +59,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

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

View File

@ -15,14 +15,14 @@ struct CheckInProvider: TimelineProvider {
func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) {
let prefs = UserDefaults(suiteName: "group.solsynth.solian")
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
let checkInRaw = prefs?.string(forKey: "pas_check_in_record")
var checkIn: SolarCheckInRecord?
if let checkInRaw = checkInRaw {
@ -31,7 +31,7 @@ struct CheckInProvider: TimelineProvider {
checkIn = nil
}
}
let entry = CheckInEntry(
date: Date(),
checkIn: checkIn
@ -54,11 +54,11 @@ struct CheckInEntry: TimelineEntry {
struct CheckInWidgetEntryView : View {
var entry: CheckInProvider.Entry
private let resultTierSymbols: [String] = ["大凶", "", "中平", "", "大吉"]
private let resultTierSymbols: [String] = ["Bad", "Poor", "Medium", "Good", "Great"]
func checkIn() -> Void {}
func seeDetail() -> Void {}
var body: some View {
@ -68,9 +68,9 @@ struct CheckInWidgetEntryView : View {
Text(resultTierSymbols[checkIn.resultTier]).font(.system(size: 27, weight: .bold))
Text("+\(checkIn.resultExperience) EXP").font(.system(size: 15, design: .monospaced))
}.padding(.horizontal, 4)
Spacer()
HStack {
VStack(alignment: .leading) {
Text(
@ -82,7 +82,7 @@ struct CheckInWidgetEntryView : View {
format: .dateTime.day().month()
).font(.system(size: 13))
}.padding(.leading, 4)
Button("See Detail", systemImage: "arrow.right", action: seeDetail)
.labelStyle(.iconOnly)
.buttonBorderShape(.circle)
@ -91,11 +91,11 @@ struct CheckInWidgetEntryView : View {
} else {
VStack(alignment: .leading) {
Text("Check In").font(.system(size: 19, weight: .bold))
Text("You haven't check in today").font(.system(size: 15))
Text("You haven't divined today").font(.system(size: 15))
}.padding(.horizontal, 4)
Spacer()
HStack(alignment: .bottom) {
Button("Check In", systemImage: "checkmark", action: checkIn).labelStyle(.iconOnly).buttonBorderShape(.circle).frame(maxWidth: .infinity, alignment: .trailing)
}

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,6 @@ 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';
@ -24,6 +23,7 @@ import 'package:surface/firebase_options.dart';
import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/link_preview.dart';
import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/notification.dart';
@ -31,6 +31,7 @@ import 'package:surface/providers/post.dart';
import 'package:surface/providers/relationship.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/special_day.dart';
import 'package:surface/providers/theme.dart';
@ -39,14 +40,15 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/providers/widget.dart';
import 'package:surface/router.dart';
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';
import 'package:image_picker_android/image_picker_android.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:local_notifier/local_notifier.dart';
@pragma('vm:entry-point')
void appBackgroundDispatcher() {
@ -67,20 +69,6 @@ void appBackgroundDispatcher() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
await Hive.initFlutter();
Hive.registerAdapter(SnChannelImplAdapter());
Hive.registerAdapter(SnRealmImplAdapter());
Hive.registerAdapter(SnChannelMemberImplAdapter());
Hive.registerAdapter(SnChatMessageImplAdapter());
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
GoRouter.optionURLReflectsImperativeAPIs = true;
usePathUrlStrategy();
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
doWhenWindowReady(() {
@ -91,6 +79,17 @@ void main() async {
});
}
await EasyLocalization.ensureInitialized();
if (!kIsWeb && !Platform.isLinux) {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
}
GoRouter.optionURLReflectsImperativeAPIs = true;
usePathUrlStrategy();
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Workmanager().initialize(
appBackgroundDispatcher,
@ -107,6 +106,14 @@ void main() async {
}
}
if (!kIsWeb && Platform.isAndroid) {
final ImagePickerPlatform imagePickerImplementation =
ImagePickerPlatform.instance;
if (imagePickerImplementation is ImagePickerAndroid) {
imagePickerImplementation.useAndroidPhotoPicker = true;
}
}
runApp(const SolianApp());
}
@ -129,6 +136,9 @@ class SolianApp extends StatelessWidget {
assetLoader: JsonAssetLoader(),
child: MultiProvider(
providers: [
// Infrastructure layer
Provider(create: (ctx) => DatabaseProvider(ctx)),
// System extensions layer
Provider(create: (ctx) => HomeWidgetProvider(ctx)),
@ -143,6 +153,7 @@ class SolianApp extends StatelessWidget {
Provider(create: (ctx) => SnNetworkProvider(ctx)),
Provider(create: (ctx) => UserDirectoryProvider(ctx)),
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
Provider(create: (ctx) => SnRealmProvider(ctx)),
Provider(create: (ctx) => SnPostContentProvider(ctx)),
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
@ -160,8 +171,8 @@ class SolianApp extends StatelessWidget {
),
),
breakpoints: [
const Breakpoint(start: 0, end: 450, name: MOBILE),
const Breakpoint(start: 451, end: 800, name: TABLET),
const Breakpoint(start: 0, end: 600, name: MOBILE),
const Breakpoint(start: 601, end: 800, name: TABLET),
const Breakpoint(start: 801, end: 1920, name: DESKTOP),
],
);
@ -216,7 +227,8 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
if (prefs.containsKey('first_boot_time')) {
final rawTime = prefs.getString('first_boot_time');
final time = DateTime.tryParse(rawTime ?? '');
if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
if (time != null &&
time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
final inAppReview = InAppReview.instance;
if (prefs.getBool('rating_requested') == true) return;
if (await inAppReview.isAvailable()) {
@ -242,20 +254,26 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
receiveTimeout: const Duration(seconds: 60),
),
).get(
'https://git.solsynth.dev/api/v1/repos/HyperNet/Surface/tags?page=1&limit=1',
'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest',
);
final remoteVersionString = (resp.data as List).firstOrNull?['name'] ?? '0.0.0+0';
final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0';
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
final localVersion = Version.parse(localVersionString.split('+').first);
final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0;
final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0;
final remoteBuildNumber =
int.tryParse(remoteVersionString.split('+').last) ?? 0;
final localBuildNumber =
int.tryParse(localVersionString.split('+').last) ?? 0;
log("[Update] Local: $localVersionString, Remote: $remoteVersionString");
if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) {
if ((remoteVersion > localVersion ||
remoteBuildNumber > localBuildNumber) &&
mounted) {
final config = context.read<ConfigProvider>();
config.setUpdate(remoteVersionString);
config.setUpdate(
remoteVersionString, resp.data?['body'] ?? 'No changelog');
log("[Update] Update available: $remoteVersionString");
}
} catch (e) {
log('[Error] Unable to check update: $e');
if (mounted) context.showErrorDialog('Unable to check update: $e');
}
}
@ -287,7 +305,8 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
await notify.registerPushNotifications();
if (!mounted) return;
final sticker = context.read<SnStickerProvider>();
await sticker.listStickerEagerly();
await sticker.listSticker();
log('[Bootstrap] Everything initialized!');
} catch (err) {
if (!mounted) return;
await context.showErrorDialog(err);
@ -317,7 +336,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
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 icon = Platform.isWindows
? 'assets/icon/tray-icon.ico'
: 'assets/icon/tray-icon.png';
final appVersion = await PackageInfo.fromPlatform();
trayManager.addListener(this);
@ -331,6 +352,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
disabled: true,
),
MenuItem.separator(),
MenuItem(
key: 'window_show',
label: 'trayMenuShow'.tr(),
),
MenuItem(
key: 'exit',
label: 'trayMenuExit'.tr(),
@ -340,6 +365,15 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
await trayManager.setContextMenu(menu);
}
Future<void> _notifyInitialization() async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
await localNotifier.setup(
appName: 'solian',
shortcutPolicy: ShortcutPolicy.requireCreate,
);
}
AppLifecycleListener? _appLifecycleListener;
@override
@ -354,6 +388,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
_trayInitialization();
_hotkeyInitialization();
_notifyInitialization();
_initialize().then((_) {
_postInitialization();
_tryRequestRating();
@ -389,6 +424,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
@override
void onTrayMenuItemClick(MenuItem menuItem) {
switch (menuItem.key) {
case 'window_show':
appWindow.show();
break;
case 'exit':
_appLifecycleListener?.dispose();
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
@ -415,8 +453,16 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
});
return false;
},
child: SizeChangedLayoutNotifier(
child: widget.child,
child: OrientationBuilder(
builder: (context, orientation) {
final cfg = context.read<ConfigProvider>();
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
return SizeChangedLayoutNotifier(
child: widget.child,
);
},
),
);
}

View File

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

View File

@ -17,6 +17,7 @@ const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const kAppExpandPostLink = 'app_expand_post_link';
const kAppExpandChatLink = 'app_expand_chat_link';
const kAppRealmCompactView = 'app_realm_compact_view';
const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none,
@ -45,8 +46,8 @@ class ConfigProvider extends ChangeNotifier {
bool newDrawerIsCollapsed = false;
bool newDrawerIsExpanded = false;
if (withMediaQuery) {
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450;
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451;
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600;
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 601;
} else {
final rpb = ResponsiveBreakpoints.of(context);
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
@ -57,7 +58,8 @@ class ConfigProvider extends ChangeNotifier {
: false;
}
if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
if (newDrawerIsExpanded != drawerIsExpanded ||
newDrawerIsCollapsed != drawerIsCollapsed) {
drawerIsExpanded = newDrawerIsExpanded;
drawerIsCollapsed = newDrawerIsCollapsed;
notifyListeners();
@ -65,22 +67,34 @@ class ConfigProvider extends ChangeNotifier {
}
FilterQuality get imageQuality {
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
return kImageQualityLevel.values
.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ??
FilterQuality.high;
}
String get serverUrl {
return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
}
bool get realmCompactView {
return prefs.getBool(kAppRealmCompactView) ?? false;
}
set realmCompactView(bool value) {
prefs.setBool(kAppRealmCompactView, value);
}
set serverUrl(String url) {
prefs.setString(kNetworkServerStoreKey, url);
_home.saveWidgetData("nex_server_url", url);
}
String? updatableVersion;
String? updatableChangelog;
void setUpdate(String newVersion) {
void setUpdate(String newVersion, String newChangelog) {
updatableVersion = newVersion;
updatableChangelog = newChangelog;
notifyListeners();
}
}

View File

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

View File

@ -63,6 +63,11 @@ class NavigationProvider extends ChangeNotifier {
screen: 'news',
label: 'screenNews',
),
AppNavDestination(
icon: Icon(Symbols.emoji_emotions, weight: 400, opticalSize: 20),
screen: 'stickers',
label: 'screenStickers',
),
AppNavDestination(
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
screen: 'album',
@ -88,7 +93,8 @@ class NavigationProvider extends ChangeNotifier {
List<AppNavDestination> destinations = [];
int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
int get pinnedDestinationCount =>
destinations.where((ele) => ele.isPinned).length;
NavigationProvider() {
buildDestinations(kDefaultPinnedDestination);
@ -117,13 +123,17 @@ class NavigationProvider extends ChangeNotifier {
}
bool isIndexInRange(int min, int max) {
return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
return _currentIndex != null &&
_currentIndex! >= min &&
_currentIndex! < max;
}
void autoDetectIndex(GoRouter? state) {
if (state == null) return;
final idx = destinations.indexWhere(
(ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
(ele) =>
ele.screen ==
state.routerDelegate.currentConfiguration.last.route.name,
);
_currentIndex = idx == -1 ? null : idx;
notifyListeners();

View File

@ -1,11 +1,13 @@
import 'dart:developer';
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:local_notifier/local_notifier.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart';
@ -84,7 +86,7 @@ class NotificationProvider extends ChangeNotifier {
showingCount++;
showingTrayCount++;
notifications.add(notification);
Future.delayed(const Duration(seconds: 3), () {
Future.delayed(const Duration(seconds: 5), () {
if (showingCount >= 0) showingCount--;
notifyListeners();
});
@ -92,6 +94,20 @@ class NotificationProvider extends ChangeNotifier {
updateTray();
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.mediumImpact();
if (!kIsWeb) {
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
LocalNotification notify = LocalNotification(
title: notification.title,
subtitle: notification.subtitle,
body: notification.body,
);
notify.onClick = () {
appWindow.show();
};
notify.show();
}
}
}
});
}

View File

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

View File

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

View File

@ -11,7 +11,8 @@ class SnStickerProvider {
final Map<int, List<SnSticker>> stickersByPack = {};
List<SnSticker> get stickers => _cache.values.where((ele) => ele != null).cast<SnSticker>().toList();
List<SnSticker> get stickers =>
_cache.values.where((ele) => ele != null).cast<SnSticker>().toList();
SnStickerProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
@ -23,8 +24,18 @@ class SnStickerProvider {
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);
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);
}
}
void putSticker(Iterable<SnSticker> sticker) {
for (final ele in sticker) {
_cacheSticker(ele);
}
}
Future<SnSticker?> lookupSticker(String alias) async {
@ -46,26 +57,14 @@ 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 {
Future<void> listSticker() async {
try {
final resp = await _sn.client.get('/cgi/uc/stickers', queryParameters: {
'take': 10,
'offset': page * 10,
});
final resp = await _sn.client.get('/cgi/uc/stickers');
final data = resp.data;
final stickers = List.from(data['data']).map((ele) => SnSticker.fromJson(ele));
final stickers = List.from(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

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

View File

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

View File

@ -34,13 +34,15 @@ 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/stickers.dart';
import 'package:surface/screens/stickers/pack_detail.dart';
import 'package:surface/screens/wallet.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/about.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
Widget _fadeThroughTransition(
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
Widget _fadeThroughTransition(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
@ -82,13 +84,15 @@ final _appRoutes = [
name: 'postSearch',
builder: (context, state) => PostSearchScreen(
initialTags: state.uri.queryParameters['tags']?.split(','),
initialCategories: state.uri.queryParameters['categories']?.split(','),
initialCategories:
state.uri.queryParameters['categories']?.split(','),
),
),
GoRoute(
path: '/publishers/:name',
name: 'postPublisher',
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
builder: (context, state) =>
PostPublisherScreen(name: state.pathParameters['name']!),
),
GoRoute(
path: '/:slug',
@ -100,52 +104,56 @@ final _appRoutes = [
),
],
),
GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
GoRoute(
path: '/wallet',
name: 'accountWallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: '/settings',
name: 'accountSettings',
builder: (context, state) => AccountSettingsScreen(),
),
GoRoute(
path: '/settings/factors',
name: 'factorSettings',
builder: (context, state) => FactorSettingsScreen(),
),
GoRoute(
path: '/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(),
),
GoRoute(
path: '/publishers',
name: 'accountPublishers',
builder: (context, state) => PublisherScreen(),
),
GoRoute(
path: '/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(),
),
GoRoute(
path: '/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen(
name: state.pathParameters['name']!,
),
),
GoRoute(
path: '/:name',
name: 'accountProfilePage',
pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!),
),
),
]),
GoRoute(
path: '/account',
name: 'account',
builder: (context, state) => const AccountScreen(),
routes: [
GoRoute(
path: '/wallet',
name: 'accountWallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: '/settings',
name: 'accountSettings',
builder: (context, state) => AccountSettingsScreen(),
),
GoRoute(
path: '/settings/factors',
name: 'factorSettings',
builder: (context, state) => FactorSettingsScreen(),
),
GoRoute(
path: '/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(),
),
GoRoute(
path: '/publishers',
name: 'accountPublishers',
builder: (context, state) => PublisherScreen(),
),
GoRoute(
path: '/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(),
),
GoRoute(
path: '/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen(
name: state.pathParameters['name']!,
),
),
GoRoute(
path: '/:name',
name: 'accountProfilePage',
pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!),
),
),
]),
GoRoute(
path: '/chat',
name: 'chat',
@ -208,19 +216,39 @@ final _appRoutes = [
GoRoute(
path: '/:alias',
name: 'realmDetail',
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
builder: (context, state) =>
RealmDetailScreen(alias: state.pathParameters['alias']!),
),
],
),
GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
GoRoute(
path: '/:hash',
name: 'newsDetail',
builder: (context, state) => NewsDetailScreen(
hash: state.pathParameters['hash']!,
GoRoute(
path: '/news',
name: 'news',
builder: (context, state) => const NewsScreen(),
routes: [
GoRoute(
path: '/:hash',
name: 'newsDetail',
builder: (context, state) => NewsDetailScreen(
hash: state.pathParameters['hash']!,
),
),
),
]),
],
),
GoRoute(
path: '/stickers',
name: 'stickers',
builder: (context, state) => const StickerScreen(),
routes: [
GoRoute(
path: '/packs/:id',
name: 'stickerPack',
builder: (context, state) => StickerPackScreen(
id: int.tryParse(state.pathParameters['id']!)!,
),
),
],
),
GoRoute(
path: '/album',
name: 'album',

View File

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

View File

@ -4,10 +4,10 @@ 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:hive_flutter/hive_flutter.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart';
@ -45,7 +45,8 @@ class AccountScreen extends StatelessWidget {
? Stack(
fit: StackFit.expand,
children: [
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover),
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner),
fit: BoxFit.cover),
Positioned(
top: 0,
left: 0,
@ -79,7 +80,9 @@ class AccountScreen extends StatelessWidget {
],
),
body: SingleChildScrollView(
child: ua.isAuthorized ? _AuthorizedAccountScreen() : _UnauthorizedAccountScreen(),
child: ua.isAuthorized
? _AuthorizedAccountScreen()
: _UnauthorizedAccountScreen(),
),
);
}
@ -115,12 +118,15 @@ class _AuthorizedAccountScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(ua.user!.nick).textStyle(Theme.of(context).textTheme.titleLarge!),
Text(ua.user!.nick)
.textStyle(Theme.of(context).textTheme.titleLarge!),
const Gap(4),
Text('@${ua.user!.name}').textStyle(Theme.of(context).textTheme.bodySmall!),
Text('@${ua.user!.name}')
.textStyle(Theme.of(context).textTheme.bodySmall!),
],
),
Text(ua.user!.description).textStyle(Theme.of(context).textTheme.bodyMedium!),
Text(ua.user!.description)
.textStyle(Theme.of(context).textTheme.bodyMedium!),
],
),
);
@ -193,8 +199,7 @@ class _AuthorizedAccountScreen extends StatelessWidget {
ua.logoutUser();
final ws = context.read<WebSocketProvider>();
ws.disconnect();
await Hive.deleteFromDisk();
await Hive.initFlutter();
context.read<DatabaseProvider>().removeDatabase();
},
),
],
@ -220,7 +225,9 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
child: Icon(Symbols.waving_hand, size: 28),
),
const Gap(8),
Text('accountIntroTitle').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
Text('accountIntroTitle')
.tr()
.textStyle(Theme.of(context).textTheme.titleLarge!),
Text('accountIntroSubtitle').tr(),
],
).padding(all: 20),

View File

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

View File

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

View File

@ -5,21 +5,23 @@ 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: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/screens/chat/room.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/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:uuid/uuid.dart';
import '../providers/sn_network.dart';
import '../providers/userinfo.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@ -34,8 +36,18 @@ class _ChatScreenState extends State<ChatScreen> {
List<SnChannel>? _channels;
Map<int, SnChatMessage>? _lastMessages;
Map<int, int>? _unreadCounts;
void _refreshChannels() {
Future<void> _fetchWhatsNew() async {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/im/whats-new');
final List<dynamic> out = resp.data;
setState(() {
_unreadCounts = {for (var v in out) v['channel_id']: v['count']};
});
}
void _refreshChannels({bool noRemote = false}) {
final ua = context.read<UserProvider>();
if (!ua.isAuthorized) {
setState(() => _isBusy = false);
@ -43,12 +55,15 @@ class _ChatScreenState extends State<ChatScreen> {
}
final chan = context.read<ChatChannelProvider>();
chan.fetchChannels().listen((channels) async {
chan.fetchChannels(noRemote: noRemote).listen((channels) async {
final lastMessages = await chan.getLastMessages(channels);
_lastMessages = {for (final val in lastMessages) val.channelId: val};
channels.sort((a, b) {
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) {
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt);
if (_lastMessages!.containsKey(a.id) &&
_lastMessages!.containsKey(b.id)) {
return _lastMessages![b.id]!
.createdAt
.compareTo(_lastMessages![a.id]!.createdAt);
}
if (_lastMessages!.containsKey(a.id)) return -1;
if (_lastMessages!.containsKey(b.id)) return 1;
@ -86,7 +101,8 @@ class _ChatScreenState extends State<ChatScreen> {
void _newDirectMessage() async {
final user = await showModalBottomSheet(
context: context,
builder: (context) => AccountSelect(title: 'channelNewDirectMessage'.tr()),
builder: (context) =>
AccountSelect(title: 'channelNewDirectMessage'.tr()),
);
if (user == null) return;
if (!mounted) return;
@ -98,7 +114,8 @@ class _ChatScreenState extends State<ChatScreen> {
await sn.client.post('/cgi/im/channels/global/dm', data: {
'alias': uuid.v4().replaceAll('-', '').substring(0, 12),
'name': 'DM',
'description': 'A direct message channel between @${ua.user?.name} and @${user.name}',
'description':
'A direct message channel between @${ua.user?.name} and @${user.name}',
'related_user': user.id,
});
_fabKey.currentState!.toggle();
@ -109,10 +126,13 @@ class _ChatScreenState extends State<ChatScreen> {
}
}
SnChannel? _focusChannel;
@override
void initState() {
super.initState();
_refreshChannels();
_fetchWhatsNew();
}
@override
@ -132,7 +152,10 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
return AppScaffold(
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
final chatList = AppScaffold(
noBackground: doExpand,
appBar: AppBar(
leading: AutoAppBarLeading(),
title: Text('screenChat').tr(),
@ -144,20 +167,27 @@ class _ChatScreenState extends State<ChatScreen> {
type: ExpandableFabType.up,
childrenAnimation: ExpandableFabAnimation.none,
overlayStyle: ExpandableFabOverlayStyle(
color: Theme.of(context).colorScheme.surface.withAlpha((255 * 0.5).round()),
color: Theme.of(context)
.colorScheme
.surface
.withAlpha((255 * 0.5).round()),
),
openButtonBuilder: RotateFloatingActionButtonBuilder(
child: const Icon(Symbols.add, size: 28),
fabSize: ExpandableFabSize.regular,
foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor,
backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor,
foregroundColor:
Theme.of(context).floatingActionButtonTheme.foregroundColor,
backgroundColor:
Theme.of(context).floatingActionButtonTheme.backgroundColor,
shape: const CircleBorder(),
),
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
child: const Icon(Symbols.close, size: 28),
fabSize: ExpandableFabSize.regular,
foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor,
backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor,
foregroundColor:
Theme.of(context).floatingActionButtonTheme.foregroundColor,
backgroundColor:
Theme.of(context).floatingActionButtonTheme.backgroundColor,
shape: const CircleBorder(),
),
children: [
@ -200,7 +230,10 @@ class _ChatScreenState extends State<ChatScreen> {
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: () => Future.sync(() => _refreshChannels()),
onRefresh: () => Future.wait([
Future.sync(() => _refreshChannels()),
_fetchWhatsNew(),
]),
child: ListView.builder(
itemCount: _channels?.length ?? 0,
itemBuilder: (context, idx) {
@ -208,13 +241,29 @@ class _ChatScreenState extends State<ChatScreen> {
final lastMessage = _lastMessages?[channel.id];
if (channel.type == 1) {
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
(ele) => ele?.accountId != ua.user?.id,
orElse: () => null,
);
final otherMember =
channel.members?.cast<SnChannelMember?>().firstWhere(
(ele) => ele?.accountId != ua.user?.id,
orElse: () => null,
);
return ListTile(
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
title: Row(
children: [
Expanded(
child: Text(ud
.getAccountFromCache(
otherMember?.accountId)
?.nick ??
channel.name),
),
const Gap(8),
if (_unreadCounts?[channel.id] != null)
Badge(
label: Text('${_unreadCounts![channel.id]}'),
),
],
),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
@ -222,17 +271,22 @@ class _ChatScreenState extends State<ChatScreen> {
overflow: TextOverflow.ellipsis,
)
: Text(
'channelDirectMessageDescription'.tr(args: [
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
]),
channel.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
content: ud
.getAccountFromCache(otherMember?.accountId)
?.avatar,
),
onTap: () {
if (doExpand) {
setState(() => _focusChannel = channel);
return;
}
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
@ -240,14 +294,23 @@ class _ChatScreenState extends State<ChatScreen> {
'alias': channel.alias,
},
).then((value) {
if (mounted) _refreshChannels();
if (mounted) _refreshChannels(noRemote: true);
});
},
);
}
return ListTile(
title: Text(channel.name),
title: Row(
children: [
Expanded(child: Text(channel.name)),
const Gap(8),
if (_unreadCounts?[channel.id] != null)
Badge(
label: Text('${_unreadCounts![channel.id]}'),
),
],
),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
@ -259,12 +322,17 @@ class _ChatScreenState extends State<ChatScreen> {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: null,
fallbackWidget: const Icon(Symbols.chat, size: 20),
),
onTap: () {
if (doExpand) {
setState(() => _focusChannel = channel);
return;
}
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
@ -272,7 +340,7 @@ class _ChatScreenState extends State<ChatScreen> {
'alias': channel.alias,
},
).then((value) {
if (value == true) _refreshChannels();
if (value == true) _refreshChannels(noRemote: true);
});
},
);
@ -284,5 +352,27 @@ class _ChatScreenState extends State<ChatScreen> {
],
),
);
if (doExpand) {
return AppBackground(
isRoot: true,
child: Row(
children: [
SizedBox(width: 340, child: chatList),
const VerticalDivider(width: 1),
if (_focusChannel != null)
Expanded(
child: ChatRoomScreen(
key: ValueKey(_focusChannel!.id),
scope: _focusChannel!.realm?.alias ?? 'global',
alias: _focusChannel!.alias,
),
),
],
),
);
}
return chatList;
}
}

View File

@ -58,7 +58,8 @@ 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}/me');
_profile = SnChannelMember.fromJson(resp.data);
_notifyLevel = _profile!.notify;
if (!mounted) return;
@ -104,7 +105,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.delete(
'/cgi/im/channels/${_channel!.realm?.alias ?? 'global'}/${_channel!.id}/members/me',
'/cgi/im/channels/${_channel!.realm?.alias ?? 'global'}/${_channel!.alias}/me',
);
if (!mounted) return;
Navigator.pop(context, false);
@ -245,7 +246,11 @@ 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(
@ -284,7 +289,8 @@ 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),
@ -303,7 +309,8 @@ 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,
),
],
@ -311,7 +318,11 @@ 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),
@ -333,7 +344,11 @@ 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),
@ -379,10 +394,12 @@ class _ChannelProfileDetailDialog extends StatefulWidget {
});
@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();
@ -457,7 +474,8 @@ class _ChannelMemberListWidget extends StatefulWidget {
const _ChannelMemberListWidget({required this.channel});
@override
State<_ChannelMemberListWidget> createState() => _ChannelMemberListWidgetState();
State<_ChannelMemberListWidget> createState() =>
_ChannelMemberListWidgetState();
}
class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
@ -472,10 +490,12 @@ 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': _members.length,
});
final out = List<SnChannelMember>.from(
resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
);
@ -533,7 +553,9 @@ 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(
@ -544,7 +566,8 @@ 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) {
@ -555,7 +578,8 @@ 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(
@ -565,7 +589,8 @@ 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),
),
],

View File

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

View File

@ -39,7 +39,8 @@ class ChatRoomScreen extends StatefulWidget {
final String alias;
final ChatRoomScreenExtra? extra;
const ChatRoomScreen({super.key, required this.scope, required this.alias, this.extra});
const ChatRoomScreen(
{super.key, required this.scope, required this.alias, this.extra});
@override
State<ChatRoomScreen> createState() => _ChatRoomScreenState();
@ -58,6 +59,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
StreamSubscription? _wsSubscription;
// TODO fetch user identity and ask them to join the channel or not
Future<void> _fetchChannel() async {
setState(() => _isBusy = true);
@ -191,10 +193,12 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
WidgetsBinding.instance.addPostFrameCallback((_) {
log('[ChatInput] Setting initial text and attachments...');
if (widget.extra!.initialText != null) {
_inputGlobalKey.currentState?.setInitialText(widget.extra!.initialText!);
_inputGlobalKey.currentState
?.setInitialText(widget.extra!.initialText!);
}
if (widget.extra!.initialAttachments != null) {
_inputGlobalKey.currentState?.setInitialAttachments(widget.extra!.initialAttachments!);
_inputGlobalKey.currentState
?.setInitialAttachments(widget.extra!.initialAttachments!);
}
});
}
@ -240,12 +244,15 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
appBar: AppBar(
title: Text(
_channel?.type == 1
? ud.getAccountFromCache(_otherMember?.accountId)?.nick ?? _channel!.name
? ud.getAccountFromCache(_otherMember?.accountId)?.nick ??
_channel!.name
: _channel?.name ?? 'loading'.tr(),
),
actions: [
IconButton(
icon: _ongoingCall == null ? const Icon(Symbols.call) : const Icon(Symbols.call_end),
icon: _ongoingCall == null
? const Icon(Symbols.call)
: const Icon(Symbols.call_end),
onPressed: _isCalling
? null
: _ongoingCall == null
@ -295,9 +302,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
)
],
),
)
.height(_ongoingCall != null ? 54 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
).height(_ongoingCall != null ? 54 : 0, animate: true).animate(
const Duration(milliseconds: 300),
Curves.fastLinearToSlowEaseIn),
if (_messageController.isPending)
Expanded(
child: const CircularProgressIndicator().center(),
@ -315,6 +322,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
},
itemBuilder: (context, idx) {
final message = _messageController.messages[idx];
_messageController.readEvent(message.id);
bool canMerge = false, canMergePrevious = false;
if (idx > 0) {
@ -336,7 +344,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
data: message,
isMerged: canMerge,
hasMerged: canMergePrevious,
isPending: _messageController.unconfirmedMessages.contains(message.uuid),
isPending: _messageController.unconfirmedMessages
.contains(message.uuid),
onReply: (value) {
_inputGlobalKey.currentState?.setReply(value);
},

View File

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

View File

@ -1,10 +1,8 @@
import 'dart:io';
import 'dart:math' as math;
import 'dart:ui';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_app_update/flutter_app_update.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
@ -29,6 +27,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/updater.dart';
class HomeScreenDashEntry {
final String name;
@ -83,14 +82,20 @@ class _HomeScreenState extends State<HomeScreen> {
body: LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: constraints.maxWidth > 640 ? Alignment.center : Alignment.topCenter,
alignment: constraints.maxWidth > 640
? Alignment.center
: Alignment.topCenter,
child: Container(
constraints: const BoxConstraints(maxWidth: 640),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start,
mainAxisAlignment: constraints.maxWidth > 640
? MainAxisAlignment.center
: MainAxisAlignment.start,
children: [
_HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)),
_HomeDashUpdateWidget(
padding: const EdgeInsets.only(
bottom: 8, left: 8, right: 8)),
_HomeDashSpecialDayWidget().padding(horizontal: 8),
StaggeredGrid.extent(
maxCrossAxisExtent: 280,
@ -131,25 +136,20 @@ class _HomeDashUpdateWidget extends StatelessWidget {
return Container(
padding: padding,
child: Card(
margin: EdgeInsets.zero,
child: ListTile(
leading: Icon(Symbols.update),
title: Text('updateAvailable').tr(),
subtitle: Text(config.updatableVersion!),
trailing: (kIsWeb || Platform.isWindows || Platform.isLinux)
? null
: IconButton(
icon: const Icon(Symbols.arrow_right_alt),
onPressed: () {
final model = UpdateModel(
'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk',
'solian-app-release-${config.updatableVersion!}.apk',
'ic_launcher',
'https://apps.apple.com/us/app/solian/id6499032345',
);
AzhonAppUpdate.update(model);
context.showSnackbar('updateOngoing'.tr());
},
),
trailing: IconButton(
icon: const Icon(Symbols.arrow_right_alt),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) => VersionUpdatePopup(),
);
},
),
),
),
);
@ -165,7 +165,8 @@ class _HomeDashSpecialDayWidget extends StatefulWidget {
const _HomeDashSpecialDayWidget();
@override
State<_HomeDashSpecialDayWidget> createState() => _HomeDashSpecialDayWidgetState();
State<_HomeDashSpecialDayWidget> createState() =>
_HomeDashSpecialDayWidgetState();
}
class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
@ -180,6 +181,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
return Column(
children: days.map((ele) {
return Card(
margin: EdgeInsets.zero,
child: ListTile(
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
@ -203,9 +205,12 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
final diff = nextOne.$2.difference(DateTime.now());
return Card(
margin: EdgeInsets.zero,
child: ListTile(
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
title: Text('pending$name').tr(args: [
RelativeTime(context).format(date).replaceFirst('in', '').trim()
]),
subtitle: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -270,6 +275,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -293,12 +299,19 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
children: [
Text(
_article!.title,
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18),
maxLines: MediaQuery.of(context).size.width >= 640 ? 2 : 1,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 18),
maxLines:
MediaQuery.of(context).size.width >= 640 ? 2 : 1,
overflow: TextOverflow.ellipsis,
),
Text(
parse(_article!.description).children.map((e) => e.text.trim()).join(),
parse(_article!.description)
.children
.map((e) => e.text.trim())
.join(),
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
@ -309,9 +322,13 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 2,
children: [
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
Text(DateFormat().format(date)).textStyle(
Theme.of(context).textTheme.bodySmall!),
Text(' · ')
.textStyle(Theme.of(context).textTheme.bodySmall!)
.bold(),
Text(RelativeTime(context).format(date)).textStyle(
Theme.of(context).textTheme.bodySmall!),
],
).opacity(0.75);
}),
@ -382,15 +399,20 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
}
Widget _buildDetailChunk(int index, bool positive) {
final prefix = positive ? 'dailyCheckPositiveHint' : 'dailyCheckNegativeHint';
final mod = positive ? kSuggestionPositiveHintCount : kSuggestionNegativeHintCount;
final prefix =
positive ? 'dailyCheckPositiveHint' : 'dailyCheckNegativeHint';
final mod =
positive ? kSuggestionPositiveHintCount : kSuggestionNegativeHintCount;
final pos = math.max(1, _todayRecord!.resultModifiers[index] % mod);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
prefix.tr(args: ['$prefix$pos'.tr()]),
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
Text(
'$prefix${pos}Description',
@ -425,7 +447,10 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
else
Text(
'dailyCheckEverythingIsNegative',
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
).tr(),
const Gap(8),
if (_todayRecord?.resultTier != 4)
@ -441,7 +466,10 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
else
Text(
'dailyCheckEverythingIsPositive',
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
).tr(),
],
),
@ -469,6 +497,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -566,10 +595,12 @@ class _HomeDashNotificationWidget extends StatefulWidget {
const _HomeDashNotificationWidget();
@override
State<_HomeDashNotificationWidget> createState() => _HomeDashNotificationWidgetState();
State<_HomeDashNotificationWidget> createState() =>
_HomeDashNotificationWidgetState();
}
class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget> {
class _HomeDashNotificationWidgetState
extends State<_HomeDashNotificationWidget> {
int? _count;
Future<void> _fetchNotificationCount() async {
@ -594,6 +625,7 @@ class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -606,7 +638,9 @@ class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget
style: Theme.of(context).textTheme.titleLarge,
).tr(),
Text(
_count == null ? 'loading'.tr() : 'notificationUnreadCount'.plural(_count ?? 0),
_count == null
? 'loading'.tr()
: 'notificationUnreadCount'.plural(_count ?? 0),
style: Theme.of(context).textTheme.bodyLarge,
),
],
@ -637,10 +671,12 @@ class _HomeDashRecommendationPostWidget extends StatefulWidget {
const _HomeDashRecommendationPostWidget();
@override
State<_HomeDashRecommendationPostWidget> createState() => _HomeDashRecommendationPostWidgetState();
State<_HomeDashRecommendationPostWidget> createState() =>
_HomeDashRecommendationPostWidgetState();
}
class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendationPostWidget> {
class _HomeDashRecommendationPostWidgetState
extends State<_HomeDashRecommendationPostWidget> {
bool _isBusy = false;
List<SnPost>? _posts;
@ -657,37 +693,62 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
}
}
int _currentPage = 0;
final PageController _pageController = PageController();
@override
void initState() {
super.initState();
_fetchRecommendationPosts();
_pageController.addListener(() {
setState(() {
_currentPage = _pageController.page?.round() ?? 0;
});
});
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_isBusy) {
return Card(
margin: EdgeInsets.zero,
child: CircularProgressIndicator().center(),
);
}
return Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(Symbols.star),
const Gap(8),
Text(
'postRecommendation',
style: Theme.of(context).textTheme.titleLarge,
).tr()
Row(
children: [
const Icon(Symbols.star),
const Gap(8),
Text(
'postRecommendation',
style: Theme.of(context).textTheme.titleLarge,
).tr(),
],
),
Text('${_currentPage + 1}/${_posts?.length ?? 0}',
style: GoogleFonts.robotoMono())
],
).padding(horizontal: 18, top: 12, bottom: 8),
Expanded(
child: PageView.builder(
scrollBehavior: ScrollConfiguration.of(context).copyWith(dragDevices: {
controller: _pageController,
scrollBehavior:
ScrollConfiguration.of(context).copyWith(dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
}),
@ -700,7 +761,8 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
showMenu: false,
).padding(bottom: 8),
onTap: () {
GoRouter.of(context).pushNamed('postDetail', pathParameters: {
GoRouter.of(context)
.pushNamed('postDetail', pathParameters: {
'slug': _posts![index].id.toString(),
});
},

View File

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

View File

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.dart';
@ -19,6 +20,7 @@ import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/attachment/attachment_input.dart';
import 'package:surface/widgets/attachment/attachment_item.dart';
@ -34,6 +36,8 @@ import 'package:provider/provider.dart';
import 'package:surface/widgets/post/post_poll_editor.dart';
import 'package:uuid/uuid.dart';
import '../../providers/sn_realm.dart';
class PostEditorExtra {
final String? text;
final String? title;
@ -78,6 +82,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
bool get _isLoading => _isFetching || _writeController.isLoading;
List<SnPublisher>? _publishers;
List<SnRealm>? _realms;
Future<void> _fetchPublishers() async {
setState(() => _isFetching = true);
@ -100,6 +105,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
}
}
Future<void> _fetchRealms() async {
final rels = context.read<SnRealmProvider>();
try {
_realms = await rels.listAvailableRealms();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
void _updateMeta() {
showModalBottomSheet(
context: context,
@ -110,7 +125,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
final HotKey _pasteHotKey = HotKey(
key: PhysicalKeyboardKey.keyV,
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control],
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
scope: HotKeyScope.inapp,
);
@ -136,6 +151,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
builder: (context) => _PostPublisherPopup(
controller: _writeController,
publishers: _publishers,
onUpdate: () {
_fetchPublishers();
},
),
);
}
void _showRealmPopup() {
showModalBottomSheet(
context: context,
builder: (context) => _PostRealmPopup(
controller: _writeController,
realms: _realms,
onUpdate: () {
_fetchRealms();
},
),
);
}
@ -157,10 +188,26 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
}
}
void _showThumbnailEditorDialog() async {
final attachment = await showDialog<SnAttachment?>(
context: context,
builder: (context) => AttachmentInputDialog(
title: 'postThumbnail'.tr(),
pool: 'interactive',
mediaType: SnMediaType.image,
),
);
if (!context.mounted) return;
if (attachment == null) return;
_writeController.setThumbnail(attachment);
}
@override
void dispose() {
_writeController.dispose();
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
hotKeyManager.unregister(_pasteHotKey);
}
super.dispose();
}
@ -174,6 +221,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
} else {
_writeController.setMode(widget.mode);
}
_fetchRealms();
_fetchPublishers();
_writeController.fetchRelatedPost(
context,
@ -250,6 +298,62 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
],
),
),
if (_writeController.replyingPost != null)
Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.reply, size: 16),
const Gap(10),
Text('@${_writeController.replyingPost!.publisher.name}').bold(),
const Gap(4),
Expanded(
child: Text(
_writeController.replyingPost!.body['content'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
if (_writeController.repostingPost != null)
Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.forward, size: 16),
const Gap(10),
Text('@${_writeController.repostingPost!.publisher.name}').bold(),
const Gap(4),
Expanded(
child: Text(
_writeController.repostingPost!.body['content'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Expanded(
child: Stack(
children: [
@ -259,18 +363,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
'stories' => _PostStoryEditor(
controller: _writeController,
onTapPublisher: _showPublisherPopup,
onTapRealm: _showRealmPopup,
),
'articles' => _PostArticleEditor(
controller: _writeController,
onTapPublisher: _showPublisherPopup,
onTapRealm: _showRealmPopup,
),
'questions' => _PostQuestionEditor(
controller: _writeController,
onTapPublisher: _showPublisherPopup,
onTapRealm: _showRealmPopup,
),
'videos' => _PostVideoEditor(
controller: _writeController,
onTapPublisher: _showPublisherPopup,
onTapRealm: _showRealmPopup,
),
_ => const Placeholder(),
})
@ -282,15 +390,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
left: 0,
right: 0,
child: PostMediaPendingList(
thumbnail: _writeController.thumbnail,
attachments: _writeController.attachments,
isBusy: _writeController.isBusy,
onUpload: (int idx) async {
await _writeController.uploadSingleAttachment(context, idx);
},
onPostSetThumbnail: (int? idx) {
_writeController.setThumbnail(idx);
},
onInsertLink: (int idx) async {
_writeController.contentController.text +=
'\n![](solink://attachments/${_writeController.attachments[idx].attachment!.rid})';
@ -391,6 +495,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
_showPollEditorDialog();
},
),
if (_writeController.mode == 'articles')
IconButton(
icon: Icon(Symbols.full_coverage, color: Theme.of(context).colorScheme.primary),
style: ButtonStyle(
backgroundColor: _writeController.thumbnail == null
? null
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
),
onPressed: () {
if (_writeController.thumbnail != null) {
_writeController.setThumbnail(null);
return;
}
_showThumbnailEditorDialog();
},
),
],
),
),
@ -434,8 +554,9 @@ class _PostEditorActionScrollBehavior extends MaterialScrollBehavior {
class _PostPublisherPopup extends StatelessWidget {
final PostWriteController controller;
final List<SnPublisher>? publishers;
final Function onUpdate;
const _PostPublisherPopup({required this.controller, this.publishers});
const _PostPublisherPopup({required this.controller, this.publishers, required this.onUpdate});
@override
Widget build(BuildContext context) {
@ -450,6 +571,20 @@ class _PostPublisherPopup extends StatelessWidget {
Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
ListTile(
leading: const Icon(Symbols.add),
title: Text('publishersNew').tr(),
subtitle: Text('publisherNewSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () {
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
if (value == true) {
onUpdate();
}
});
},
),
const Divider(height: 1),
Expanded(
child: ListView.builder(
itemCount: publishers?.length ?? 0,
@ -472,11 +607,65 @@ class _PostPublisherPopup extends StatelessWidget {
}
}
class _PostRealmPopup extends StatelessWidget {
final PostWriteController controller;
final List<SnRealm>? realms;
final Function onUpdate;
const _PostRealmPopup({required this.controller, this.realms, required this.onUpdate});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.face, size: 24),
const Gap(16),
Text('accountRealms', style: Theme.of(context).textTheme.titleLarge).tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
ListTile(
leading: const Icon(Symbols.close),
title: Text('postInGlobal').tr(),
subtitle: Text('postInGlobalDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () {
controller.setRealm(null);
Navigator.pop(context, true);
},
),
const Divider(height: 1),
Expanded(
child: ListView.builder(
itemCount: realms?.length ?? 0,
itemBuilder: (context, idx) {
final realm = realms![idx];
return ListTile(
title: Text(realm.name),
subtitle: Text('@${realm.alias}'),
leading: AccountImage(content: realm.avatar, radius: 18),
onTap: () {
controller.setRealm(realm);
Navigator.pop(context, true);
},
);
},
),
),
],
);
}
}
class _PostStoryEditor extends StatelessWidget {
final PostWriteController controller;
final Function? onTapPublisher;
final Function? onTapRealm;
const _PostStoryEditor({required this.controller, this.onTapPublisher});
const _PostStoryEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
@override
Widget build(BuildContext context) {
@ -486,17 +675,36 @@ class _PostStoryEditor extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Material(
elevation: 2,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapPublisher?.call();
},
child: AccountImage(
content: controller.publisher?.avatar,
Column(
children: [
Material(
elevation: 2,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapPublisher?.call();
},
child: AccountImage(
content: controller.publisher?.avatar,
),
),
),
),
const Gap(11),
Material(
elevation: 1,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapRealm?.call();
},
child: AccountImage(
content: controller.realm?.avatar,
fallbackWidget: const Icon(Symbols.globe, size: 20),
radius: 14,
),
),
),
],
),
Expanded(
child: Column(
@ -525,6 +733,7 @@ class _PostStoryEditor extends StatelessWidget {
border: InputBorder.none,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
),
],
),
@ -538,8 +747,9 @@ class _PostStoryEditor extends StatelessWidget {
class _PostArticleEditor extends StatelessWidget {
final PostWriteController controller;
final Function? onTapPublisher;
final Function? onTapRealm;
const _PostArticleEditor({required this.controller, this.onTapPublisher});
const _PostArticleEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
@override
Widget build(BuildContext context) {
@ -560,6 +770,21 @@ class _PostArticleEditor extends StatelessWidget {
],
),
),
Material(
elevation: 1,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapRealm?.call();
},
child: AccountImage(
content: controller.realm?.avatar,
fallbackWidget: const Icon(Symbols.globe, size: 20),
radius: 14,
),
),
),
const Gap(8),
],
).padding(horizontal: 12, vertical: 8),
onTap: () {
@ -588,8 +813,26 @@ class _PostArticleEditor extends StatelessWidget {
keyboardType: TextInputType.multiline,
style: Theme.of(context).textTheme.bodyLarge,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
).padding(horizontal: 16),
const Gap(4),
if (controller.thumbnail != null)
Container(
margin: const EdgeInsets.only(left: 12, right: 12, top: 8, bottom: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Theme.of(context).dividerColor),
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: 16 / 9,
child: AttachmentItem(
data: controller.thumbnail!.attachment!,
heroTag: "post-editor-thumbnail-preview",
),
),
),
),
];
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) {
@ -615,6 +858,7 @@ class _PostArticleEditor extends StatelessWidget {
border: InputBorder.none,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
),
),
const Gap(8),
@ -649,6 +893,7 @@ class _PostArticleEditor extends StatelessWidget {
border: InputBorder.none,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
),
),
],
@ -659,8 +904,9 @@ class _PostArticleEditor extends StatelessWidget {
class _PostQuestionEditor extends StatelessWidget {
final PostWriteController controller;
final Function? onTapPublisher;
final Function? onTapRealm;
const _PostQuestionEditor({required this.controller, this.onTapPublisher});
const _PostQuestionEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
@override
Widget build(BuildContext context) {
@ -670,17 +916,36 @@ class _PostQuestionEditor extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Material(
elevation: 1,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapPublisher?.call();
},
child: AccountImage(
content: controller.publisher?.avatar,
Column(
children: [
Material(
elevation: 2,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapPublisher?.call();
},
child: AccountImage(
content: controller.publisher?.avatar,
),
),
),
),
const Gap(11),
Material(
elevation: 1,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapRealm?.call();
},
child: AccountImage(
content: controller.realm?.avatar,
fallbackWidget: const Icon(Symbols.globe, size: 20),
radius: 14,
),
),
),
],
),
Expanded(
child: Column(
@ -720,6 +985,7 @@ class _PostQuestionEditor extends StatelessWidget {
border: InputBorder.none,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
),
],
),
@ -733,8 +999,9 @@ class _PostQuestionEditor extends StatelessWidget {
class _PostVideoEditor extends StatelessWidget {
final PostWriteController controller;
final Function? onTapPublisher;
final Function? onTapRealm;
const _PostVideoEditor({required this.controller, this.onTapPublisher});
const _PostVideoEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
void _selectVideo(BuildContext context) async {
final video = await showDialog<SnAttachment?>(
@ -822,28 +1089,36 @@ class _PostVideoEditor extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
children: [
Material(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: InkWell(
child: Row(
children: [
AccountImage(content: controller.publisher?.avatar, radius: 20),
const Gap(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(controller.publisher?.nick ?? 'loading'.tr()).bold(),
Text('@${controller.publisher?.name}'),
],
),
Column(
children: [
Material(
elevation: 2,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapPublisher?.call();
},
child: AccountImage(
content: controller.publisher?.avatar,
),
],
).padding(horizontal: 12, vertical: 8),
onTap: () {
onTapPublisher?.call();
},
),
),
),
const Gap(11),
Material(
elevation: 1,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapRealm?.call();
},
child: AccountImage(
content: controller.realm?.avatar,
fallbackWidget: const Icon(Symbols.globe, size: 20),
radius: 14,
),
),
),
],
),
const Gap(16),
TextField(

View File

@ -4,17 +4,16 @@ import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/realm/realm_item.dart';
import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:surface/widgets/universal_image.dart';
class RealmScreen extends StatefulWidget {
const RealmScreen({super.key});
@ -75,12 +74,12 @@ class _RealmScreenState extends State<RealmScreen> {
@override
void initState() {
super.initState();
_isCompactView = context.read<ConfigProvider>().realmCompactView;
_fetchRealms();
}
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
if (!ua.isAuthorized) {
@ -110,6 +109,7 @@ class _RealmScreenState extends State<RealmScreen> {
icon: !_isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
onPressed: () {
setState(() => _isCompactView = !_isCompactView);
context.read<ConfigProvider>().realmCompactView = _isCompactView;
},
),
const Gap(8),
@ -134,121 +134,46 @@ class _RealmScreenState extends State<RealmScreen> {
itemCount: _realms?.length ?? 0,
itemBuilder: (context, idx) {
final realm = _realms![idx];
if (_isCompactView) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: realm.avatar,
fallbackWidget: const Icon(Symbols.group, size: 20),
),
title: Text(realm.name),
subtitle: Text(
realm.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: PopupMenuButton(
itemBuilder: (BuildContext context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.edit),
const Gap(16),
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': realm.alias},
).then((value) {
if (value != null) {
_fetchRealms();
}
});
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deleteRealm(realm);
},
),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': realm.alias},
);
},
);
}
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,
return RealmItemWidget(
showPopularity: false,
item: realm,
isListView: _isCompactView,
actionListView: [
PopupMenuItem(
child: Row(
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),
const Icon(Symbols.edit),
const Gap(16),
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': realm.alias},
);
'realmManage',
queryParameters: {'editing': realm.alias},
).then((value) {
if (value != null) {
_fetchRealms();
}
});
},
),
),
).center();
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deleteRealm(realm);
},
),
],
onUpdate: _fetchRealms,
);
},
),
),

View File

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

View File

@ -5,16 +5,19 @@ import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/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/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:surface/widgets/post/post_item.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class RealmDetailScreen extends StatefulWidget {
@ -60,18 +63,36 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
}
}
List<SnChannel>? _channels;
Future<void> _fetchChannels() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/im/channels/${widget.alias}/public');
_channels = List<SnChannel>.from(
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
);
} catch (err) {
if (mounted) context.showErrorDialog(err);
rethrow;
} finally {
setState(() {});
}
}
@override
void initState() {
super.initState();
_fetchRealm().then((_) {
_fetchPublishers();
_fetchChannels();
});
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
length: 4,
child: AppScaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
@ -83,6 +104,7 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
bottom: TabBar(
tabs: [
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.explore, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
],
@ -93,7 +115,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
},
body: TabBarView(
children: [
_RealmDetailHomeWidget(realm: _realm, publishers: _publishers),
_RealmDetailHomeWidget(realm: _realm, publishers: _publishers, channels: _channels),
_RealmPostListWidget(realm: _realm),
_RealmMemberListWidget(realm: _realm),
_RealmSettingsWidget(
realm: _realm,
@ -112,8 +135,9 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
class _RealmDetailHomeWidget extends StatelessWidget {
final SnRealm? realm;
final List<SnPublisher>? publishers;
final List<SnChannel>? channels;
const _RealmDetailHomeWidget({required this.realm, this.publishers});
const _RealmDetailHomeWidget({required this.realm, this.publishers, this.channels});
@override
Widget build(BuildContext context) {
@ -135,30 +159,76 @@ class _RealmDetailHomeWidget extends StatelessWidget {
],
).padding(horizontal: 24),
const Gap(16),
const Divider(),
const Divider(height: 1),
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: publishers?.length ?? 0,
itemBuilder: (context, idx) {
final ele = publishers![idx];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: AccountImage(
content: ele.avatar,
fallbackWidget: const Icon(Symbols.group, size: 24),
child: CustomScrollView(
slivers: [
if (publishers?.isNotEmpty ?? false)
SliverToBoxAdapter(
child: Container(
width: double.infinity,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Text('realmCommunityPublishersHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
.padding(horizontal: 24, vertical: 8),
),
),
title: Text(ele.nick),
subtitle: Text('@${ele.name}'),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed(
'postPublisher',
pathParameters: {'name': ele.name},
SliverList.builder(
itemCount: publishers?.length ?? 0,
itemBuilder: (context, idx) {
final ele = publishers![idx];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: AccountImage(
content: ele.avatar,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
title: Text(ele.nick),
subtitle: Text('@${ele.name}'),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed(
'postPublisher',
pathParameters: {'name': ele.name},
);
},
);
},
);
},
),
if (channels?.isNotEmpty ?? false)
SliverToBoxAdapter(
child: Container(
width: double.infinity,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
.padding(horizontal: 24, vertical: 8),
),
),
SliverList.builder(
itemCount: channels?.length ?? 0,
itemBuilder: (context, idx) {
final ele = channels![idx];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: AccountImage(
content: null,
fallbackWidget: const Icon(Symbols.chat, size: 20),
),
title: Text(ele.name),
subtitle: Text('#${ele.alias}'),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': realm?.alias ?? 'global',
'alias': ele.alias,
},
);
},
);
},
),
],
),
),
],
@ -166,6 +236,72 @@ class _RealmDetailHomeWidget extends StatelessWidget {
}
}
class _RealmPostListWidget extends StatefulWidget {
final SnRealm? realm;
const _RealmPostListWidget({this.realm});
@override
State<_RealmPostListWidget> createState() => _RealmPostListWidgetState();
}
class _RealmPostListWidgetState extends State<_RealmPostListWidget> {
bool _isBusy = false;
int? _totalCount;
final List<SnPost> _posts = List.empty(growable: true);
Future<void> _fetchPosts() async {
setState(() => _isBusy = true);
try {
final pt = context.read<SnPostContentProvider>();
final out = await pt.listPosts(
take: 10,
offset: _posts.length,
realm: widget.realm?.id.toString(),
);
_totalCount = out.$2;
_posts.addAll(out.$1);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
return MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: _fetchPosts,
child: InfiniteList(
itemCount: _posts.length,
isLoading: _isBusy,
hasReachedMax: _totalCount != null && _posts.length >= _totalCount!,
onFetchData: _fetchPosts,
itemBuilder: (context, idx) {
final post = _posts[idx];
return OpenablePostItem(
data: post,
maxWidth: 640,
onChanged: (data) {
setState(() => _posts[idx] = data);
},
onDeleted: () {
setState(() => _posts.removeAt(idx));
},
);
},
separatorBuilder: (_, __) => const Gap(8),
),
),
).padding(top: 8);
}
}
class _RealmMemberListWidget extends StatefulWidget {
final SnRealm? realm;
@ -189,7 +325,7 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: {
'take': 10,
'offset': 0,
'offset': _members.length,
});
final out = List<SnRealmMember>.from(
@ -343,12 +479,31 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
setState(() => _isBusy = true);
try {
await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}');
await sn.client.delete('/cgi/id/realms/${widget.realm!.id}');
if (!mounted) return;
Navigator.pop(context, true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _leaveRealm() async {
final confirm = await context.showConfirmDialog(
'realmLeave'.tr(),
'realmLeaveDescription'.tr(),
);
if (!confirm) return;
if (!mounted) return;
final sn = context.read<SnNetworkProvider>();
try {
await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}/me');
if (!mounted) return;
Navigator.pop(context, true);
context.showSnackbar('realmDeleted'.tr(args: [
'#${widget.realm!.alias}',
]));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -367,22 +522,31 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
children: [
const Gap(8),
ListTile(
leading: const Icon(Symbols.edit),
leading: const Icon(Symbols.logout),
trailing: const Icon(Symbols.chevron_right),
title: Text('realmEdit').tr(),
subtitle: Text('realmEditDescription').tr(),
title: Text('realmLeave').tr(),
subtitle: Text('realmLeaveDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () {
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': widget.realm!.alias},
).then((value) {
if (value != null) {
widget.onUpdate();
}
});
},
onTap: _isBusy ? null : () => _leaveRealm(),
),
if (isOwned)
ListTile(
leading: const Icon(Symbols.edit),
trailing: const Icon(Symbols.chevron_right),
title: Text('realmEdit').tr(),
subtitle: Text('realmEditDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () {
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': widget.realm!.alias},
).then((value) {
if (value != null) {
widget.onUpdate();
}
});
},
),
if (isOwned)
ListTile(
leading: const Icon(Symbols.delete),

View File

@ -4,6 +4,7 @@ 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/config.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/chat.dart';
@ -12,7 +13,7 @@ import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:surface/widgets/realm/realm_item.dart';
class RealmDiscoveryScreen extends StatefulWidget {
const RealmDiscoveryScreen({super.key});
@ -24,6 +25,7 @@ class RealmDiscoveryScreen extends StatefulWidget {
class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
List<SnRealm>? _realms;
bool _isBusy = false;
bool _isCompactView = false;
Future<void> _fetchRealms() async {
try {
@ -44,16 +46,25 @@ class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
@override
void initState() {
super.initState();
_isCompactView = context.read<ConfigProvider>().realmCompactView;
_fetchRealms();
}
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
return AppScaffold(
appBar: AppBar(
title: Text('screenRealmDiscovery').tr(),
actions: [
IconButton(
icon: _isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
onPressed: () {
setState(() => _isCompactView = !_isCompactView);
context.read<ConfigProvider>().realmCompactView = _isCompactView;
},
),
const Gap(8),
],
),
body: Column(
children: [
@ -66,64 +77,16 @@ class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
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();
return RealmItemWidget(
item: realm,
isListView: _isCompactView,
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => _RealmJoinPopup(realm: realm),
);
},
);
},
),
),
@ -154,7 +117,7 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}');
final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
final out = List<SnChannel>.from(
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
);
@ -235,6 +198,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
),
Text(
widget.realm.description,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
],

View File

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

464
lib/screens/stickers.dart Normal file
View File

@ -0,0 +1,464 @@
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';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StickerScreen extends StatefulWidget {
const StickerScreen({super.key});
@override
State<StickerScreen> createState() => _StickerScreenState();
}
class _StickerScreenState extends State<StickerScreen>
with SingleTickerProviderStateMixin {
late final TabController _tabController =
TabController(length: 3, vsync: this);
bool _isBusy = false;
int? _totalCount;
final List<SnStickerPack> _packs = List.empty(growable: true);
Future<void> _fetchPacks() async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
final resp = await sn.client.get(
_tabController.index == 1
? '/cgi/uc/stickers/packs/own'
: '/cgi/uc/stickers/packs',
queryParameters: {
'take': 10,
'offset': _packs.length,
if (_tabController.index == 2) 'author': ua.user?.id,
},
);
if (resp.data is Map<String, dynamic>) {
_totalCount = resp.data['count'] as int?;
final out = List<SnStickerPack>.from(
resp.data['data'].map((ele) => SnStickerPack.fromJson(ele)),
);
_packs.addAll(out);
} else {
_totalCount = 0;
final out = List<SnStickerPack>.from(
resp.data.map((ele) => SnStickerPack.fromJson(ele)),
);
_packs.addAll(out);
}
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _removePack(SnStickerPack pack) async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
await sn.client.delete('/cgi/uc/stickers/packs/${pack.id}/own');
if (!mounted) return;
context.showSnackbar('stickersRemoved'.tr());
_refreshPacks();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _deletePack(SnStickerPack pack) async {
final confirm = await context.showConfirmDialog(
'stickersPackDelete'.tr(args: [pack.name]),
'stickersPackDeleteDescription'.tr(),
);
if (!confirm) return;
if (!mounted) return;
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
await sn.client.delete('/cgi/uc/stickers/packs/${pack.id}');
if (!mounted) return;
context.showSnackbar('stickersDeleted'.tr());
_refreshPacks();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _refreshPacks() async {
_packs.clear();
_totalCount = null;
await _fetchPacks();
}
@override
void initState() {
super.initState();
_fetchPacks();
_tabController.addListener(() {
if (_tabController.indexIsChanging) {
_refreshPacks();
}
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
leading: AutoAppBarLeading(),
title: Text('screenStickers').tr(),
actions: [
IconButton(
icon: const Icon(Symbols.add_circle),
onPressed: () {
showDialog(
context: context,
builder: (context) => _StickerPackCreateDialog(),
).then((value) {
if (value == true) _refreshPacks();
});
},
),
const Gap(8),
],
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(
child: Text('stickersDiscovery'.tr()).textColor(
Theme.of(context).appBarTheme.foregroundColor,
),
),
Tab(
child: Text('stickersOwned'.tr()).textColor(
Theme.of(context).appBarTheme.foregroundColor,
),
),
Tab(
child: Text('stickersCreated'.tr()).textColor(
Theme.of(context).appBarTheme.foregroundColor,
),
),
],
),
),
body: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: _refreshPacks,
child: InfiniteList(
itemCount: _packs.length,
onFetchData: _fetchPacks,
hasReachedMax: _totalCount != null && _packs.length >= _totalCount!,
isLoading: _isBusy,
itemBuilder: (context, idx) {
final pack = _packs[idx];
return ListTile(
title: Text(pack.name),
subtitle: Text(
pack.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
trailing: _tabController.index == 1
? IconButton(
onPressed: () {
_removePack(pack);
},
icon: const Icon(Symbols.remove),
)
: _tabController.index == 2
? IconButton(
onPressed: () {
_deletePack(pack);
},
icon: const Icon(Symbols.delete),
)
: null,
onTap: () {
if (_tabController.index == 0) {
showModalBottomSheet(
context: context,
builder: (context) => _StickerPackAddPopup(pack: pack),
).then((value) {
if (value == true && _tabController.index == 1) {
_refreshPacks();
}
});
} else {
GoRouter.of(context).pushNamed(
'stickerPack',
pathParameters: {
'id': pack.id.toString(),
},
);
}
},
);
},
),
),
),
);
}
}
class _StickerPackAddPopup extends StatefulWidget {
final SnStickerPack pack;
const _StickerPackAddPopup({required this.pack});
@override
State<_StickerPackAddPopup> createState() => _StickerPackAddPopupState();
}
class _StickerPackAddPopupState extends State<_StickerPackAddPopup> {
SnStickerPack? _pack;
bool _isBusy = false;
Future<void> _fetchPack() async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp =
await sn.client.get('/cgi/uc/stickers/packs/${widget.pack.id}');
_pack = SnStickerPack.fromJson(resp.data);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchPack();
}
bool _isAdding = false;
Future<void> _addPack() async {
if (_pack == null) return;
try {
setState(() => _isAdding = true);
final sn = context.read<SnNetworkProvider>();
final stickers = context.read<SnStickerProvider>();
await sn.client.post(
'/cgi/uc/stickers/packs/${widget.pack.id}/own',
);
if (!mounted) return;
context.showSnackbar('stickersAdded'.tr());
if (_pack?.stickers != null) stickers.putSticker(_pack!.stickers!);
Navigator.pop(context, true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isAdding = false);
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.add, size: 24),
const Gap(16),
Text('stickersAdd', 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.pack.name).bold(),
Text(
widget.pack.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
ElevatedButton(
onPressed: _isAdding ? null : _addPack,
child: Text('add').tr(),
),
],
).padding(horizontal: 24),
LoadingIndicator(isActive: _isBusy),
if (_pack?.stickers != null)
Expanded(
child: GridView.extent(
padding: EdgeInsets.only(left: 20, right: 20, top: 8),
maxCrossAxisExtent: 48,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: _pack!.stickers!
.map(
(ele) => ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color:
Theme.of(context).colorScheme.surfaceContainerHigh,
child: AttachmentItem(
data: ele.attachment,
heroTag: 'sticker-pack-${ele.attachment.rid}',
fit: BoxFit.contain,
),
),
),
)
.toList(),
),
),
],
);
}
}
class _StickerPackCreateDialog extends StatefulWidget {
const _StickerPackCreateDialog();
@override
State<_StickerPackCreateDialog> createState() =>
_StickerPackCreateDialogState();
}
class _StickerPackCreateDialogState extends State<_StickerPackCreateDialog> {
final TextEditingController _nameController = TextEditingController();
final TextEditingController _prefixController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
bool _isBusy = false;
Future<void> _createPack() async {
if (_nameController.text.isEmpty ||
_prefixController.text.isEmpty ||
_descriptionController.text.isEmpty) {
return;
}
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post(
'/cgi/uc/stickers/packs',
data: {
'name': _nameController.text,
'prefix': _prefixController.text,
'description': _descriptionController.text,
},
);
if (!mounted) return;
Navigator.pop(context, true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
@override
void dispose() {
_nameController.dispose();
_prefixController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('stickersPackNew').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _nameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldStickerPackName'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _prefixController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldStickerPackPrefix'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _descriptionController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldStickerPackDescription'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: _isBusy
? null
: () {
Navigator.pop(context);
},
child: Text('dialogDismiss').tr(),
),
TextButton(
onPressed: _isBusy ? null : () => _createPack(),
child: Text('dialogConfirm').tr(),
),
],
);
}
}

View File

@ -0,0 +1,266 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_input.dart';
import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class StickerPackScreen extends StatefulWidget {
final int id;
const StickerPackScreen({super.key, required this.id});
@override
State<StickerPackScreen> createState() => _StickerPackScreenState();
}
class _StickerPackScreenState extends State<StickerPackScreen> {
SnStickerPack? _pack;
Future<void> _fetchPack() async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/uc/stickers/packs/${widget.id}');
_pack = SnStickerPack.fromJson(resp.data);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
bool _isBusy = false;
Future<void> _deleteSticker(SnSticker sticker) async {
final confirm = await context.showConfirmDialog(
'stickersDelete'.tr(args: [sticker.name]),
'stickersDeleteDescription'.tr(),
);
if (!confirm) return;
if (!mounted) return;
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
await sn.client.delete('/cgi/uc/stickers/${sticker.id}');
if (!mounted) return;
context.showSnackbar('stickersDeleted'.tr());
_fetchPack();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchPack();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
title: Text(_pack?.name ?? 'loading'.tr()),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LoadingIndicator(isActive: _isBusy),
if (_pack != null)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_pack!.name).bold(),
Text(
_pack!.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
).padding(horizontal: 24, vertical: 16),
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.add),
title: Text('stickersNew').tr(),
subtitle: Text('stickersNewDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () {
showDialog(
context: context,
builder: (context) => _StickerCreateDialog(pack: _pack!),
).then((value) {
if (value) _fetchPack();
});
},
),
const Divider(height: 1),
if (_pack?.stickers != null)
Expanded(
child: GridView.extent(
padding: EdgeInsets.only(left: 20, right: 20, top: 16),
maxCrossAxisExtent: 48,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: _pack!.stickers!
.map(
(ele) => GestureDetector(
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context)
.colorScheme
.surfaceContainerHigh,
child: AttachmentItem(
data: ele.attachment,
heroTag: 'sticker-pack-${ele.attachment.rid}',
fit: BoxFit.contain,
),
),
),
onTap: () {
_deleteSticker(ele);
},
),
)
.toList(),
),
),
],
),
);
}
}
class _StickerCreateDialog extends StatefulWidget {
final SnStickerPack pack;
const _StickerCreateDialog({required this.pack});
@override
State<_StickerCreateDialog> createState() => _StickerCreateDialogState();
}
class _StickerCreateDialogState extends State<_StickerCreateDialog> {
final TextEditingController _nameController = TextEditingController();
final TextEditingController _aliasController = TextEditingController();
final TextEditingController _attachmentController = TextEditingController();
bool _isBusy = false;
@override
void dispose() {
_nameController.dispose();
_aliasController.dispose();
_attachmentController.dispose();
super.dispose();
}
Future<void> _createSticker() async {
if (_nameController.text.isEmpty ||
_aliasController.text.isEmpty ||
_attachmentController.text.isEmpty) {
return;
}
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post(
'/cgi/uc/stickers',
data: {
'name': _nameController.text,
'alias': _aliasController.text,
'attachment_id': _attachmentController.text,
'pack_id': widget.pack.id,
},
);
if (!mounted) return;
Navigator.pop(context, true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('stickersNew'.tr()),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _nameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldStickerName'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _aliasController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldStickerAlias'.tr(),
helperText: 'fieldStickerAliasHint'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _attachmentController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldStickerAttachment'.tr(),
),
readOnly: true,
onTap: () async {
final attachment = await showDialog<SnAttachment?>(
context: context,
builder: (context) => AttachmentInputDialog(
title: 'fieldStickerAttachment'.tr(),
pool: 'sticker',
mediaType: SnMediaType.image,
),
);
if (attachment != null) {
setState(() {
_attachmentController.text = attachment.rid;
});
}
},
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: _isBusy
? null
: () {
Navigator.pop(context);
},
child: Text('dialogDismiss').tr(),
),
TextButton(
onPressed: _isBusy ? null : () => _createSticker(),
child: Text('dialogConfirm').tr(),
),
],
);
}
}

View File

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

View File

@ -1,5 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_flutter/hive_flutter.dart';
part 'account.freezed.dart';
part 'account.g.dart';
@ -9,7 +8,7 @@ class SnAccount with _$SnAccount {
const SnAccount._();
const factory SnAccount({
@HiveField(0) required int id,
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
@ -20,7 +19,7 @@ class SnAccount with _$SnAccount {
required String description,
required String name,
required String nick,
required Map<String, dynamic> permNodes,
@Default({}) Map<String, dynamic> permNodes,
required String language,
required SnAccountProfile? profile,
@Default([]) List<SnAccountBadge> badges,

View File

@ -20,7 +20,6 @@ SnAccount _$SnAccountFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$SnAccount {
@HiveField(0)
int get id => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
@ -58,7 +57,7 @@ abstract class $SnAccountCopyWith<$Res> {
_$SnAccountCopyWithImpl<$Res, SnAccount>;
@useResult
$Res call(
{@HiveField(0) int id,
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
@ -226,7 +225,7 @@ abstract class _$$SnAccountImplCopyWith<$Res>
@override
@useResult
$Res call(
{@HiveField(0) int id,
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
@ -374,7 +373,7 @@ class __$$SnAccountImplCopyWithImpl<$Res>
@JsonSerializable()
class _$SnAccountImpl extends _SnAccount {
const _$SnAccountImpl(
{@HiveField(0) required this.id,
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
@ -385,7 +384,7 @@ class _$SnAccountImpl extends _SnAccount {
required this.description,
required this.name,
required this.nick,
required final Map<String, dynamic> permNodes,
final Map<String, dynamic> permNodes = const {},
required this.language,
required this.profile,
final List<SnAccountBadge> badges = const [],
@ -403,7 +402,6 @@ class _$SnAccountImpl extends _SnAccount {
_$$SnAccountImplFromJson(json);
@override
@HiveField(0)
final int id;
@override
final DateTime createdAt;
@ -437,6 +435,7 @@ class _$SnAccountImpl extends _SnAccount {
final String nick;
final Map<String, dynamic> _permNodes;
@override
@JsonKey()
Map<String, dynamic> get permNodes {
if (_permNodes is EqualUnmodifiableMapView) return _permNodes;
// ignore: implicit_dynamic_type
@ -555,7 +554,7 @@ class _$SnAccountImpl extends _SnAccount {
abstract class _SnAccount extends SnAccount {
const factory _SnAccount(
{@HiveField(0) required final int id,
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final DateTime? deletedAt,
@ -566,7 +565,7 @@ abstract class _SnAccount extends SnAccount {
required final String description,
required final String name,
required final String nick,
required final Map<String, dynamic> permNodes,
final Map<String, dynamic> permNodes,
required final String language,
required final SnAccountProfile? profile,
final List<SnAccountBadge> badges,
@ -581,7 +580,6 @@ abstract class _SnAccount extends SnAccount {
_$SnAccountImpl.fromJson;
@override
@HiveField(0)
int get id;
@override
DateTime get createdAt;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:surface/types/account.dart';
import 'package:surface/types/attachment.dart';
@ -12,23 +11,22 @@ part 'chat.g.dart';
class SnChannel with _$SnChannel {
const SnChannel._();
@HiveType(typeId: 2)
const factory SnChannel({
@HiveField(0) required int id,
@HiveField(1) required DateTime createdAt,
@HiveField(2) required DateTime updatedAt,
@HiveField(3) required dynamic deletedAt,
@HiveField(4) required String alias,
@HiveField(5) required String name,
@HiveField(6) required String description,
@HiveField(7) required List<SnChannelMember>? members,
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required dynamic deletedAt,
required String alias,
required String name,
required String description,
required List<SnChannelMember>? members,
List<SnChatMessage>? messages,
@HiveField(8) required int type,
@HiveField(9) required int accountId,
@HiveField(10) required SnRealm? realm,
@HiveField(11) required int? realmId,
@HiveField(12) required bool isPublic,
@HiveField(13) required bool isCommunity,
required int type,
required int accountId,
required SnRealm? realm,
required int? realmId,
required bool isPublic,
required bool isCommunity,
}) = _SnChannel;
factory SnChannel.fromJson(Map<String, dynamic> json) =>
@ -42,19 +40,18 @@ class SnChannel with _$SnChannel {
class SnChannelMember with _$SnChannelMember {
const SnChannelMember._();
@HiveType(typeId: 3)
const factory SnChannelMember({
@HiveField(0) required int id,
@HiveField(1) required DateTime createdAt,
@HiveField(2) required DateTime updatedAt,
@HiveField(3) required DateTime? deletedAt,
@HiveField(4) required int channelId,
@HiveField(5) required int accountId,
@HiveField(6) required String? nick,
@HiveField(7) required SnChannel? channel,
@HiveField(8) required SnAccount? account,
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required int channelId,
required int accountId,
required String? nick,
required SnChannel? channel,
required SnAccount? account,
@Default(0) int notify,
@HiveField(9) required int powerLevel,
required int powerLevel,
dynamic calls,
dynamic events,
}) = _SnChannelMember;
@ -67,21 +64,20 @@ class SnChannelMember with _$SnChannelMember {
class SnChatMessage with _$SnChatMessage {
const SnChatMessage._();
@HiveType(typeId: 4)
const factory SnChatMessage({
@HiveField(0) required int id,
@HiveField(1) required DateTime createdAt,
@HiveField(2) required DateTime updatedAt,
@HiveField(3) required DateTime? deletedAt,
@HiveField(4) required String uuid,
@HiveField(5) @Default({}) Map<String, dynamic> body,
@HiveField(6) required String type,
@HiveField(7) required SnChannel channel,
@HiveField(8) required SnChannelMember sender,
@HiveField(9) required int channelId,
@HiveField(10) required int senderId,
@HiveField(11) required int? quoteEventId,
@HiveField(12) required int? relatedEventId,
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String uuid,
@Default({}) Map<String, dynamic> body,
required String type,
required SnChannel channel,
required SnChannelMember sender,
required int channelId,
required int senderId,
required int? quoteEventId,
required int? relatedEventId,
SnChatMessagePreload? preload,
}) = _SnChatMessage;

View File

@ -20,34 +20,20 @@ SnChannel _$SnChannelFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$SnChannel {
@HiveField(0)
int get id => throw _privateConstructorUsedError;
@HiveField(1)
DateTime get createdAt => throw _privateConstructorUsedError;
@HiveField(2)
DateTime get updatedAt => throw _privateConstructorUsedError;
@HiveField(3)
dynamic get deletedAt => throw _privateConstructorUsedError;
@HiveField(4)
String get alias => throw _privateConstructorUsedError;
@HiveField(5)
String get name => throw _privateConstructorUsedError;
@HiveField(6)
String get description => throw _privateConstructorUsedError;
@HiveField(7)
List<SnChannelMember>? get members => throw _privateConstructorUsedError;
List<SnChatMessage>? get messages => throw _privateConstructorUsedError;
@HiveField(8)
int get type => throw _privateConstructorUsedError;
@HiveField(9)
int get accountId => throw _privateConstructorUsedError;
@HiveField(10)
SnRealm? get realm => throw _privateConstructorUsedError;
@HiveField(11)
int? get realmId => throw _privateConstructorUsedError;
@HiveField(12)
bool get isPublic => throw _privateConstructorUsedError;
@HiveField(13)
bool get isCommunity => throw _privateConstructorUsedError;
/// Serializes this SnChannel to a JSON map.
@ -66,21 +52,21 @@ abstract class $SnChannelCopyWith<$Res> {
_$SnChannelCopyWithImpl<$Res, SnChannel>;
@useResult
$Res call(
{@HiveField(0) int id,
@HiveField(1) DateTime createdAt,
@HiveField(2) DateTime updatedAt,
@HiveField(3) dynamic deletedAt,
@HiveField(4) String alias,
@HiveField(5) String name,
@HiveField(6) String description,
@HiveField(7) List<SnChannelMember>? members,
{int id,
DateTime createdAt,
DateTime updatedAt,
dynamic deletedAt,
String alias,
String name,
String description,
List<SnChannelMember>? members,
List<SnChatMessage>? messages,
@HiveField(8) int type,
@HiveField(9) int accountId,
@HiveField(10) SnRealm? realm,
@HiveField(11) int? realmId,
@HiveField(12) bool isPublic,
@HiveField(13) bool isCommunity});
int type,
int accountId,
SnRealm? realm,
int? realmId,
bool isPublic,
bool isCommunity});
$SnRealmCopyWith<$Res>? get realm;
}
@ -204,21 +190,21 @@ abstract class _$$SnChannelImplCopyWith<$Res>
@override
@useResult
$Res call(
{@HiveField(0) int id,
@HiveField(1) DateTime createdAt,
@HiveField(2) DateTime updatedAt,
@HiveField(3) dynamic deletedAt,
@HiveField(4) String alias,
@HiveField(5) String name,
@HiveField(6) String description,
@HiveField(7) List<SnChannelMember>? members,
{int id,
DateTime createdAt,
DateTime updatedAt,
dynamic deletedAt,
String alias,
String name,
String description,
List<SnChannelMember>? members,
List<SnChatMessage>? messages,
@HiveField(8) int type,
@HiveField(9) int accountId,
@HiveField(10) SnRealm? realm,
@HiveField(11) int? realmId,
@HiveField(12) bool isPublic,
@HiveField(13) bool isCommunity});
int type,
int accountId,
SnRealm? realm,
int? realmId,
bool isPublic,
bool isCommunity});
@override
$SnRealmCopyWith<$Res>? get realm;
@ -320,24 +306,23 @@ class __$$SnChannelImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
@HiveType(typeId: 2)
class _$SnChannelImpl extends _SnChannel {
const _$SnChannelImpl(
{@HiveField(0) required this.id,
@HiveField(1) required this.createdAt,
@HiveField(2) required this.updatedAt,
@HiveField(3) required this.deletedAt,
@HiveField(4) required this.alias,
@HiveField(5) required this.name,
@HiveField(6) required this.description,
@HiveField(7) required final List<SnChannelMember>? members,
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.alias,
required this.name,
required this.description,
required final List<SnChannelMember>? members,
final List<SnChatMessage>? messages,
@HiveField(8) required this.type,
@HiveField(9) required this.accountId,
@HiveField(10) required this.realm,
@HiveField(11) required this.realmId,
@HiveField(12) required this.isPublic,
@HiveField(13) required this.isCommunity})
required this.type,
required this.accountId,
required this.realm,
required this.realmId,
required this.isPublic,
required this.isCommunity})
: _members = members,
_messages = messages,
super._();
@ -346,29 +331,21 @@ class _$SnChannelImpl extends _SnChannel {
_$$SnChannelImplFromJson(json);
@override
@HiveField(0)
final int id;
@override
@HiveField(1)
final DateTime createdAt;
@override
@HiveField(2)
final DateTime updatedAt;
@override
@HiveField(3)
final dynamic deletedAt;
@override
@HiveField(4)
final String alias;
@override
@HiveField(5)
final String name;
@override
@HiveField(6)
final String description;
final List<SnChannelMember>? _members;
@override
@HiveField(7)
List<SnChannelMember>? get members {
final value = _members;
if (value == null) return null;
@ -388,22 +365,16 @@ class _$SnChannelImpl extends _SnChannel {
}
@override
@HiveField(8)
final int type;
@override
@HiveField(9)
final int accountId;
@override
@HiveField(10)
final SnRealm? realm;
@override
@HiveField(11)
final int? realmId;
@override
@HiveField(12)
final bool isPublic;
@override
@HiveField(13)
final bool isCommunity;
@override
@ -477,69 +448,55 @@ class _$SnChannelImpl extends _SnChannel {
abstract class _SnChannel extends SnChannel {
const factory _SnChannel(
{@HiveField(0) required final int id,
@HiveField(1) required final DateTime createdAt,
@HiveField(2) required final DateTime updatedAt,
@HiveField(3) required final dynamic deletedAt,
@HiveField(4) required final String alias,
@HiveField(5) required final String name,
@HiveField(6) required final String description,
@HiveField(7) required final List<SnChannelMember>? members,
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final dynamic deletedAt,
required final String alias,
required final String name,
required final String description,
required final List<SnChannelMember>? members,
final List<SnChatMessage>? messages,
@HiveField(8) required final int type,
@HiveField(9) required final int accountId,
@HiveField(10) required final SnRealm? realm,
@HiveField(11) required final int? realmId,
@HiveField(12) required final bool isPublic,
@HiveField(13) required final bool isCommunity}) = _$SnChannelImpl;
required final int type,
required final int accountId,
required final SnRealm? realm,
required final int? realmId,
required final bool isPublic,
required final bool isCommunity}) = _$SnChannelImpl;
const _SnChannel._() : super._();
factory _SnChannel.fromJson(Map<String, dynamic> json) =
_$SnChannelImpl.fromJson;
@override
@HiveField(0)
int get id;
@override
@HiveField(1)
DateTime get createdAt;
@override
@HiveField(2)
DateTime get updatedAt;
@override
@HiveField(3)
dynamic get deletedAt;
@override
@HiveField(4)
String get alias;
@override
@HiveField(5)
String get name;
@override
@HiveField(6)
String get description;
@override
@HiveField(7)
List<SnChannelMember>? get members;
@override
List<SnChatMessage>? get messages;
@override
@HiveField(8)
int get type;
@override
@HiveField(9)
int get accountId;
@override
@HiveField(10)
SnRealm? get realm;
@override
@HiveField(11)
int? get realmId;
@override
@HiveField(12)
bool get isPublic;
@override
@HiveField(13)
bool get isCommunity;
/// Create a copy of SnChannel
@ -556,26 +513,16 @@ SnChannelMember _$SnChannelMemberFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$SnChannelMember {
@HiveField(0)
int get id => throw _privateConstructorUsedError;
@HiveField(1)
DateTime get createdAt => throw _privateConstructorUsedError;
@HiveField(2)
DateTime get updatedAt => throw _privateConstructorUsedError;
@HiveField(3)
DateTime? get deletedAt => throw _privateConstructorUsedError;
@HiveField(4)
int get channelId => throw _privateConstructorUsedError;
@HiveField(5)
int get accountId => throw _privateConstructorUsedError;
@HiveField(6)
String? get nick => throw _privateConstructorUsedError;
@HiveField(7)
SnChannel? get channel => throw _privateConstructorUsedError;
@HiveField(8)
SnAccount? get account => throw _privateConstructorUsedError;
int get notify => throw _privateConstructorUsedError;
@HiveField(9)
int get powerLevel => throw _privateConstructorUsedError;
dynamic get calls => throw _privateConstructorUsedError;
dynamic get events => throw _privateConstructorUsedError;
@ -597,17 +544,17 @@ abstract class $SnChannelMemberCopyWith<$Res> {
_$SnChannelMemberCopyWithImpl<$Res, SnChannelMember>;
@useResult
$Res call(
{@HiveField(0) int id,
@HiveField(1) DateTime createdAt,
@HiveField(2) DateTime updatedAt,
@HiveField(3) DateTime? deletedAt,
@HiveField(4) int channelId,
@HiveField(5) int accountId,
@HiveField(6) String? nick,
@HiveField(7) SnChannel? channel,
@HiveField(8) SnAccount? account,
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
int channelId,
int accountId,
String? nick,
SnChannel? channel,
SnAccount? account,
int notify,
@HiveField(9) int powerLevel,
int powerLevel,
dynamic calls,
dynamic events});
@ -738,17 +685,17 @@ abstract class _$$SnChannelMemberImplCopyWith<$Res>
@override
@useResult
$Res call(
{@HiveField(0) int id,
@HiveField(1) DateTime createdAt,
@HiveField(2) DateTime updatedAt,
@HiveField(3) DateTime? deletedAt,
@HiveField(4) int channelId,
@HiveField(5) int accountId,
@HiveField(6) String? nick,
@HiveField(7) SnChannel? channel,
@HiveField(8) SnAccount? account,
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
int channelId,
int accountId,
String? nick,
SnChannel? channel,
SnAccount? account,
int notify,
@HiveField(9) int powerLevel,
int powerLevel,
dynamic calls,
dynamic events});
@ -844,20 +791,19 @@ class __$$SnChannelMemberImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
@HiveType(typeId: 3)
class _$SnChannelMemberImpl extends _SnChannelMember {
const _$SnChannelMemberImpl(
{@HiveField(0) required this.id,
@HiveField(1) required this.createdAt,
@HiveField(2) required this.updatedAt,
@HiveField(3) required this.deletedAt,
@HiveField(4) required this.channelId,
@HiveField(5) required this.accountId,
@HiveField(6) required this.nick,
@HiveField(7) required this.channel,
@HiveField(8) required this.account,
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.channelId,
required this.accountId,
required this.nick,
required this.channel,
required this.account,
this.notify = 0,
@HiveField(9) required this.powerLevel,
required this.powerLevel,
this.calls,
this.events})
: super._();
@ -866,37 +812,27 @@ class _$SnChannelMemberImpl extends _SnChannelMember {
_$$SnChannelMemberImplFromJson(json);
@override
@HiveField(0)
final int id;
@override
@HiveField(1)
final DateTime createdAt;
@override
@HiveField(2)
final DateTime updatedAt;
@override
@HiveField(3)
final DateTime? deletedAt;
@override
@HiveField(4)
final int channelId;
@override
@HiveField(5)
final int accountId;
@override
@HiveField(6)
final String? nick;
@override
@HiveField(7)
final SnChannel? channel;
@override
@HiveField(8)
final SnAccount? account;
@override
@JsonKey()
final int notify;
@override
@HiveField(9)
final int powerLevel;
@override
final dynamic calls;
@ -971,17 +907,17 @@ class _$SnChannelMemberImpl extends _SnChannelMember {
abstract class _SnChannelMember extends SnChannelMember {
const factory _SnChannelMember(
{@HiveField(0) required final int id,
@HiveField(1) required final DateTime createdAt,
@HiveField(2) required final DateTime updatedAt,
@HiveField(3) required final DateTime? deletedAt,
@HiveField(4) required final int channelId,
@HiveField(5) required final int accountId,
@HiveField(6) required final String? nick,
@HiveField(7) required final SnChannel? channel,
@HiveField(8) required final SnAccount? account,
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final DateTime? deletedAt,
required final int channelId,
required final int accountId,
required final String? nick,
required final SnChannel? channel,
required final SnAccount? account,
final int notify,
@HiveField(9) required final int powerLevel,
required final int powerLevel,
final dynamic calls,
final dynamic events}) = _$SnChannelMemberImpl;
const _SnChannelMember._() : super._();
@ -990,36 +926,26 @@ abstract class _SnChannelMember extends SnChannelMember {
_$SnChannelMemberImpl.fromJson;
@override
@HiveField(0)
int get id;
@override
@HiveField(1)
DateTime get createdAt;
@override
@HiveField(2)
DateTime get updatedAt;
@override
@HiveField(3)
DateTime? get deletedAt;
@override
@HiveField(4)
int get channelId;
@override
@HiveField(5)
int get accountId;
@override
@HiveField(6)
String? get nick;
@override
@HiveField(7)
SnChannel? get channel;
@override
@HiveField(8)
SnAccount? get account;
@override
int get notify;
@override
@HiveField(9)
int get powerLevel;
@override
dynamic get calls;
@ -1040,31 +966,18 @@ SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$SnChatMessage {
@HiveField(0)
int get id => throw _privateConstructorUsedError;
@HiveField(1)
DateTime get createdAt => throw _privateConstructorUsedError;
@HiveField(2)
DateTime get updatedAt => throw _privateConstructorUsedError;
@HiveField(3)
DateTime? get deletedAt => throw _privateConstructorUsedError;
@HiveField(4)
String get uuid => throw _privateConstructorUsedError;
@HiveField(5)
Map<String, dynamic> get body => throw _privateConstructorUsedError;
@HiveField(6)
String get type => throw _privateConstructorUsedError;
@HiveField(7)
SnChannel get channel => throw _privateConstructorUsedError;
@HiveField(8)
SnChannelMember get sender => throw _privateConstructorUsedError;
@HiveField(9)
int get channelId => throw _privateConstructorUsedError;
@HiveField(10)
int get senderId => throw _privateConstructorUsedError;
@HiveField(11)
int? get quoteEventId => throw _privateConstructorUsedError;
@HiveField(12)
int? get relatedEventId => throw _privateConstructorUsedError;
SnChatMessagePreload? get preload => throw _privateConstructorUsedError;
@ -1085,19 +998,19 @@ abstract class $SnChatMessageCopyWith<$Res> {
_$SnChatMessageCopyWithImpl<$Res, SnChatMessage>;
@useResult
$Res call(
{@HiveField(0) int id,
@HiveField(1) DateTime createdAt,
@HiveField(2) DateTime updatedAt,
@HiveField(3) DateTime? deletedAt,
@HiveField(4) String uuid,
@HiveField(5) Map<String, dynamic> body,
@HiveField(6) String type,
@HiveField(7) SnChannel channel,
@HiveField(8) SnChannelMember sender,
@HiveField(9) int channelId,
@HiveField(10) int senderId,
@HiveField(11) int? quoteEventId,
@HiveField(12) int? relatedEventId,
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String uuid,
Map<String, dynamic> body,
String type,
SnChannel channel,
SnChannelMember sender,
int channelId,
int senderId,
int? quoteEventId,
int? relatedEventId,
SnChatMessagePreload? preload});
$SnChannelCopyWith<$Res> get channel;
@ -1239,19 +1152,19 @@ abstract class _$$SnChatMessageImplCopyWith<$Res>
@override
@useResult
$Res call(
{@HiveField(0) int id,
@HiveField(1) DateTime createdAt,
@HiveField(2) DateTime updatedAt,
@HiveField(3) DateTime? deletedAt,
@HiveField(4) String uuid,
@HiveField(5) Map<String, dynamic> body,
@HiveField(6) String type,
@HiveField(7) SnChannel channel,
@HiveField(8) SnChannelMember sender,
@HiveField(9) int channelId,
@HiveField(10) int senderId,
@HiveField(11) int? quoteEventId,
@HiveField(12) int? relatedEventId,
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String uuid,
Map<String, dynamic> body,
String type,
SnChannel channel,
SnChannelMember sender,
int channelId,
int senderId,
int? quoteEventId,
int? relatedEventId,
SnChatMessagePreload? preload});
@override
@ -1353,22 +1266,21 @@ class __$$SnChatMessageImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
@HiveType(typeId: 4)
class _$SnChatMessageImpl extends _SnChatMessage {
const _$SnChatMessageImpl(
{@HiveField(0) required this.id,
@HiveField(1) required this.createdAt,
@HiveField(2) required this.updatedAt,
@HiveField(3) required this.deletedAt,
@HiveField(4) required this.uuid,
@HiveField(5) final Map<String, dynamic> body = const {},
@HiveField(6) required this.type,
@HiveField(7) required this.channel,
@HiveField(8) required this.sender,
@HiveField(9) required this.channelId,
@HiveField(10) required this.senderId,
@HiveField(11) required this.quoteEventId,
@HiveField(12) required this.relatedEventId,
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.uuid,
final Map<String, dynamic> body = const {},
required this.type,
required this.channel,
required this.sender,
required this.channelId,
required this.senderId,
required this.quoteEventId,
required this.relatedEventId,
this.preload})
: _body = body,
super._();
@ -1377,24 +1289,18 @@ class _$SnChatMessageImpl extends _SnChatMessage {
_$$SnChatMessageImplFromJson(json);
@override
@HiveField(0)
final int id;
@override
@HiveField(1)
final DateTime createdAt;
@override
@HiveField(2)
final DateTime updatedAt;
@override
@HiveField(3)
final DateTime? deletedAt;
@override
@HiveField(4)
final String uuid;
final Map<String, dynamic> _body;
@override
@JsonKey()
@HiveField(5)
Map<String, dynamic> get body {
if (_body is EqualUnmodifiableMapView) return _body;
// ignore: implicit_dynamic_type
@ -1402,25 +1308,18 @@ class _$SnChatMessageImpl extends _SnChatMessage {
}
@override
@HiveField(6)
final String type;
@override
@HiveField(7)
final SnChannel channel;
@override
@HiveField(8)
final SnChannelMember sender;
@override
@HiveField(9)
final int channelId;
@override
@HiveField(10)
final int senderId;
@override
@HiveField(11)
final int? quoteEventId;
@override
@HiveField(12)
final int? relatedEventId;
@override
final SnChatMessagePreload? preload;
@ -1495,19 +1394,19 @@ class _$SnChatMessageImpl extends _SnChatMessage {
abstract class _SnChatMessage extends SnChatMessage {
const factory _SnChatMessage(
{@HiveField(0) required final int id,
@HiveField(1) required final DateTime createdAt,
@HiveField(2) required final DateTime updatedAt,
@HiveField(3) required final DateTime? deletedAt,
@HiveField(4) required final String uuid,
@HiveField(5) final Map<String, dynamic> body,
@HiveField(6) required final String type,
@HiveField(7) required final SnChannel channel,
@HiveField(8) required final SnChannelMember sender,
@HiveField(9) required final int channelId,
@HiveField(10) required final int senderId,
@HiveField(11) required final int? quoteEventId,
@HiveField(12) required final int? relatedEventId,
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final DateTime? deletedAt,
required final String uuid,
final Map<String, dynamic> body,
required final String type,
required final SnChannel channel,
required final SnChannelMember sender,
required final int channelId,
required final int senderId,
required final int? quoteEventId,
required final int? relatedEventId,
final SnChatMessagePreload? preload}) = _$SnChatMessageImpl;
const _SnChatMessage._() : super._();
@ -1515,43 +1414,30 @@ abstract class _SnChatMessage extends SnChatMessage {
_$SnChatMessageImpl.fromJson;
@override
@HiveField(0)
int get id;
@override
@HiveField(1)
DateTime get createdAt;
@override
@HiveField(2)
DateTime get updatedAt;
@override
@HiveField(3)
DateTime? get deletedAt;
@override
@HiveField(4)
String get uuid;
@override
@HiveField(5)
Map<String, dynamic> get body;
@override
@HiveField(6)
String get type;
@override
@HiveField(7)
SnChannel get channel;
@override
@HiveField(8)
SnChannelMember get sender;
@override
@HiveField(9)
int get channelId;
@override
@HiveField(10)
int get senderId;
@override
@HiveField(11)
int? get quoteEventId;
@override
@HiveField(12)
int? get relatedEventId;
@override
SnChatMessagePreload? get preload;

View File

@ -2,214 +2,6 @@
part of 'chat.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class SnChannelImplAdapter extends TypeAdapter<_$SnChannelImpl> {
@override
final int typeId = 2;
@override
_$SnChannelImpl read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return _$SnChannelImpl(
id: fields[0] as int,
createdAt: fields[1] as DateTime,
updatedAt: fields[2] as DateTime,
deletedAt: fields[3] as dynamic,
alias: fields[4] as String,
name: fields[5] as String,
description: fields[6] as String,
members: (fields[7] as List?)?.cast<SnChannelMember>(),
type: fields[8] as int,
accountId: fields[9] as int,
realm: fields[10] as SnRealm?,
realmId: fields[11] as int?,
isPublic: fields[12] as bool,
isCommunity: fields[13] as bool,
);
}
@override
void write(BinaryWriter writer, _$SnChannelImpl obj) {
writer
..writeByte(14)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.createdAt)
..writeByte(2)
..write(obj.updatedAt)
..writeByte(3)
..write(obj.deletedAt)
..writeByte(4)
..write(obj.alias)
..writeByte(5)
..write(obj.name)
..writeByte(6)
..write(obj.description)
..writeByte(8)
..write(obj.type)
..writeByte(9)
..write(obj.accountId)
..writeByte(10)
..write(obj.realm)
..writeByte(11)
..write(obj.realmId)
..writeByte(12)
..write(obj.isPublic)
..writeByte(13)
..write(obj.isCommunity)
..writeByte(7)
..write(obj.members);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SnChannelImplAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class SnChannelMemberImplAdapter extends TypeAdapter<_$SnChannelMemberImpl> {
@override
final int typeId = 3;
@override
_$SnChannelMemberImpl read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return _$SnChannelMemberImpl(
id: fields[0] as int,
createdAt: fields[1] as DateTime,
updatedAt: fields[2] as DateTime,
deletedAt: fields[3] as DateTime?,
channelId: fields[4] as int,
accountId: fields[5] as int,
nick: fields[6] as String?,
channel: fields[7] as SnChannel?,
account: fields[8] as SnAccount?,
powerLevel: fields[9] as int,
);
}
@override
void write(BinaryWriter writer, _$SnChannelMemberImpl obj) {
writer
..writeByte(10)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.createdAt)
..writeByte(2)
..write(obj.updatedAt)
..writeByte(3)
..write(obj.deletedAt)
..writeByte(4)
..write(obj.channelId)
..writeByte(5)
..write(obj.accountId)
..writeByte(6)
..write(obj.nick)
..writeByte(7)
..write(obj.channel)
..writeByte(8)
..write(obj.account)
..writeByte(9)
..write(obj.powerLevel);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SnChannelMemberImplAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class SnChatMessageImplAdapter extends TypeAdapter<_$SnChatMessageImpl> {
@override
final int typeId = 4;
@override
_$SnChatMessageImpl read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return _$SnChatMessageImpl(
id: fields[0] as int,
createdAt: fields[1] as DateTime,
updatedAt: fields[2] as DateTime,
deletedAt: fields[3] as DateTime?,
uuid: fields[4] as String,
body: (fields[5] as Map).cast<String, dynamic>(),
type: fields[6] as String,
channel: fields[7] as SnChannel,
sender: fields[8] as SnChannelMember,
channelId: fields[9] as int,
senderId: fields[10] as int,
quoteEventId: fields[11] as int?,
relatedEventId: fields[12] as int?,
);
}
@override
void write(BinaryWriter writer, _$SnChatMessageImpl obj) {
writer
..writeByte(13)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.createdAt)
..writeByte(2)
..write(obj.updatedAt)
..writeByte(3)
..write(obj.deletedAt)
..writeByte(4)
..write(obj.uuid)
..writeByte(6)
..write(obj.type)
..writeByte(7)
..write(obj.channel)
..writeByte(8)
..write(obj.sender)
..writeByte(9)
..write(obj.channelId)
..writeByte(10)
..write(obj.senderId)
..writeByte(11)
..write(obj.quoteEventId)
..writeByte(12)
..write(obj.relatedEventId)
..writeByte(5)
..write(obj.body);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SnChatMessageImplAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/poll.dart';
import 'package:surface/types/realm.dart';
part 'post.freezed.dart';
part 'post.g.dart';
@ -24,6 +25,7 @@ class SnPost with _$SnPost {
required List<SnPost>? replies,
required int? replyId,
required int? repostId,
required int? realmId,
required SnPost? replyTo,
required SnPost? repostTo,
required List<int>? visibleUsersList,
@ -37,6 +39,8 @@ class SnPost with _$SnPost {
required DateTime? publishedUntil,
required int totalUpvote,
required int totalDownvote,
@Default(0) int totalViews,
@Default(0) int totalAggregatedViews,
required int publisherId,
required int? pollId,
required SnPublisher publisher,
@ -93,6 +97,7 @@ class SnPostPreload with _$SnPostPreload {
required List<SnAttachment?>? attachments,
required SnAttachment? video,
required SnPoll? poll,
required SnRealm? realm,
}) = _SnPostPreload;
factory SnPostPreload.fromJson(Map<String, Object?> json) =>

View File

@ -34,6 +34,7 @@ mixin _$SnPost {
List<SnPost>? get replies => throw _privateConstructorUsedError;
int? get replyId => throw _privateConstructorUsedError;
int? get repostId => throw _privateConstructorUsedError;
int? get realmId => throw _privateConstructorUsedError;
SnPost? get replyTo => throw _privateConstructorUsedError;
SnPost? get repostTo => throw _privateConstructorUsedError;
List<int>? get visibleUsersList => throw _privateConstructorUsedError;
@ -47,6 +48,8 @@ mixin _$SnPost {
DateTime? get publishedUntil => throw _privateConstructorUsedError;
int get totalUpvote => throw _privateConstructorUsedError;
int get totalDownvote => throw _privateConstructorUsedError;
int get totalViews => throw _privateConstructorUsedError;
int get totalAggregatedViews => throw _privateConstructorUsedError;
int get publisherId => throw _privateConstructorUsedError;
int? get pollId => throw _privateConstructorUsedError;
SnPublisher get publisher => throw _privateConstructorUsedError;
@ -82,6 +85,7 @@ abstract class $SnPostCopyWith<$Res> {
List<SnPost>? replies,
int? replyId,
int? repostId,
int? realmId,
SnPost? replyTo,
SnPost? repostTo,
List<int>? visibleUsersList,
@ -95,6 +99,8 @@ abstract class $SnPostCopyWith<$Res> {
DateTime? publishedUntil,
int totalUpvote,
int totalDownvote,
int totalViews,
int totalAggregatedViews,
int publisherId,
int? pollId,
SnPublisher publisher,
@ -137,6 +143,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
Object? replies = freezed,
Object? replyId = freezed,
Object? repostId = freezed,
Object? realmId = freezed,
Object? replyTo = freezed,
Object? repostTo = freezed,
Object? visibleUsersList = freezed,
@ -150,6 +157,8 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
Object? publishedUntil = freezed,
Object? totalUpvote = null,
Object? totalDownvote = null,
Object? totalViews = null,
Object? totalAggregatedViews = null,
Object? publisherId = null,
Object? pollId = freezed,
Object? publisher = null,
@ -213,6 +222,10 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
? _value.repostId
: repostId // ignore: cast_nullable_to_non_nullable
as int?,
realmId: freezed == realmId
? _value.realmId
: realmId // ignore: cast_nullable_to_non_nullable
as int?,
replyTo: freezed == replyTo
? _value.replyTo
: replyTo // ignore: cast_nullable_to_non_nullable
@ -265,6 +278,14 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
? _value.totalDownvote
: totalDownvote // ignore: cast_nullable_to_non_nullable
as int,
totalViews: null == totalViews
? _value.totalViews
: totalViews // ignore: cast_nullable_to_non_nullable
as int,
totalAggregatedViews: null == totalAggregatedViews
? _value.totalAggregatedViews
: totalAggregatedViews // ignore: cast_nullable_to_non_nullable
as int,
publisherId: null == publisherId
? _value.publisherId
: publisherId // ignore: cast_nullable_to_non_nullable
@ -373,6 +394,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
List<SnPost>? replies,
int? replyId,
int? repostId,
int? realmId,
SnPost? replyTo,
SnPost? repostTo,
List<int>? visibleUsersList,
@ -386,6 +408,8 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
DateTime? publishedUntil,
int totalUpvote,
int totalDownvote,
int totalViews,
int totalAggregatedViews,
int publisherId,
int? pollId,
SnPublisher publisher,
@ -431,6 +455,7 @@ class __$$SnPostImplCopyWithImpl<$Res>
Object? replies = freezed,
Object? replyId = freezed,
Object? repostId = freezed,
Object? realmId = freezed,
Object? replyTo = freezed,
Object? repostTo = freezed,
Object? visibleUsersList = freezed,
@ -444,6 +469,8 @@ class __$$SnPostImplCopyWithImpl<$Res>
Object? publishedUntil = freezed,
Object? totalUpvote = null,
Object? totalDownvote = null,
Object? totalViews = null,
Object? totalAggregatedViews = null,
Object? publisherId = null,
Object? pollId = freezed,
Object? publisher = null,
@ -507,6 +534,10 @@ class __$$SnPostImplCopyWithImpl<$Res>
? _value.repostId
: repostId // ignore: cast_nullable_to_non_nullable
as int?,
realmId: freezed == realmId
? _value.realmId
: realmId // ignore: cast_nullable_to_non_nullable
as int?,
replyTo: freezed == replyTo
? _value.replyTo
: replyTo // ignore: cast_nullable_to_non_nullable
@ -559,6 +590,14 @@ class __$$SnPostImplCopyWithImpl<$Res>
? _value.totalDownvote
: totalDownvote // ignore: cast_nullable_to_non_nullable
as int,
totalViews: null == totalViews
? _value.totalViews
: totalViews // ignore: cast_nullable_to_non_nullable
as int,
totalAggregatedViews: null == totalAggregatedViews
? _value.totalAggregatedViews
: totalAggregatedViews // ignore: cast_nullable_to_non_nullable
as int,
publisherId: null == publisherId
? _value.publisherId
: publisherId // ignore: cast_nullable_to_non_nullable
@ -601,6 +640,7 @@ class _$SnPostImpl extends _SnPost {
required final List<SnPost>? replies,
required this.replyId,
required this.repostId,
required this.realmId,
required this.replyTo,
required this.repostTo,
required final List<int>? visibleUsersList,
@ -614,6 +654,8 @@ class _$SnPostImpl extends _SnPost {
required this.publishedUntil,
required this.totalUpvote,
required this.totalDownvote,
this.totalViews = 0,
this.totalAggregatedViews = 0,
required this.publisherId,
required this.pollId,
required this.publisher,
@ -687,6 +729,8 @@ class _$SnPostImpl extends _SnPost {
@override
final int? repostId;
@override
final int? realmId;
@override
final SnPost? replyTo;
@override
final SnPost? repostTo;
@ -731,6 +775,12 @@ class _$SnPostImpl extends _SnPost {
@override
final int totalDownvote;
@override
@JsonKey()
final int totalViews;
@override
@JsonKey()
final int totalAggregatedViews;
@override
final int publisherId;
@override
final int? pollId;
@ -743,7 +793,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, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)';
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)';
}
@override
@ -772,6 +822,7 @@ class _$SnPostImpl extends _SnPost {
(identical(other.replyId, replyId) || other.replyId == replyId) &&
(identical(other.repostId, repostId) ||
other.repostId == repostId) &&
(identical(other.realmId, realmId) || other.realmId == realmId) &&
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
(identical(other.repostTo, repostTo) ||
other.repostTo == repostTo) &&
@ -796,6 +847,10 @@ class _$SnPostImpl extends _SnPost {
other.totalUpvote == totalUpvote) &&
(identical(other.totalDownvote, totalDownvote) ||
other.totalDownvote == totalDownvote) &&
(identical(other.totalViews, totalViews) ||
other.totalViews == totalViews) &&
(identical(other.totalAggregatedViews, totalAggregatedViews) ||
other.totalAggregatedViews == totalAggregatedViews) &&
(identical(other.publisherId, publisherId) ||
other.publisherId == publisherId) &&
(identical(other.pollId, pollId) || other.pollId == pollId) &&
@ -823,6 +878,7 @@ class _$SnPostImpl extends _SnPost {
const DeepCollectionEquality().hash(_replies),
replyId,
repostId,
realmId,
replyTo,
repostTo,
const DeepCollectionEquality().hash(_visibleUsersList),
@ -836,6 +892,8 @@ class _$SnPostImpl extends _SnPost {
publishedUntil,
totalUpvote,
totalDownvote,
totalViews,
totalAggregatedViews,
publisherId,
pollId,
publisher,
@ -875,6 +933,7 @@ abstract class _SnPost extends SnPost {
required final List<SnPost>? replies,
required final int? replyId,
required final int? repostId,
required final int? realmId,
required final SnPost? replyTo,
required final SnPost? repostTo,
required final List<int>? visibleUsersList,
@ -888,6 +947,8 @@ abstract class _SnPost extends SnPost {
required final DateTime? publishedUntil,
required final int totalUpvote,
required final int totalDownvote,
final int totalViews,
final int totalAggregatedViews,
required final int publisherId,
required final int? pollId,
required final SnPublisher publisher,
@ -926,6 +987,8 @@ abstract class _SnPost extends SnPost {
@override
int? get repostId;
@override
int? get realmId;
@override
SnPost? get replyTo;
@override
SnPost? get repostTo;
@ -952,6 +1015,10 @@ abstract class _SnPost extends SnPost {
@override
int get totalDownvote;
@override
int get totalViews;
@override
int get totalAggregatedViews;
@override
int get publisherId;
@override
int? get pollId;
@ -1590,6 +1657,7 @@ mixin _$SnPostPreload {
List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
SnAttachment? get video => throw _privateConstructorUsedError;
SnPoll? get poll => throw _privateConstructorUsedError;
SnRealm? get realm => throw _privateConstructorUsedError;
/// Serializes this SnPostPreload to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -1611,11 +1679,13 @@ abstract class $SnPostPreloadCopyWith<$Res> {
{SnAttachment? thumbnail,
List<SnAttachment?>? attachments,
SnAttachment? video,
SnPoll? poll});
SnPoll? poll,
SnRealm? realm});
$SnAttachmentCopyWith<$Res>? get thumbnail;
$SnAttachmentCopyWith<$Res>? get video;
$SnPollCopyWith<$Res>? get poll;
$SnRealmCopyWith<$Res>? get realm;
}
/// @nodoc
@ -1637,6 +1707,7 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
Object? attachments = freezed,
Object? video = freezed,
Object? poll = freezed,
Object? realm = freezed,
}) {
return _then(_value.copyWith(
thumbnail: freezed == thumbnail
@ -1655,6 +1726,10 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
? _value.poll
: poll // ignore: cast_nullable_to_non_nullable
as SnPoll?,
realm: freezed == realm
? _value.realm
: realm // ignore: cast_nullable_to_non_nullable
as SnRealm?,
) as $Val);
}
@ -1699,6 +1774,20 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
return _then(_value.copyWith(poll: value) as $Val);
});
}
/// Create a copy of SnPostPreload
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnRealmCopyWith<$Res>? get realm {
if (_value.realm == null) {
return null;
}
return $SnRealmCopyWith<$Res>(_value.realm!, (value) {
return _then(_value.copyWith(realm: value) as $Val);
});
}
}
/// @nodoc
@ -1713,7 +1802,8 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res>
{SnAttachment? thumbnail,
List<SnAttachment?>? attachments,
SnAttachment? video,
SnPoll? poll});
SnPoll? poll,
SnRealm? realm});
@override
$SnAttachmentCopyWith<$Res>? get thumbnail;
@ -1721,6 +1811,8 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res>
$SnAttachmentCopyWith<$Res>? get video;
@override
$SnPollCopyWith<$Res>? get poll;
@override
$SnRealmCopyWith<$Res>? get realm;
}
/// @nodoc
@ -1740,6 +1832,7 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
Object? attachments = freezed,
Object? video = freezed,
Object? poll = freezed,
Object? realm = freezed,
}) {
return _then(_$SnPostPreloadImpl(
thumbnail: freezed == thumbnail
@ -1758,6 +1851,10 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
? _value.poll
: poll // ignore: cast_nullable_to_non_nullable
as SnPoll?,
realm: freezed == realm
? _value.realm
: realm // ignore: cast_nullable_to_non_nullable
as SnRealm?,
));
}
}
@ -1769,7 +1866,8 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
{required this.thumbnail,
required final List<SnAttachment?>? attachments,
required this.video,
required this.poll})
required this.poll,
required this.realm})
: _attachments = attachments;
factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) =>
@ -1791,10 +1889,12 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
final SnAttachment? video;
@override
final SnPoll? poll;
@override
final SnRealm? realm;
@override
String toString() {
return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video, poll: $poll)';
return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video, poll: $poll, realm: $realm)';
}
@override
@ -1807,13 +1907,14 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
const DeepCollectionEquality()
.equals(other._attachments, _attachments) &&
(identical(other.video, video) || other.video == video) &&
(identical(other.poll, poll) || other.poll == poll));
(identical(other.poll, poll) || other.poll == poll) &&
(identical(other.realm, realm) || other.realm == realm));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, thumbnail,
const DeepCollectionEquality().hash(_attachments), video, poll);
const DeepCollectionEquality().hash(_attachments), video, poll, realm);
/// Create a copy of SnPostPreload
/// with the given fields replaced by the non-null parameter values.
@ -1836,7 +1937,8 @@ abstract class _SnPostPreload implements SnPostPreload {
{required final SnAttachment? thumbnail,
required final List<SnAttachment?>? attachments,
required final SnAttachment? video,
required final SnPoll? poll}) = _$SnPostPreloadImpl;
required final SnPoll? poll,
required final SnRealm? realm}) = _$SnPostPreloadImpl;
factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
_$SnPostPreloadImpl.fromJson;
@ -1849,6 +1951,8 @@ abstract class _SnPostPreload implements SnPostPreload {
SnAttachment? get video;
@override
SnPoll? get poll;
@override
SnRealm? get realm;
/// Create a copy of SnPostPreload
/// with the given fields replaced by the non-null parameter values.

View File

@ -31,6 +31,7 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
.toList(),
replyId: (json['reply_id'] as num?)?.toInt(),
repostId: (json['repost_id'] as num?)?.toInt(),
realmId: (json['realm_id'] as num?)?.toInt(),
replyTo: json['reply_to'] == null
? null
: SnPost.fromJson(json['reply_to'] as Map<String, dynamic>),
@ -62,6 +63,9 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
: DateTime.parse(json['published_until'] as String),
totalUpvote: (json['total_upvote'] as num).toInt(),
totalDownvote: (json['total_downvote'] as num).toInt(),
totalViews: (json['total_views'] as num?)?.toInt() ?? 0,
totalAggregatedViews:
(json['total_aggregated_views'] as num?)?.toInt() ?? 0,
publisherId: (json['publisher_id'] as num).toInt(),
pollId: (json['poll_id'] as num?)?.toInt(),
publisher:
@ -88,6 +92,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
'replies': instance.replies?.map((e) => e.toJson()).toList(),
'reply_id': instance.replyId,
'repost_id': instance.repostId,
'realm_id': instance.realmId,
'reply_to': instance.replyTo?.toJson(),
'repost_to': instance.repostTo?.toJson(),
'visible_users_list': instance.visibleUsersList,
@ -101,6 +106,8 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
'published_until': instance.publishedUntil?.toIso8601String(),
'total_upvote': instance.totalUpvote,
'total_downvote': instance.totalDownvote,
'total_views': instance.totalViews,
'total_aggregated_views': instance.totalAggregatedViews,
'publisher_id': instance.publisherId,
'poll_id': instance.pollId,
'publisher': instance.publisher.toJson(),
@ -173,6 +180,9 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
poll: json['poll'] == null
? null
: SnPoll.fromJson(json['poll'] as Map<String, dynamic>),
realm: json['realm'] == null
? null
: SnRealm.fromJson(json['realm'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
@ -181,6 +191,7 @@ Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
'video': instance.video?.toJson(),
'poll': instance.poll?.toJson(),
'realm': instance.realm?.toJson(),
};
_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(

View File

@ -1,5 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:surface/types/account.dart';
part 'realm.freezed.dart';
@ -27,22 +26,22 @@ class SnRealmMember with _$SnRealmMember {
class SnRealm with _$SnRealm {
const SnRealm._();
@HiveType(typeId: 1)
const factory SnRealm({
@HiveField(0) required int id,
@HiveField(1) required DateTime createdAt,
@HiveField(2) required DateTime updatedAt,
@HiveField(3) required DateTime? deletedAt,
@HiveField(4) required String alias,
@HiveField(5) required String name,
@HiveField(6) required String description,
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String alias,
required String name,
required String description,
List<SnRealmMember>? members,
@HiveField(7) required String? avatar,
@HiveField(8) required String? banner,
@HiveField(9) required Map<String, dynamic>? accessPolicy,
@HiveField(10) required int accountId,
@HiveField(11) required bool isPublic,
@HiveField(12) required bool isCommunity,
required String? avatar,
required String? banner,
required Map<String, dynamic>? accessPolicy,
required int accountId,
required bool isPublic,
required bool isCommunity,
@Default(0) int popularity,
}) = _SnRealm;
factory SnRealm.fromJson(Map<String, dynamic> json) =>

View File

@ -367,33 +367,21 @@ SnRealm _$SnRealmFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$SnRealm {
@HiveField(0)
int get id => throw _privateConstructorUsedError;
@HiveField(1)
DateTime get createdAt => throw _privateConstructorUsedError;
@HiveField(2)
DateTime get updatedAt => throw _privateConstructorUsedError;
@HiveField(3)
DateTime? get deletedAt => throw _privateConstructorUsedError;
@HiveField(4)
String get alias => throw _privateConstructorUsedError;
@HiveField(5)
String get name => throw _privateConstructorUsedError;
@HiveField(6)
String get description => throw _privateConstructorUsedError;
List<SnRealmMember>? get members => throw _privateConstructorUsedError;
@HiveField(7)
String? get avatar => throw _privateConstructorUsedError;
@HiveField(8)
String? get banner => throw _privateConstructorUsedError;
@HiveField(9)
Map<String, dynamic>? get accessPolicy => throw _privateConstructorUsedError;
@HiveField(10)
int get accountId => throw _privateConstructorUsedError;
@HiveField(11)
bool get isPublic => throw _privateConstructorUsedError;
@HiveField(12)
bool get isCommunity => throw _privateConstructorUsedError;
int get popularity => throw _privateConstructorUsedError;
/// Serializes this SnRealm to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -410,20 +398,21 @@ abstract class $SnRealmCopyWith<$Res> {
_$SnRealmCopyWithImpl<$Res, SnRealm>;
@useResult
$Res call(
{@HiveField(0) int id,
@HiveField(1) DateTime createdAt,
@HiveField(2) DateTime updatedAt,
@HiveField(3) DateTime? deletedAt,
@HiveField(4) String alias,
@HiveField(5) String name,
@HiveField(6) String description,
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String alias,
String name,
String description,
List<SnRealmMember>? members,
@HiveField(7) String? avatar,
@HiveField(8) String? banner,
@HiveField(9) Map<String, dynamic>? accessPolicy,
@HiveField(10) int accountId,
@HiveField(11) bool isPublic,
@HiveField(12) bool isCommunity});
String? avatar,
String? banner,
Map<String, dynamic>? accessPolicy,
int accountId,
bool isPublic,
bool isCommunity,
int popularity});
}
/// @nodoc
@ -455,6 +444,7 @@ class _$SnRealmCopyWithImpl<$Res, $Val extends SnRealm>
Object? accountId = null,
Object? isPublic = null,
Object? isCommunity = null,
Object? popularity = null,
}) {
return _then(_value.copyWith(
id: null == id
@ -513,6 +503,10 @@ class _$SnRealmCopyWithImpl<$Res, $Val extends SnRealm>
? _value.isCommunity
: isCommunity // ignore: cast_nullable_to_non_nullable
as bool,
popularity: null == popularity
? _value.popularity
: popularity // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
@ -525,20 +519,21 @@ abstract class _$$SnRealmImplCopyWith<$Res> implements $SnRealmCopyWith<$Res> {
@override
@useResult
$Res call(
{@HiveField(0) int id,
@HiveField(1) DateTime createdAt,
@HiveField(2) DateTime updatedAt,
@HiveField(3) DateTime? deletedAt,
@HiveField(4) String alias,
@HiveField(5) String name,
@HiveField(6) String description,
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String alias,
String name,
String description,
List<SnRealmMember>? members,
@HiveField(7) String? avatar,
@HiveField(8) String? banner,
@HiveField(9) Map<String, dynamic>? accessPolicy,
@HiveField(10) int accountId,
@HiveField(11) bool isPublic,
@HiveField(12) bool isCommunity});
String? avatar,
String? banner,
Map<String, dynamic>? accessPolicy,
int accountId,
bool isPublic,
bool isCommunity,
int popularity});
}
/// @nodoc
@ -568,6 +563,7 @@ class __$$SnRealmImplCopyWithImpl<$Res>
Object? accountId = null,
Object? isPublic = null,
Object? isCommunity = null,
Object? popularity = null,
}) {
return _then(_$SnRealmImpl(
id: null == id
@ -626,29 +622,33 @@ class __$$SnRealmImplCopyWithImpl<$Res>
? _value.isCommunity
: isCommunity // ignore: cast_nullable_to_non_nullable
as bool,
popularity: null == popularity
? _value.popularity
: popularity // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
@HiveType(typeId: 1)
class _$SnRealmImpl extends _SnRealm {
const _$SnRealmImpl(
{@HiveField(0) required this.id,
@HiveField(1) required this.createdAt,
@HiveField(2) required this.updatedAt,
@HiveField(3) required this.deletedAt,
@HiveField(4) required this.alias,
@HiveField(5) required this.name,
@HiveField(6) required this.description,
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.alias,
required this.name,
required this.description,
final List<SnRealmMember>? members,
@HiveField(7) required this.avatar,
@HiveField(8) required this.banner,
@HiveField(9) required final Map<String, dynamic>? accessPolicy,
@HiveField(10) required this.accountId,
@HiveField(11) required this.isPublic,
@HiveField(12) required this.isCommunity})
required this.avatar,
required this.banner,
required final Map<String, dynamic>? accessPolicy,
required this.accountId,
required this.isPublic,
required this.isCommunity,
this.popularity = 0})
: _members = members,
_accessPolicy = accessPolicy,
super._();
@ -657,25 +657,18 @@ class _$SnRealmImpl extends _SnRealm {
_$$SnRealmImplFromJson(json);
@override
@HiveField(0)
final int id;
@override
@HiveField(1)
final DateTime createdAt;
@override
@HiveField(2)
final DateTime updatedAt;
@override
@HiveField(3)
final DateTime? deletedAt;
@override
@HiveField(4)
final String alias;
@override
@HiveField(5)
final String name;
@override
@HiveField(6)
final String description;
final List<SnRealmMember>? _members;
@override
@ -688,14 +681,11 @@ class _$SnRealmImpl extends _SnRealm {
}
@override
@HiveField(7)
final String? avatar;
@override
@HiveField(8)
final String? banner;
final Map<String, dynamic>? _accessPolicy;
@override
@HiveField(9)
Map<String, dynamic>? get accessPolicy {
final value = _accessPolicy;
if (value == null) return null;
@ -705,18 +695,18 @@ class _$SnRealmImpl extends _SnRealm {
}
@override
@HiveField(10)
final int accountId;
@override
@HiveField(11)
final bool isPublic;
@override
@HiveField(12)
final bool isCommunity;
@override
@JsonKey()
final int popularity;
@override
String toString() {
return 'SnRealm(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, alias: $alias, name: $name, description: $description, members: $members, avatar: $avatar, banner: $banner, accessPolicy: $accessPolicy, accountId: $accountId, isPublic: $isPublic, isCommunity: $isCommunity)';
return 'SnRealm(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, alias: $alias, name: $name, description: $description, members: $members, avatar: $avatar, banner: $banner, accessPolicy: $accessPolicy, accountId: $accountId, isPublic: $isPublic, isCommunity: $isCommunity, popularity: $popularity)';
}
@override
@ -745,7 +735,9 @@ class _$SnRealmImpl extends _SnRealm {
(identical(other.isPublic, isPublic) ||
other.isPublic == isPublic) &&
(identical(other.isCommunity, isCommunity) ||
other.isCommunity == isCommunity));
other.isCommunity == isCommunity) &&
(identical(other.popularity, popularity) ||
other.popularity == popularity));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@ -765,7 +757,8 @@ class _$SnRealmImpl extends _SnRealm {
const DeepCollectionEquality().hash(_accessPolicy),
accountId,
isPublic,
isCommunity);
isCommunity,
popularity);
/// Create a copy of SnRealm
/// with the given fields replaced by the non-null parameter values.
@ -785,65 +778,55 @@ class _$SnRealmImpl extends _SnRealm {
abstract class _SnRealm extends SnRealm {
const factory _SnRealm(
{@HiveField(0) required final int id,
@HiveField(1) required final DateTime createdAt,
@HiveField(2) required final DateTime updatedAt,
@HiveField(3) required final DateTime? deletedAt,
@HiveField(4) required final String alias,
@HiveField(5) required final String name,
@HiveField(6) required final String description,
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final DateTime? deletedAt,
required final String alias,
required final String name,
required final String description,
final List<SnRealmMember>? members,
@HiveField(7) required final String? avatar,
@HiveField(8) required final String? banner,
@HiveField(9) required final Map<String, dynamic>? accessPolicy,
@HiveField(10) required final int accountId,
@HiveField(11) required final bool isPublic,
@HiveField(12) required final bool isCommunity}) = _$SnRealmImpl;
required final String? avatar,
required final String? banner,
required final Map<String, dynamic>? accessPolicy,
required final int accountId,
required final bool isPublic,
required final bool isCommunity,
final int popularity}) = _$SnRealmImpl;
const _SnRealm._() : super._();
factory _SnRealm.fromJson(Map<String, dynamic> json) = _$SnRealmImpl.fromJson;
@override
@HiveField(0)
int get id;
@override
@HiveField(1)
DateTime get createdAt;
@override
@HiveField(2)
DateTime get updatedAt;
@override
@HiveField(3)
DateTime? get deletedAt;
@override
@HiveField(4)
String get alias;
@override
@HiveField(5)
String get name;
@override
@HiveField(6)
String get description;
@override
List<SnRealmMember>? get members;
@override
@HiveField(7)
String? get avatar;
@override
@HiveField(8)
String? get banner;
@override
@HiveField(9)
Map<String, dynamic>? get accessPolicy;
@override
@HiveField(10)
int get accountId;
@override
@HiveField(11)
bool get isPublic;
@override
@HiveField(12)
bool get isCommunity;
@override
int get popularity;
/// Create a copy of SnRealm
/// with the given fields replaced by the non-null parameter values.

View File

@ -2,80 +2,6 @@
part of 'realm.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class SnRealmImplAdapter extends TypeAdapter<_$SnRealmImpl> {
@override
final int typeId = 1;
@override
_$SnRealmImpl read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return _$SnRealmImpl(
id: fields[0] as int,
createdAt: fields[1] as DateTime,
updatedAt: fields[2] as DateTime,
deletedAt: fields[3] as DateTime?,
alias: fields[4] as String,
name: fields[5] as String,
description: fields[6] as String,
avatar: fields[7] as String?,
banner: fields[8] as String?,
accessPolicy: (fields[9] as Map?)?.cast<String, dynamic>(),
accountId: fields[10] as int,
isPublic: fields[11] as bool,
isCommunity: fields[12] as bool,
);
}
@override
void write(BinaryWriter writer, _$SnRealmImpl obj) {
writer
..writeByte(13)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.createdAt)
..writeByte(2)
..write(obj.updatedAt)
..writeByte(3)
..write(obj.deletedAt)
..writeByte(4)
..write(obj.alias)
..writeByte(5)
..write(obj.name)
..writeByte(6)
..write(obj.description)
..writeByte(7)
..write(obj.avatar)
..writeByte(8)
..write(obj.banner)
..writeByte(10)
..write(obj.accountId)
..writeByte(11)
..write(obj.isPublic)
..writeByte(12)
..write(obj.isCommunity)
..writeByte(9)
..write(obj.accessPolicy);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SnRealmImplAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
@ -128,6 +54,7 @@ _$SnRealmImpl _$$SnRealmImplFromJson(Map<String, dynamic> json) =>
accountId: (json['account_id'] as num).toInt(),
isPublic: json['is_public'] as bool,
isCommunity: json['is_community'] as bool,
popularity: (json['popularity'] as num?)?.toInt() ?? 0,
);
Map<String, dynamic> _$$SnRealmImplToJson(_$SnRealmImpl instance) =>
@ -146,4 +73,5 @@ Map<String, dynamic> _$$SnRealmImplToJson(_$SnRealmImpl instance) =>
'account_id': instance.accountId,
'is_public': instance.isPublic,
'is_community': instance.isCommunity,
'popularity': instance.popularity,
};

View File

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

View File

@ -109,11 +109,13 @@ class ChatMessage extends StatelessWidget {
onTap: () {
if (user == null) return;
showPopover(
backgroundColor: Theme.of(context).colorScheme.surface,
backgroundColor:
Theme.of(context).colorScheme.surface,
context: context,
transition: PopoverTransition.other,
bodyBuilder: (context) => SizedBox(
width: math.min(400, MediaQuery.of(context).size.width - 10),
width: math.min(
400, MediaQuery.of(context).size.width - 10),
child: AccountPopoverCard(
data: user,
),
@ -144,11 +146,14 @@ class ChatMessage extends StatelessWidget {
radius: 12,
).padding(right: 8),
Text(
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
(data.sender.nick?.isNotEmpty ?? false)
? data.sender.nick!
: user?.nick ?? 'unknown',
).bold(),
const Gap(8),
Text(
dateFormatter.format(data.createdAt.toLocal()),
dateFormatter
.format(data.createdAt.toLocal()),
).fontSize(13),
],
).height(21),
@ -159,7 +164,8 @@ class ChatMessage extends StatelessWidget {
maxWidth: 480,
),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
borderRadius:
const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
@ -207,9 +213,12 @@ class ChatMessage extends StatelessWidget {
maxHeight: 560,
maxWidth: 480,
minWidth: 480,
padding: padding.copyWith(top: 8),
padding: padding.copyWith(top: 8, left: 48 + padding.left),
),
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(8),
if (!hasMerged && !isCompact)
const Gap(12)
else if (!isCompact)
const Gap(8),
],
),
),
@ -223,7 +232,8 @@ class _ChatMessageText extends StatelessWidget {
final Function(SnChatMessage)? onEdit;
final Function(SnChatMessage)? onDelete;
const _ChatMessageText({required this.data, this.onReply, this.onEdit, this.onDelete});
const _ChatMessageText(
{required this.data, this.onReply, this.onEdit, this.onDelete});
@override
Widget build(BuildContext context) {
@ -237,7 +247,8 @@ class _ChatMessageText extends StatelessWidget {
children: [
SelectionArea(
contextMenuBuilder: (context, editableTextState) {
final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
final List<ContextMenuButtonItem> items =
editableTextState.contextMenuButtonItems;
if (onReply != null) {
items.insert(
@ -286,6 +297,8 @@ class _ChatMessageText extends StatelessWidget {
child: MarkdownTextContent(
content: data.body['text'],
isAutoWarp: true,
isEnlargeSticker:
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
),
),
),

View File

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

View File

@ -44,7 +44,9 @@ class MarkdownTextContent extends StatelessWidget {
Theme.of(context),
).copyWith(
textScaler: textScaler,
p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null,
p: textColor != null
? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor)
: null,
blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
@ -115,7 +117,7 @@ class MarkdownTextContent extends StatelessWidget {
final alias = segments[1];
final st = context.read<SnStickerProvider>();
final sn = context.read<SnNetworkProvider>();
final double size = isEnlargeSticker ? 128 : 32;
final double size = isEnlargeSticker ? 96 : 32;
return Container(
width: size,
height: size,
@ -131,7 +133,8 @@ class MarkdownTextContent extends StatelessWidget {
if (snapshot.hasData) {
return GestureDetector(
child: UniversalImage(
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
sn.getAttachmentUrl(
snapshot.data!.attachment.rid),
fit: BoxFit.contain,
width: size,
height: size,
@ -177,7 +180,9 @@ class MarkdownTextContent extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: attachment.metadata['ratio'] ??
switch (attachment.mimetype.split('/').firstOrNull) {
switch (attachment.mimetype
.split('/')
.firstOrNull) {
'audio' => 16 / 9,
'video' => 16 / 9,
_ => 1,

View File

@ -31,6 +31,7 @@ class AppScaffold extends StatelessWidget {
final AppBar? appBar;
final DrawerCallback? onDrawerChanged;
final DrawerCallback? onEndDrawerChanged;
final bool noBackground;
const AppScaffold({
super.key,
@ -45,6 +46,7 @@ class AppScaffold extends StatelessWidget {
this.endDrawer,
this.onDrawerChanged,
this.onEndDrawerChanged,
this.noBackground = false,
});
@override
@ -52,20 +54,23 @@ class AppScaffold extends StatelessWidget {
final appBarHeight = appBar?.preferredSize.height ?? 0;
final safeTop = MediaQuery.of(context).padding.top;
final content = Column(
children: [
IgnorePointer(
child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0),
),
if (body != null) Expanded(child: body!),
],
);
return Scaffold(
extendBody: true,
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: SizedBox.expand(
child: AppBackground(
isRoot: true,
child: Column(
children: [
IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)),
if (body != null) Expanded(child: body!),
],
),
),
child: noBackground
? content
: AppBackground(isRoot: true, child: content),
),
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
@ -107,11 +112,19 @@ class AppRootScaffold extends StatelessWidget {
final isCollapseDrawer = cfg.drawerIsCollapsed;
final isExpandedDrawer = cfg.drawerIsExpanded;
final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name;
final isShowBottomNavigation = NavigationProvider.kShowBottomNavScreen.contains(routeName)
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
: false;
final isPopable = !NavigationProvider.kAllDestination.map((ele) => ele.screen).contains(routeName);
final routeName = GoRouter.of(context)
.routerDelegate
.currentConfiguration
.last
.route
.name;
final isShowBottomNavigation =
NavigationProvider.kShowBottomNavScreen.contains(routeName)
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
: false;
final isPopable = !NavigationProvider.kAllDestination
.map((ele) => ele.screen)
.contains(routeName);
final innerWidget = isCollapseDrawer
? body
@ -126,7 +139,9 @@ class AppRootScaffold extends StatelessWidget {
),
),
),
child: isExpandedDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(),
child: isExpandedDrawer
? AppNavigationDrawer(elevation: 0)
: AppRailNavigation(),
),
Expanded(child: body),
],
@ -150,7 +165,8 @@ class AppRootScaffold extends StatelessWidget {
children: [
Column(
children: [
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
if (!kIsWeb &&
(Platform.isWindows || Platform.isLinux || Platform.isMacOS))
WindowTitleBarBox(
child: Container(
decoration: BoxDecoration(
@ -164,12 +180,21 @@ class AppRootScaffold extends StatelessWidget {
child: MoveWindow(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
mainAxisAlignment: Platform.isMacOS
? MainAxisAlignment.center
: MainAxisAlignment.start,
children: [
Text(
'Solar Network',
style: GoogleFonts.spaceGrotesk(),
).padding(horizontal: 12, vertical: 5),
Expanded(
child: Text(
'Solar Network',
style: GoogleFonts.spaceGrotesk(),
textAlign: !kIsWeb
? Platform.isMacOS
? TextAlign.center
: null
: null,
).padding(horizontal: 12, vertical: 5),
),
if (!Platform.isMacOS)
Row(
mainAxisSize: MainAxisSize.min,
@ -177,9 +202,12 @@ class AppRootScaffold extends StatelessWidget {
Expanded(child: MoveWindow()),
Row(
children: [
MinimizeWindowButton(colors: windowButtonColor),
MaximizeWindowButton(colors: windowButtonColor),
CloseWindowButton(colors: windowButtonColor),
MinimizeWindowButton(
colors: windowButtonColor),
MaximizeWindowButton(
colors: windowButtonColor),
CloseWindowButton(
colors: windowButtonColor),
],
),
],
@ -192,16 +220,28 @@ class AppRootScaffold extends StatelessWidget {
Expanded(child: innerWidget),
],
),
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
Positioned(
top: safeTop > 0 ? safeTop : 16,
right: 8,
child: NotifyIndicator()),
if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE))
Positioned(bottom: safeBottom > 0 ? safeBottom : 16, left: 0, right: 0, child: ConnectionIndicator())
Positioned(
bottom: safeBottom > 0 ? safeBottom : 16,
left: 0,
right: 0,
child: ConnectionIndicator())
else
Positioned(top: safeTop > 0 ? safeTop : 16, left: 0, right: 0, child: ConnectionIndicator()),
Positioned(
top: safeTop > 0 ? safeTop : 16,
left: 0,
right: 0,
child: ConnectionIndicator()),
],
),
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null,
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
bottomNavigationBar:
isShowBottomNavigation ? AppBottomNavigationBar() : null,
);
}
}

View File

@ -23,7 +23,8 @@ class NotifyIndicator extends StatefulWidget {
State<NotifyIndicator> createState() => _NotifyIndicatorState();
}
class _NotifyIndicatorState extends State<NotifyIndicator> with SingleTickerProviderStateMixin {
class _NotifyIndicatorState extends State<NotifyIndicator>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
@ -101,7 +102,8 @@ class _NotifyIndicatorState extends State<NotifyIndicator> with SingleTickerProv
padding: const EdgeInsets.symmetric(vertical: 16),
width: double.infinity,
constraints: BoxConstraints(
maxWidth: isMobile ? MediaQuery.of(context).size.width - 16 : 360,
maxWidth:
isMobile ? MediaQuery.of(context).size.width - 16 : 360,
),
child: Material(
elevation: 2,
@ -118,7 +120,8 @@ class _NotifyIndicatorState extends State<NotifyIndicator> with SingleTickerProv
),
)
else
Icon(kNotificationTopicIcons[current?.topic] ?? Symbols.notifications),
Icon(kNotificationTopicIcons[current?.topic] ??
Symbols.notifications),
const Gap(16),
Expanded(
child: Column(
@ -126,14 +129,20 @@ class _NotifyIndicatorState extends State<NotifyIndicator> with SingleTickerProv
children: [
Text(
current?.title ?? 'Notification',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.bold,
),
),
if (current?.subtitle?.isNotEmpty ?? false)
Text(
current!.subtitle!,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.bold,
),
),
@ -148,18 +157,25 @@ class _NotifyIndicatorState extends State<NotifyIndicator> with SingleTickerProv
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(DateFormat('HH:mm').format(current?.createdAt.toLocal() ?? DateTime.now()))
.fontSize(12)
.padding(right: 2),
Text(
DateFormat('HH:mm').format(
(current?.createdAt ?? DateTime.now())
.millisecondsSinceEpoch >
0
? (current?.createdAt ?? DateTime.now())
: DateTime.now()),
).fontSize(12).padding(right: 2),
const Gap(6),
if (current?.metadata['image'] != null)
SizedBox(
width: 40,
height: 40,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
borderRadius: const BorderRadius.all(
Radius.circular(8)),
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(current?.metadata['image']),
sn.getAttachmentUrl(
current?.metadata['image']),
fit: BoxFit.cover,
),
),

View File

@ -92,9 +92,10 @@ class OpenablePostItem extends StatelessWidget {
openColor: Colors.transparent,
openElevation: 0,
transitionType: ContainerTransitionType.fade,
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
),
closedColor:
Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
),
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
@ -135,9 +136,11 @@ class PostItem extends StatelessWidget {
final box = context.findRenderObject() as RenderBox?;
final url = 'https://solsynth.dev/posts/${data.id}';
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Share.shareUri(Uri.parse(url), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
Share.shareUri(Uri.parse(url),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
} else {
Share.share(url, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
Share.share(url,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
}
}
@ -155,7 +158,8 @@ class PostItem extends StatelessWidget {
child: MultiProvider(
providers: [
Provider<SnNetworkProvider>(create: (_) => context.read()),
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()),
ChangeNotifierProvider<ConfigProvider>(
create: (_) => context.read()),
],
child: ResponsiveBreakpoints.builder(
breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
@ -183,7 +187,8 @@ class PostItem extends StatelessWidget {
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
} else {
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile);
await FileSaver.instance.saveFile(
name: 'Solar Network Post #${data.id}.png', file: imageFile);
}
await imageFile.delete();
@ -197,7 +202,9 @@ class PostItem extends StatelessWidget {
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
// Video full view
if (showFullPost && data.type == 'video' && ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
if (showFullPost &&
data.type == 'video' &&
ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -217,7 +224,8 @@ class PostItem extends StatelessWidget {
if (onDeleted != null) {}
},
).padding(bottom: 8),
if (data.preload?.video != null) _PostVideoPlayer(data: data).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(
@ -265,7 +273,8 @@ class PostItem extends StatelessWidget {
if (onDeleted != null) {}
},
).padding(horizontal: 12, top: 8, bottom: 8),
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
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),
@ -308,8 +317,13 @@ class PostItem extends StatelessWidget {
],
),
),
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
Text('postArticle')
.tr()
.fontSize(13)
.opacity(0.75)
.padding(horizontal: 24, bottom: 8),
_PostFeaturedComment(data: data, maxWidth: maxWidth)
.padding(horizontal: 12),
_PostBottomAction(
data: data,
showComments: showComments,
@ -324,7 +338,8 @@ class PostItem extends StatelessWidget {
}
final displayableAttachments = data.preload?.attachments
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
?.where((ele) =>
ele?.mediaType != SnMediaType.image || data.type != 'article')
.toList();
final cfg = context.read<ConfigProvider>();
@ -349,9 +364,13 @@ class PostItem extends StatelessWidget {
if (onDeleted != null) onDeleted!();
},
).padding(horizontal: 12, vertical: 8),
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
if (data.body['title'] != null || data.body['description'] != null)
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,
isEnlarge: data.type == 'article' && showFullPost,
@ -365,7 +384,8 @@ class PostItem extends StatelessWidget {
if (data.repostTo != null)
_PostQuoteContent(child: data.repostTo!).padding(
horizontal: 12,
bottom: data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
bottom:
data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
),
if (data.visibility > 0)
_PostVisibilityHint(data: data).padding(
@ -377,7 +397,9 @@ class PostItem extends StatelessWidget {
horizontal: 16,
vertical: 4,
),
if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, top: 4, bottom: 6),
if (data.tags.isNotEmpty)
_PostTagsList(data: data)
.padding(horizontal: 16, top: 4, bottom: 6),
],
),
),
@ -390,12 +412,16 @@ 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))
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'],
).padding(horizontal: 4),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
_PostFeaturedComment(data: data, maxWidth: maxWidth)
.padding(horizontal: 12),
Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Column(
@ -457,7 +483,8 @@ 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),
if (data.type == 'question')
_PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
_PostHeadline(
data: data,
isEnlarge: data.type == 'article',
@ -472,7 +499,8 @@ class PostShareImageWidget extends StatelessWidget {
child: data.repostTo!,
isRelativeDate: false,
).padding(horizontal: 16, bottom: 8),
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
if (data.type != 'article' &&
(data.preload?.attachments?.isNotEmpty ?? false))
StyledWidget(AttachmentList(
data: data.preload!.attachments!,
columned: true,
@ -481,7 +509,8 @@ class PostShareImageWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (data.visibility > 0) _PostVisibilityHint(data: data),
if (data.body['content_truncated'] == true) _PostTruncatedHint(data: data),
if (data.body['content_truncated'] == true)
_PostTruncatedHint(data: data),
],
).padding(horizontal: 16),
_PostBottomAction(
@ -541,7 +570,8 @@ class PostShareImageWidget extends StatelessWidget {
version: QrVersions.auto,
size: 100,
gapless: true,
embeddedImage: AssetImage('assets/icon/icon-light-radius.png'),
embeddedImage:
AssetImage('assets/icon/icon-light-radius.png'),
embeddedImageStyle: QrEmbeddedImageStyle(
size: Size(28, 28),
),
@ -572,9 +602,11 @@ class _PostQuestionHint extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
children: [
Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20),
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)
if (data.body['answer'] == null &&
data.body['reward']?.toDouble() != null)
Text('postQuestionUnansweredWithReward'.tr(args: [
'${data.body['reward']}',
])).opacity(0.75)
@ -610,7 +642,9 @@ class _PostBottomAction extends StatelessWidget {
);
final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty
? data.metric.reactionList.entries.reduce((a, b) => a.value > b.value ? a : b).key
? data.metric.reactionList.entries
.reduce((a, b) => a.value > b.value ? a : b)
.key
: null;
return Row(
@ -624,7 +658,8 @@ class _PostBottomAction extends StatelessWidget {
InkWell(
child: Row(
children: [
if (mostTypicalReaction == null || kTemplateReactions[mostTypicalReaction] == null)
if (mostTypicalReaction == null ||
kTemplateReactions[mostTypicalReaction] == null)
Icon(Symbols.add_reaction, size: 20, color: iconColor)
else
Text(
@ -636,7 +671,8 @@ class _PostBottomAction extends StatelessWidget {
),
),
const Gap(8),
if (data.totalUpvote > 0 && data.totalUpvote >= data.totalDownvote)
if (data.totalUpvote > 0 &&
data.totalUpvote >= data.totalDownvote)
Text('postReactionUpvote').plural(
data.totalUpvote,
)
@ -655,8 +691,12 @@ class _PostBottomAction extends StatelessWidget {
data: data,
onChanged: (value, attr, delta) {
onChanged(data.copyWith(
totalUpvote: attr == 1 ? data.totalUpvote + delta : data.totalUpvote,
totalDownvote: attr == 2 ? data.totalDownvote + delta : data.totalDownvote,
totalUpvote: attr == 1
? data.totalUpvote + delta
: data.totalUpvote,
totalDownvote: attr == 2
? data.totalDownvote + delta
: data.totalDownvote,
metric: data.metric.copyWith(reactionList: value),
));
},
@ -684,6 +724,15 @@ class _PostBottomAction extends StatelessWidget {
);
},
),
InkWell(
child: Row(
children: [
Icon(Symbols.play_circle, size: 20, color: iconColor),
const Gap(8),
Text('postViews').plural(data.totalViews),
],
),
),
],
),
InkWell(
@ -829,7 +878,6 @@ class _PostContentHeader extends StatelessWidget {
await sn.client.delete('/cgi/co/posts/${data.id}', queryParameters: {
'publisherId': data.publisherId,
});
if (!context.mounted) return;
context.showSnackbar('postDeleted'.tr(args: ['#${data.id}']));
} catch (err) {
@ -838,6 +886,25 @@ class _PostContentHeader extends StatelessWidget {
}
}
Future<void> _flagPost(BuildContext context) async {
final confirm = await context.showConfirmDialog(
'flagPost'.tr(),
'flagPostDescription'.tr(),
);
if (!confirm) return;
if (!context.mounted) return;
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/co/posts/${data.id}/flag');
if (!context.mounted) return;
context.showSnackbar('postFlagged'.tr());
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
}
}
@override
Widget build(BuildContext context) {
return Row(
@ -877,8 +944,10 @@ class _PostContentHeader extends StatelessWidget {
const Gap(4),
Text(
isRelativeDate
? RelativeTime(context).format(data.publishedAt ?? data.createdAt)
: DateFormat('y/M/d HH:mm').format(data.publishedAt ?? data.createdAt),
? RelativeTime(context)
.format(data.publishedAt ?? data.createdAt)
: DateFormat('y/M/d HH:mm')
.format(data.publishedAt ?? data.createdAt),
).fontSize(13),
],
).opacity(0.8),
@ -896,8 +965,10 @@ class _PostContentHeader extends StatelessWidget {
const Gap(4),
Text(
isRelativeDate
? RelativeTime(context).format(data.publishedAt ?? data.createdAt)
: DateFormat('y/M/d HH:mm').format(data.publishedAt ?? data.createdAt),
? RelativeTime(context)
.format(data.publishedAt ?? data.createdAt)
: DateFormat('y/M/d HH:mm')
.format(data.publishedAt ?? data.createdAt),
).fontSize(13),
],
).opacity(0.8),
@ -965,7 +1036,7 @@ class _PostContentHeader extends StatelessWidget {
onTap: () {
GoRouter.of(context).pushNamed(
'postEditor',
pathParameters: {'mode': data.typePlural},
pathParameters: {'mode': 'stories'},
queryParameters: {'replying': data.id.toString()},
);
},
@ -981,7 +1052,7 @@ class _PostContentHeader extends StatelessWidget {
onTap: () {
GoRouter.of(context).pushNamed(
'postEditor',
pathParameters: {'mode': data.typePlural},
pathParameters: {'mode': 'stories'},
queryParameters: {'reposting': data.id.toString()},
);
},
@ -1029,6 +1100,18 @@ class _PostContentHeader extends StatelessWidget {
children: [
const Icon(Symbols.flag),
const Gap(16),
Text('flagPostAction').tr(),
],
),
onTap: () {
_flagPost(context);
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.report),
const Gap(16),
Text('report').tr(),
],
),
@ -1068,7 +1151,8 @@ class _PostContentBody extends StatelessWidget {
if (data.body['content'] == null) return const SizedBox.shrink();
final content = MarkdownTextContent(
isAutoWarp: data.type == 'story',
isEnlargeSticker: true,
isEnlargeSticker:
RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: data.body['content'],
attachments: data.preload?.attachments,
@ -1117,10 +1201,12 @@ class _PostQuoteContent extends StatelessWidget {
onDeleted: () {},
).padding(bottom: 4),
_PostContentBody(data: child),
if (child.visibility > 0) _PostVisibilityHint(data: child).padding(top: 4),
if (child.visibility > 0)
_PostVisibilityHint(data: child).padding(top: 4),
],
).padding(horizontal: 16),
if (child.type != 'article' && (child.preload?.attachments?.isNotEmpty ?? false))
if (child.type != 'article' &&
(child.preload?.attachments?.isNotEmpty ?? false))
ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8),
@ -1271,7 +1357,9 @@ class _PostTruncatedHint extends StatelessWidget {
const Gap(4),
Text('postReadEstimate').tr(args: [
'${Duration(
seconds: (data.body['content_length'] as num).toDouble() * 60 ~/ kHumanReadSpeed,
seconds: (data.body['content_length'] as num).toDouble() *
60 ~/
kHumanReadSpeed,
).inSeconds}s',
]),
],
@ -1310,7 +1398,8 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
// 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']}');
final resp =
await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
_isAnswer = true;
setState(() => _featuredComment = SnPost.fromJson(resp.data));
return;
@ -1318,9 +1407,11 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
'take': 1,
});
final resp = await sn.client.get(
'/cgi/co/posts/${widget.data.id}/replies/featured',
queryParameters: {
'take': 1,
});
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
} catch (err) {
if (!mounted) return;
@ -1349,7 +1440,9 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
width: double.infinity,
child: Material(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: _isAnswer ? Colors.green.withOpacity(0.5) : 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: () {
@ -1369,11 +1462,17 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Gap(2),
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, size: 20),
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),
_isAnswer
? 'postQuestionAnswerTitle'
: 'postFeaturedComment',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 15),
).tr(),
],
),
@ -1511,7 +1610,8 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
}
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
setState(
() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -1534,11 +1634,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
children: [
const Icon(Symbols.book_4_spark, size: 24),
const Gap(16),
Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(),
Text('postGetInsightTitle',
style: Theme.of(context).textTheme.titleLarge)
.tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
const Gap(4),
Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20),
Text('postGetInsightDescription',
style: Theme.of(context).textTheme.bodySmall)
.tr()
.padding(horizontal: 20),
const Gap(4),
if (_response == null)
Expanded(
@ -1556,12 +1661,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
leading: const Icon(Symbols.info),
title: Text('aiThinkingProcess'.tr()),
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainerHigh,
minTileHeight: 32,
children: [
SelectableText(
_thinkingProcess!,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic),
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontStyle: FontStyle.italic),
).padding(horizontal: 20, vertical: 8),
],
).padding(vertical: 8),
@ -1598,7 +1707,8 @@ class _PostVideoPlayer extends StatelessWidget {
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
child: AttachmentItem(
data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
),
),
);

View File

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

View File

@ -31,19 +31,27 @@ class _PostPollState extends State<PostPoll> {
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');
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;
context.showErrorDialog(err);
// ignore because it may not found
} finally {
setState(() => _isBusy = false);
}
@ -59,8 +67,9 @@ class _PostPollState extends State<PostPoll> {
'answer': option.id,
});
if (!mounted) return;
context.showSnackbar('pollAnswered'.tr());
HapticFeedback.heavyImpact();
_answeredChoice = option.id;
_refreshPoll();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -78,15 +87,24 @@ class _PostPollState extends State<PostPoll> {
for (final option in _poll.options)
Stack(
children: [
Container(
height: 60,
width: MediaQuery.of(context).size.width * (_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble(),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
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)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
minTileHeight: 60,
leading: _answeredChoice == option.id ? const Icon(Symbols.circle, fill: 1) : const Icon(Symbols.circle),
leading: _answeredChoice == option.id
? const Icon(Symbols.circle, fill: 1)
: const Icon(Symbols.circle),
title: Text(option.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -95,14 +113,18 @@ class _PostPollState extends State<PostPoll> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('pollVotes'.plural(_poll.metric.byOptions[option.id] ?? 0)),
Text(
'pollVotes'
.plural(_poll.metric.byOptions[option.id] ?? 0),
),
Text(' · ').padding(horizontal: 4),
Text(
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%',
),
],
),
if (option.description.isNotEmpty) Text(option.description),
if (option.description.isNotEmpty)
Text(option.description),
],
),
onTap: _isBusy ? null : () => _voteForOption(option),

View File

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

96
lib/widgets/updater.dart Normal file
View File

@ -0,0 +1,96 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_update/azhon_app_update.dart';
import 'package:flutter_app_update/update_model.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/markdown_content.dart';
import 'package:url_launcher/url_launcher_string.dart';
class VersionUpdatePopup extends StatelessWidget {
const VersionUpdatePopup({super.key});
void _update(BuildContext context) async {
if (kIsWeb) return;
final config = context.read<ConfigProvider>();
if (Platform.isAndroid) {
final model = UpdateModel(
'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk',
'solian-app-release-${config.updatableVersion!}.apk',
'ic_launcher',
'https://apps.apple.com/us/app/solian/id6499032345',
);
AzhonAppUpdate.update(model);
context.showSnackbar('updateOngoing'.tr());
return;
}
final resp = await Dio(
BaseOptions(
sendTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60),
),
).get(
'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest',
);
launchUrlString(resp.data?['html_url']);
}
@override
Widget build(BuildContext context) {
final config = context.watch<ConfigProvider>();
return Column(
children: [
Row(
children: [
const Icon(Icons.update),
const Gap(16),
Text('update')
.tr()
.textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Row(
children: [
Expanded(
child: Text(
config.updatableVersion ?? 'unknown'.tr(),
style: GoogleFonts.robotoMono(),
),
),
ElevatedButton(
style: ButtonStyle(
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -3,
),
),
onPressed: () => _update(context),
child: Text('update').tr(),
),
],
).padding(horizontal: 20),
const Divider(height: 1).padding(vertical: 8),
Expanded(
child: SingleChildScrollView(
child: MarkdownTextContent(
content: config.updatableChangelog ?? 'No changelog',
).padding(horizontal: 20),
),
)
],
);
}
}

View File

@ -12,9 +12,11 @@
#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 <local_notifier/local_notifier_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 <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
@ -37,6 +39,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
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) local_notifier_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin");
local_notifier_plugin_register_with_registrar(local_notifier_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);
@ -46,6 +51,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) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
tray_manager_plugin_register_with_registrar(tray_manager_registrar);

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import gal
import hotkey_manager_macos
import in_app_review
import livekit_client
import local_notifier
import media_kit_libs_macos_video
import media_kit_video
import package_info_plus
@ -30,6 +31,7 @@ import screen_brightness_macos
import share_plus
import shared_preferences_foundation
import sqflite_darwin
import sqlite3_flutter_libs
import tray_manager
import url_launcher_macos
import video_compress
@ -52,6 +54,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
@ -61,6 +64,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"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))

View File

@ -13,59 +13,59 @@ PODS:
- FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- Firebase/Analytics (11.7.0):
- Firebase/Analytics (11.8.0):
- Firebase/Core
- Firebase/Core (11.7.0):
- Firebase/Core (11.8.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 11.7.0)
- Firebase/CoreOnly (11.7.0):
- FirebaseCore (~> 11.7.0)
- Firebase/Messaging (11.7.0):
- FirebaseAnalytics (~> 11.8.0)
- Firebase/CoreOnly (11.8.0):
- FirebaseCore (~> 11.8.0)
- Firebase/Messaging (11.8.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.7.0)
- firebase_analytics (11.4.2):
- Firebase/Analytics (= 11.7.0)
- FirebaseMessaging (~> 11.8.0)
- firebase_analytics (11.4.3):
- Firebase/Analytics (= 11.8.0)
- firebase_core
- FlutterMacOS
- firebase_core (3.11.0):
- Firebase/CoreOnly (~> 11.7.0)
- firebase_core (3.12.0):
- Firebase/CoreOnly (~> 11.8.0)
- FlutterMacOS
- firebase_messaging (15.2.2):
- Firebase/CoreOnly (~> 11.7.0)
- Firebase/Messaging (~> 11.7.0)
- firebase_messaging (15.2.3):
- Firebase/CoreOnly (~> 11.8.0)
- Firebase/Messaging (~> 11.8.0)
- firebase_core
- FlutterMacOS
- FirebaseAnalytics (11.7.0):
- FirebaseAnalytics/AdIdSupport (= 11.7.0)
- FirebaseCore (~> 11.7.0)
- FirebaseAnalytics (11.8.0):
- FirebaseAnalytics/AdIdSupport (= 11.8.0)
- FirebaseCore (~> 11.8.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.7.0):
- FirebaseCore (~> 11.7.0)
- FirebaseAnalytics/AdIdSupport (11.8.0):
- FirebaseCore (~> 11.8.0)
- FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.7.0)
- GoogleAppMeasurement (= 11.8.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseCore (11.7.0):
- FirebaseCoreInternal (~> 11.7.0)
- FirebaseCore (11.8.1):
- FirebaseCoreInternal (~> 11.8.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreInternal (11.7.0):
- FirebaseCoreInternal (11.8.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseInstallations (11.7.0):
- FirebaseCore (~> 11.7.0)
- FirebaseInstallations (11.8.0):
- FirebaseCore (~> 11.8.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.7.0):
- FirebaseCore (~> 11.7.0)
- FirebaseMessaging (11.8.0):
- FirebaseCore (~> 11.8.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -86,21 +86,21 @@ PODS:
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleAppMeasurement (11.7.0):
- GoogleAppMeasurement/AdIdSupport (= 11.7.0)
- GoogleAppMeasurement (11.8.0):
- GoogleAppMeasurement/AdIdSupport (= 11.8.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.7.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.0)
- GoogleAppMeasurement/AdIdSupport (11.8.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.8.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.7.0):
- GoogleAppMeasurement/WithoutAdIdSupport (11.8.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
@ -142,10 +142,12 @@ PODS:
- HotKey
- in_app_review (2.0.0):
- FlutterMacOS
- livekit_client (2.3.6):
- livekit_client (2.4.0):
- flutter_webrtc
- FlutterMacOS
- WebRTC-SDK (= 125.6422.06)
- local_notifier (0.1.0):
- FlutterMacOS
- media_kit_libs_macos_video (1.0.4):
- FlutterMacOS
- media_kit_native_event_loop (1.0.0):
@ -177,6 +179,25 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.49.1):
- sqlite3/common (= 3.49.1)
- sqlite3/common (3.49.1)
- sqlite3/dbstatvtab (3.49.1):
- sqlite3/common
- sqlite3/fts5 (3.49.1):
- sqlite3/common
- sqlite3/perf-threadsafe (3.49.1):
- sqlite3/common
- sqlite3/rtree (3.49.1):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.49.0)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
- tray_manager (0.0.1):
- FlutterMacOS
- url_launcher_macos (0.0.1):
@ -206,6 +227,7 @@ DEPENDENCIES:
- 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`)
- local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
@ -216,6 +238,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`)
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/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`)
@ -237,6 +260,7 @@ SPEC REPOS:
- OrderedSet
- PromisesObjC
- SAMKeychain
- sqlite3
- WebRTC-SDK
EXTERNAL SOURCES:
@ -276,6 +300,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
livekit_client:
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
local_notifier:
:path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos
media_kit_libs_macos_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
media_kit_native_event_loop:
@ -296,6 +322,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
sqlite3_flutter_libs:
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin
tray_manager:
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
url_launcher_macos:
@ -313,27 +341,28 @@ SPEC CHECKSUMS:
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4
firebase_analytics: 41d88c024a7756462a803e36236ba74f24cdc2c5
firebase_core: 751d3d919b95d4ae46ab049d0d64d42d4eec086b
firebase_messaging: cc174f19945e9541e140e3cb0118448e59b38c6c
FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec
FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4
FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881
FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9
FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
firebase_analytics: 1a71372a9735d7046d2c69db848a8d178f9fb587
firebase_core: 68e1d27035b096239f147a041643e14e156f1481
firebase_messaging: 89b5e0e28413dd878a58d2f286cdc03887b5d467
FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
livekit_client: 0ad107154753a5a76802d2222c040223ad049499
livekit_client: 2e766be2c3ee6274a8e2633b356b98b5eb842987
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
@ -348,6 +377,8 @@ SPEC CHECKSUMS:
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f

View File

@ -59,6 +59,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

@ -5,31 +5,26 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
url: "https://pub.dev"
source: hosted
version: "76.0.0"
version: "80.0.0"
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: e051259913915ea5bc8fe18664596bea08592fd123930605d562969cd7315fcd
sha256: "401dd18096f5eaa140404ccbbbf346f83c850e6f27049698a7ee75a3488ddb32"
url: "https://pub.dev"
source: hosted
version: "1.3.51"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.3"
version: "1.3.52"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
url: "https://pub.dev"
source: hosted
version: "6.11.0"
version: "7.3.0"
animations:
dependency: "direct main"
description:
@ -50,10 +45,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a"
sha256: "528579c7e4579719f04b21eeeeddfd73a18b31dabc22766893b7d1be7f49b967"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "4.0.3"
args:
dependency: transitive
description:
@ -66,10 +61,10 @@ packages:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.11.0"
version: "2.12.0"
bitsdojo_window:
dependency: "direct main"
description:
@ -114,10 +109,10 @@ packages:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
build:
dependency: transitive
description:
@ -191,7 +186,7 @@ packages:
source: hosted
version: "3.4.1"
cached_network_image_platform_interface:
dependency: transitive
dependency: "direct main"
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
@ -226,10 +221,18 @@ packages:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
@ -250,10 +253,10 @@ packages:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
code_builder:
dependency: transitive
description:
@ -266,10 +269,10 @@ packages:
dependency: "direct main"
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
version: "1.19.1"
connectivity_plus:
dependency: transitive
description:
@ -346,18 +349,18 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
url: "https://pub.dev"
source: hosted
version: "2.3.8"
version: "3.0.1"
dart_webrtc:
dependency: "direct main"
description:
name: dart_webrtc
sha256: "3b3ff59c66cbc1577ed0f28d7005b5163555208fb1697a42207424ab8baa27c5"
sha256: b34e90bc82f33c1023cf98661369c37bccd648c8a4cf882a875d9f5d8bbef694
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "1.5.2+hotfix.1"
dbus:
dependency: transitive
description:
@ -414,6 +417,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
drift:
dependency: "direct main"
description:
name: drift
sha256: "97d5832657d49f26e7a8e07de397ddc63790b039372878d5117af816d0fdb5cb"
url: "https://pub.dev"
source: hosted
version: "2.25.1"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: f1db88482dbb016b9bbddddf746d5d0a6938b156ff20e07320052981f97388cc
url: "https://pub.dev"
source: hosted
version: "2.25.2"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922"
url: "https://pub.dev"
source: hosted
version: "0.2.4"
dropdown_button2:
dependency: "direct main"
description:
@ -474,18 +501,18 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.2"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
file:
dependency: transitive
description:
@ -498,10 +525,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: "3d57312a53746ed4eb8c843dc50372454bbda37dd0c01a4d40fedc83e2ce4921"
sha256: "6f6bfa8797f296965bdc3e1f702574ab49a540c19b9237b401e7c2b25dfe594c"
url: "https://pub.dev"
source: hosted
version: "8.3.5"
version: "9.0.0"
file_saver:
dependency: "direct main"
description:
@ -538,42 +565,42 @@ packages:
dependency: transitive
description:
name: file_selector_windows
sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4"
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
url: "https://pub.dev"
source: hosted
version: "0.9.3+3"
version: "0.9.3+4"
firebase_analytics:
dependency: "direct main"
description:
name: firebase_analytics
sha256: "47428047a0778f72af53a3c7cb5d556e1cb25e2327cc8aa40d544971dc6245b2"
sha256: "6abce50b79729d8a13c3d4ae05ac612d5ef2f57394330bc5e581ca0e762325f4"
url: "https://pub.dev"
source: hosted
version: "11.4.2"
version: "11.4.3"
firebase_analytics_platform_interface:
dependency: transitive
description:
name: firebase_analytics_platform_interface
sha256: "1076f4b041f76143e14878c70f0758f17fe5910c0cd992db9e93bd3c3584512b"
sha256: cd9ae65870bf23ab7e63a04fe9c1b38522fd3556a8c32288afd3f5cb10d4b8f4
url: "https://pub.dev"
source: hosted
version: "4.3.2"
version: "4.3.3"
firebase_analytics_web:
dependency: transitive
description:
name: firebase_analytics_web
sha256: "8f6dd64ea6d28b7f5b9e739d183a9e1c7f17027794a3e9aba1879621d42426ef"
sha256: "5654ed7e39d7a8099e60748924327159785512d78d913e965f9ca93c533af910"
url: "https://pub.dev"
source: hosted
version: "0.5.10+8"
version: "0.5.10+9"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "93dc4dd12f9b02c5767f235307f609e61ed9211047132d07f9e02c668f0bfc33"
sha256: "6a4ea0f1d533443c8afc3d809cd36a4e2b8f2e2e711f697974f55bb31d71d1b8"
url: "https://pub.dev"
source: hosted
version: "3.11.0"
version: "3.12.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -586,34 +613,34 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: "0e13c80f0de8acaa5d0519cbe23c8b4cc138a2d5d508b5755c861bdfc9762678"
sha256: e47f5c2776de018fa19bc9f6f723df136bc75cdb164d64b65305babd715c8e41
url: "https://pub.dev"
source: hosted
version: "2.20.0"
version: "2.21.0"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "3dee3b0cbfe719e64773cb7d1cad57c58b2235a8c136f5715fe733a54058c783"
sha256: "8755a083a20bac4485e8b46d223f6f2eab34e659a76a75f8cf3cded53bc98a15"
url: "https://pub.dev"
source: hosted
version: "15.2.2"
version: "15.2.3"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: e9ea726b9bb864fc6223bb66422bd9877b9973ae51967754a769b0d01e201c1e
sha256: "8cc771079677460de53ad8fcca5bc3074d58c5fc4f9d89b19585e5bfd9c64292"
url: "https://pub.dev"
source: hosted
version: "4.6.2"
version: "4.6.3"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "5f7b40e8bf861a37f8b8196e347d8a919750421a45f0b45d1bb74e98fa72726e"
sha256: caa73059b0396c97f691683c4cfc3f897c8543801579b7dd4851c431d8e4e091
url: "https://pub.dev"
source: hosted
version: "3.10.2"
version: "3.10.3"
fixnum:
dependency: transitive
description:
@ -780,10 +807,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7"
sha256: edb09c35ee9230c4b03f13dd45bb3a276d0801865f0a4650b7e2a3bba61a803a
url: "https://pub.dev"
source: hosted
version: "2.4.4"
version: "2.4.5"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -838,18 +865,18 @@ packages:
dependency: "direct main"
description:
name: flutter_webrtc
sha256: "572df3de6c828e571db4b75b4a96a15c2f34fa3d420a84438f44a3158b22e81a"
sha256: "6ea3a86d95b61cfe42d5715426d355b3cece6c88d0119de428d56f6c653811ce"
url: "https://pub.dev"
source: hosted
version: "0.12.9"
version: "0.12.11"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e"
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
url: "https://pub.dev"
source: hosted
version: "2.5.7"
version: "2.5.8"
freezed_annotation:
dependency: "direct main"
description:
@ -914,30 +941,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hive:
dependency: "direct main"
description:
name: hive
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
hive_flutter:
dependency: "direct main"
description:
name: hive_flutter
sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc
url: "https://pub.dev"
source: hosted
version: "1.1.0"
hive_generator:
dependency: "direct dev"
description:
name: hive_generator
sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
home_widget:
dependency: "direct main"
description:
@ -1030,10 +1033,10 @@ packages:
dependency: transitive
description:
name: image
sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6"
sha256: "13d3349ace88f12f4a0d175eb5c12dcdd39d35c4c109a8a13dfeb6d0bd9e31c3"
url: "https://pub.dev"
source: hosted
version: "4.5.2"
version: "4.5.3"
image_picker:
dependency: "direct main"
description:
@ -1043,13 +1046,13 @@ packages:
source: hosted
version: "1.1.2"
image_picker_android:
dependency: transitive
dependency: "direct main"
description:
name: image_picker_android
sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c
sha256: "82652a75e3dd667a91187769a6a2cc81bd8c111bbead698d8e938d2b63e5e89a"
url: "https://pub.dev"
source: hosted
version: "0.8.12+20"
version: "0.8.12+21"
image_picker_for_web:
dependency: transitive
description:
@ -1083,7 +1086,7 @@ packages:
source: hosted
version: "0.2.1+2"
image_picker_platform_interface:
dependency: transitive
dependency: "direct main"
description:
name: image_picker_platform_interface
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
@ -1150,26 +1153,26 @@ packages:
dependency: "direct dev"
description:
name: json_serializable
sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c
sha256: "81f04dee10969f89f604e1249382d46b97a1ccad53872875369622b5bfc9e58a"
url: "https://pub.dev"
source: hosted
version: "6.9.0"
version: "6.9.4"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev"
source: hosted
version: "10.0.7"
version: "10.0.8"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.8"
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
@ -1198,10 +1201,18 @@ packages:
dependency: "direct main"
description:
name: livekit_client
sha256: "0cfb2f48eff7a93ea8927696dc6f218aebd2fcd1fcc1b1a7b2f53ff3597fdb52"
sha256: "753bbf484c6b70f10f3dc1dc808dfe3755f472d80eb9682323cff07ad8e2609d"
url: "https://pub.dev"
source: hosted
version: "2.3.6"
version: "2.4.0"
local_notifier:
dependency: "direct main"
description:
name: local_notifier
sha256: f6cfc933c6fbc961f4e52b5c880f68e41b2d3cd29aad557cc654fd211093a025
url: "https://pub.dev"
source: hosted
version: "0.1.6"
logging:
dependency: transitive
description:
@ -1210,14 +1221,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.3-main.0"
markdown:
dependency: "direct main"
description:
@ -1238,10 +1241,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
@ -1342,10 +1345,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.16.0"
mime:
dependency: "direct main"
description:
@ -1414,10 +1417,10 @@ packages:
dependency: "direct main"
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.9.1"
path_parsing:
dependency: transitive
description:
@ -1486,26 +1489,26 @@ packages:
dependency: "direct main"
description:
name: permission_handler
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
url: "https://pub.dev"
source: hosted
version: "11.3.1"
version: "11.4.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1"
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
url: "https://pub.dev"
source: hosted
version: "12.0.13"
version: "12.1.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98
url: "https://pub.dev"
source: hosted
version: "9.4.5"
version: "9.4.6"
permission_handler_html:
dependency: transitive
description:
@ -1518,10 +1521,10 @@ packages:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.2.3"
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
@ -1534,10 +1537,10 @@ packages:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.dev"
source: hosted
version: "6.0.2"
version: "6.1.0"
photo_view:
dependency: "direct main"
description:
@ -1554,14 +1557,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.6"
platform_detect:
dependency: transitive
description:
name: platform_detect
sha256: a62f99417fc4fa2d099ce0ccdbb1bd3977920f2a64292c326271f049d4bc3a4f
url: "https://pub.dev"
source: hosted
version: "2.1.0"
plugin_platform_interface:
dependency: transitive
description:
@ -1642,6 +1637,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
receive_sharing_intent:
dependency: "direct main"
description:
@ -1774,10 +1777,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: ea86be7b7114f9e94fddfbb52649e59a03d6627ccd2387ebddcd6624719e9f16
sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22
url: "https://pub.dev"
source: hosted
version: "2.4.5"
version: "2.4.6"
shared_preferences_foundation:
dependency: transitive
description:
@ -1806,10 +1809,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_web
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
@ -1867,10 +1870,10 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "2.0.0"
source_helper:
dependency: transitive
description:
@ -1883,10 +1886,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.10.1"
sprintf:
dependency: transitive
description:
@ -1899,34 +1902,34 @@ packages:
dependency: transitive
description:
name: sqflite
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.2"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.1"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
url: "https://pub.dev"
source: hosted
version: "2.5.4+6"
version: "2.5.5"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
version: "2.4.1+1"
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
@ -1935,22 +1938,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d"
url: "https://pub.dev"
source: hosted
version: "2.7.4"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "57fafacd815c981735406215966ff7caaa8eab984b094f52e692accefcbd9233"
url: "https://pub.dev"
source: hosted
version: "0.5.30"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
url: "https://pub.dev"
source: hosted
version: "0.41.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.4"
stream_transform:
dependency: transitive
description:
@ -1963,10 +1990,10 @@ packages:
dependency: transitive
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.1"
styled_widget:
dependency: "direct main"
description:
@ -1987,26 +2014,26 @@ packages:
dependency: "direct main"
description:
name: synchronized
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
url: "https://pub.dev"
source: hosted
version: "3.3.0+3"
version: "3.3.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.3"
version: "0.7.4"
timing:
dependency: transitive
description:
@ -2195,10 +2222,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev"
source: hosted
version: "14.3.0"
version: "14.3.1"
volume_controller:
dependency: transitive
description:
@ -2259,18 +2286,18 @@ packages:
dependency: transitive
description:
name: webrtc_interface
sha256: "10fc6dc0ac16f909f5e434c18902415211d759313c87261f1e4ec5b4f6a04c26"
sha256: e05f00091c9c70a15bab4ccb1b6c46d9a16a6075002f02cfac3641eccb05e25d
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.1+hotfix.1"
win32:
dependency: transitive
description:
name: win32
sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
sha256: b89e6e24d1454e149ab20fbb225af58660f0c0bf4475544650700d8e2da54aef
url: "https://pub.dev"
source: hosted
version: "5.10.1"
version: "5.11.0"
win32_registry:
dependency: transitive
description:
@ -2282,10 +2309,11 @@ packages:
workmanager:
dependency: "direct main"
description:
name: workmanager
sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00
url: "https://pub.dev"
source: hosted
path: workmanager
ref: main
resolved-ref: "4ce065135dc1b91fee918f81596b42a56850391d"
url: "https://github.com/fluttercommunity/flutter_workmanager.git"
source: git
version: "0.5.2"
xdg_directories:
dependency: transitive
@ -2312,5 +2340,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.6.0 <4.0.0"
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0"

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.3.2+67
version: 2.3.2+72
environment:
sdk: ^3.5.4
@ -59,7 +59,7 @@ dependencies:
relative_time: ^5.0.0
image_picker: ^1.1.2
cross_file: ^0.3.4+2
file_picker: ^8.1.6 # pinned due to compile failed on android, https://github.com/miguelpruivo/flutter_file_picker/issues/1643
file_picker: ^9.0.0 # pinned due to compile failed on android, https://github.com/miguelpruivo/flutter_file_picker/issues/1643
croppy: ^1.3.1
flutter_expandable_fab: ^2.3.0
dropdown_button2: ^2.3.9
@ -72,8 +72,6 @@ dependencies:
collection: ^1.18.0
mime: ^2.0.0
web_socket_channel: ^3.0.1
hive: ^2.2.3
hive_flutter: ^1.1.0
swipe_to: ^1.0.6
firebase_core: ^3.8.0
firebase_messaging: ^15.1.5
@ -105,7 +103,11 @@ dependencies:
flutter_svg: ^2.0.16
home_widget: ^0.7.0
receive_sharing_intent: ^1.8.1
workmanager: ^0.5.2
workmanager:
git:
url: https://github.com/fluttercommunity/flutter_workmanager.git
path: workmanager
ref: main
flutter_app_update: ^3.2.2
in_app_review: ^2.0.10
version: ^3.0.2
@ -120,6 +122,12 @@ dependencies:
xml: ^6.5.0
tray_manager: ^0.3.2
hotkey_manager: ^0.2.3
image_picker_android: ^0.8.12+20
cached_network_image_platform_interface: ^4.1.1
image_picker_platform_interface: ^2.10.1
drift: ^2.25.1
drift_flutter: ^0.2.4
local_notifier: ^0.1.6
dev_dependencies:
flutter_test:
@ -131,13 +139,13 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
build_runner: ^2.4.13
build_runner: ^2.4.15
freezed: ^2.5.7
json_serializable: ^6.8.0
icons_launcher: ^3.0.0
flutter_native_splash: ^2.4.2
hive_generator: ^2.0.1
flutter_launcher_icons: ^0.14.1
drift_dev: ^2.25.2
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

13631
web/drift_worker.dart.js Normal file

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More