Compare commits
122 Commits
2.2.2+57
...
153eabcbf2
| Author | SHA1 | Date | |
|---|---|---|---|
| 153eabcbf2 | |||
| 6d0145c335 | |||
| 81a79f9476 | |||
| 537f404fe0 | |||
| eb29f76b9a | |||
| 56816dc060 | |||
| 899d5f3e5e | |||
| c8c455bb57 | |||
| 5468fc0748 | |||
| 78516abf2e | |||
| 0424f98eb5 | |||
| 2188b8b2e2 | |||
| 0bf614a75c | |||
| 9f21f744a4 | |||
| b94cda6205 | |||
| 3c0e4046a4 | |||
| 338c22a606 | |||
| 25dd895e0d | |||
| ea9ef9e82a | |||
| edd86eda77 | |||
| 671b857a79 | |||
| 408fd0f35e | |||
| 30184d08b1 | |||
|
|
95f257c47a | ||
|
|
41297c6712 | ||
| a8e0ade0c8 | |||
| 3338e699c4 | |||
| e07da3efa5 | |||
| 4f7f015250 | |||
| 2a4c15d0dc | |||
| 70ef894ec5 | |||
| bb9179d5f9 | |||
| e2ecb573a2 | |||
| 8cb5dff498 | |||
| a5629975ed | |||
| 972b304969 | |||
| e8ded55055 | |||
| 04875eb164 | |||
| 54a59aa470 | |||
| 365f330629 | |||
| a7829d15b2 | |||
| a3868a4281 | |||
|
|
1d1d61d60c | ||
| 03c2491587 | |||
| 2c1adc988c | |||
| c0fbee55e4 | |||
| 6e544c0b6c | |||
| 7d56c5ef31 | |||
| c2df1af16d | |||
| a8143c6453 | |||
| 04065061e0 | |||
| 226eb452e5 | |||
| a6715b0872 | |||
| 43e3404dbb | |||
| c91cf7c813 | |||
|
|
9cd1cad695 | ||
|
|
dde280833b | ||
| 42ac12b53e | |||
| 63567bf708 | |||
| 5d3cadefef | |||
| 251fbb2503 | |||
| 0b31d32217 | |||
| 5ddd4fed2e | |||
| 48b6d5f6c1 | |||
| b83b0b5efb | |||
| cb24bd953d | |||
| 4937dee182 | |||
| d612097bb1 | |||
| 058d668b6b | |||
| 8b19462c3a | |||
| 0a381ef09b | |||
| 9b84e912b2 | |||
| b3254e0f2f | |||
| f0a3bbe023 | |||
| df81c84438 | |||
| 8b12395fca | |||
| cb2b71d194 | |||
| 7ed508e2bb | |||
| dad869967e | |||
| 2d5b3b554e | |||
| 74882116e3 | |||
| a97c3bce3a | |||
| 1aa70827dc | |||
| fe028860e9 | |||
| a2d2ce4d38 | |||
| 167c11b9eb | |||
| 8cb3933fcc | |||
| 3818328afe | |||
| 11627e2455 | |||
| 3f82c06ff8 | |||
| 2350f59131 | |||
| 9fe7c9530a | |||
| 52f1826e91 | |||
| 28a4c86dbf | |||
| 85e48ce03b | |||
| efef61a8ea | |||
| 10ead95af9 | |||
| 838ee4d55d | |||
| 13e42429a9 | |||
| c6ce3fe2b7 | |||
| ae9a7eb0fd | |||
| 5d6fb2442f | |||
| 5a85985534 | |||
| c80499db03 | |||
| b8dcdb2315 | |||
| b7b921f1f4 | |||
| 319d5c7d7f | |||
| 4b5b001739 | |||
| db8871a455 | |||
| 38dcaa6066 | |||
| 03275b46ca | |||
| cf3b482fef | |||
| aa4c04d4ef | |||
| 73b82f65e4 | |||
| 9471fe40fe | |||
| 0d1e18735e | |||
| 8bb62b5992 | |||
| 1e8a6dea5b | |||
| 5c2804cc4d | |||
| 0dbb8f132a | |||
| 3395f3dbd0 | |||
| d258ba776e |
87
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
87
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal 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
|
||||||
83
.github/ISSUE_TEMPLATE/bug_report_zh.yaml
vendored
Normal file
83
.github/ISSUE_TEMPLATE/bug_report_zh.yaml
vendored
Normal 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
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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
|
||||||
59
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
59
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal 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
|
||||||
49
.github/ISSUE_TEMPLATE/feature_request_zh.yaml
vendored
Normal file
49
.github/ISSUE_TEMPLATE/feature_request_zh.yaml
vendored
Normal 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
|
||||||
25
.github/workflows/nightly.yml
vendored
25
.github/workflows/nightly.yml
vendored
@@ -38,4 +38,27 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build-output-windows
|
name: build-output-windows
|
||||||
path: build/windows/x64/runner/Release
|
path: build/windows/x64/runner/Release
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
cache: true
|
||||||
|
- run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y ninja-build libgtk-3-dev
|
||||||
|
sudo apt-get install libmpv-dev mpv
|
||||||
|
sudo apt-get install libayatana-appindicator3-dev
|
||||||
|
sudo apt-get install keybinder-3.0
|
||||||
|
- run: flutter pub get
|
||||||
|
- run: flutter build linux
|
||||||
|
- name: Archive production artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-output-linux
|
||||||
|
path: build/linux/x64/release/bundle
|
||||||
27
README.md
Normal file
27
README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Solar Network
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Hello there! Welcome to the main repository of the HyperNet (also known as the Solar Network). The code here is mainly about the frontend app (also known as Solian). But you can still post issues here to get help and request new features!
|
||||||
|
|
||||||
|
## Sub Projects
|
||||||
|
|
||||||
|
HyperNet, the Solar Network is a microservices project in which the backends are stored in separate repositories. Here is a simple index for it.
|
||||||
|
|
||||||
|
- The Core, Gateway: [Nexus](https://github.com/Solsynth/HyperNet.Nexus)
|
||||||
|
- The Auth Service: [Passport](https://github.com/Solsynth/HyperNet.Passport)
|
||||||
|
- The Posting Service: [Interactive](https://github.com/Solsynth/HyperNet.Interactive)
|
||||||
|
- The Messaging Service: [Messaging](https://github.com/Solsynth/HyperNet.Messaging)
|
||||||
|
- The Wallet Service: [Wallet](https://github.com/Solsynth/HyperNet.Wallet)
|
||||||
|
- The Crawler: [Reader](https://github.com/Solsynth/HyperNet.Reader)
|
||||||
|
- Some others may not be listed, you can search in the organization with `HyperNet.` the prefix of all HyperNet projects.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
For those people who want to know the tech stack of this project, the frontend was built by Flutter, which provides the cross-platform ability.
|
||||||
|
|
||||||
|
The backend was built in Go and PostgreSQL with our very own microservice framework included in the nexus.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
The readme will be updated in the future, to be determined. For now, you can check out the link of this repository to learn more on our official website.
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
android:label="Solian"
|
android:label="Solian"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:requestLegacyExternalStorage="true">
|
android:requestLegacyExternalStorage="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
@@ -26,7 +25,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleInstance"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class CheckInWidget : GlanceAppWidget() {
|
|||||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
|
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
|
||||||
.registerTypeAdapter(Instant::class.java, InstantAdapter())
|
.registerTypeAdapter(Instant::class.java, InstantAdapter())
|
||||||
.create()
|
.create()
|
||||||
val resultTierSymbols = listOf("大凶", "凶", "中平", "吉", "大吉")
|
val resultTierSymbols = listOf("Bad", "Poor", "Medium", "Good", "Great")
|
||||||
|
|
||||||
val prefs = currentState.preferences
|
val prefs = currentState.preferences
|
||||||
val checkInRaw: String? = prefs.getString("pas_check_in_record", null)
|
val checkInRaw: String? = prefs.getString("pas_check_in_record", null)
|
||||||
@@ -120,7 +120,7 @@ class CheckInWidget : GlanceAppWidget() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "You haven't checked in today",
|
text = "You haven't divined today",
|
||||||
style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface)
|
style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ post {
|
|||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
{
|
{
|
||||||
"alias": "AteChip",
|
"alias": "Deadge",
|
||||||
"name": "Cat ate chips",
|
"name": "Dead",
|
||||||
"attachment_id": "d0b692cc64054463",
|
"attachment_id": "pcbFd0u4zgdM39HM",
|
||||||
"pack_id": 2
|
"pack_id": 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
api/Paperclip/Stickers/Get Sticker Packs.bru
Normal file
11
api/Paperclip/Stickers/Get Sticker Packs.bru
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Get Sticker Packs
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{endpoint}}/cgi/uc/stickers/packs
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
15
api/Paperclip/Stickers/Get Stickers.bru
Normal file
15
api/Paperclip/Stickers/Get Stickers.bru
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
meta {
|
||||||
|
name: Get Stickers
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{endpoint}}/cgi/uc/stickers?take=10
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
params:query {
|
||||||
|
take: 10
|
||||||
|
}
|
||||||
@@ -15,11 +15,11 @@ body:json {
|
|||||||
"client_id": "{{third_client_id}}",
|
"client_id": "{{third_client_id}}",
|
||||||
"client_secret":"{{third_client_tk}}",
|
"client_secret":"{{third_client_tk}}",
|
||||||
"type": "general",
|
"type": "general",
|
||||||
"subject": "Merry Christmas!",
|
"subject": "新年快乐!",
|
||||||
"subtitle": "一条来自 Solar Network 团队的信息",
|
"subtitle": "一条来自 Solar Network 团队的信息",
|
||||||
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
|
"content": "今天是农历正月初一,小羊祝您新年快乐 🎉",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"image": "6EqsYQwmFRCkbmhR"
|
"image": "D2EDbcrsTugs3xk5"
|
||||||
},
|
},
|
||||||
"priority": 10
|
"priority": 10
|
||||||
}
|
}
|
||||||
|
|||||||
23
api/Passport/Developer Notify One User.bru
Normal file
23
api/Passport/Developer Notify One User.bru
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
meta {
|
||||||
|
name: Developer Notify One User
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/id/dev/notify/328
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"client_id": "{{third_client_id}}",
|
||||||
|
"client_secret":"{{third_client_tk}}",
|
||||||
|
"type": "general",
|
||||||
|
"subject": "处理该发布者 @vedal987 的决定",
|
||||||
|
"subtitle": "一条来自 Solar Network 客户支持的信息",
|
||||||
|
"content": "您的发布者违反了我们用户协议中的「禁止冒充他人」的相关条例,经管理决定,将相关内容隐藏。冒充他人的判定无论作者是否有主观意志,只要造成了误解我们就有责任处理。希望您能理解,本次决定未作出任何帐号相关的连带处罚。",
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
20
api/Wallet/Create Order.bru
Normal file
20
api/Wallet/Create Order.bru
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
meta {
|
||||||
|
name: Create Order
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/wa/orders
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"client_id": "highland-mc",
|
||||||
|
"client_secret": "(3^DLAvo3v",
|
||||||
|
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
|
||||||
|
"amount": 500
|
||||||
|
}
|
||||||
|
}
|
||||||
21
api/Wallet/Create Transaction.bru
Normal file
21
api/Wallet/Create Transaction.bru
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
meta {
|
||||||
|
name: Create Transaction
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/wa/transactions
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"client_id": "alphabot",
|
||||||
|
"client_secret": "_uR0sVnHTh",
|
||||||
|
"remark": "新年红包",
|
||||||
|
"amount": 150,
|
||||||
|
"payee_id": 18
|
||||||
|
}
|
||||||
|
}
|
||||||
20
api/Wallet/Get Order.bru
Normal file
20
api/Wallet/Get Order.bru
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
meta {
|
||||||
|
name: Get Order
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{endpoint}}/cgi/wa/orders/4
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"client_id": "highland-mc",
|
||||||
|
"client_secret": "(3^DLAvo3v",
|
||||||
|
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
|
||||||
|
"amount": 500
|
||||||
|
}
|
||||||
|
}
|
||||||
20
api/Wallet/Get Transaction.bru
Normal file
20
api/Wallet/Get Transaction.bru
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
meta {
|
||||||
|
name: Get Transaction
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{endpoint}}/cgi/wa/transactions/67
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"client_id": "highland-mc",
|
||||||
|
"client_secret": "(3^DLAvo3v",
|
||||||
|
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
|
||||||
|
"amount": 500
|
||||||
|
}
|
||||||
|
}
|
||||||
11
api/WatchTower/Run Database Maintenance.bru
Normal file
11
api/WatchTower/Run Database Maintenance.bru
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Run Database Maintenance
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/wt/maintenance/database
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
BIN
assets/icon/tray-icon.ico
Normal file
BIN
assets/icon/tray-icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/icon/tray-icon.png
Normal file
BIN
assets/icon/tray-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
@@ -17,6 +17,9 @@
|
|||||||
"screenAccountProfileEdit": "Edit Profile",
|
"screenAccountProfileEdit": "Edit Profile",
|
||||||
"screenAbuseReport": "Abuse Reports",
|
"screenAbuseReport": "Abuse Reports",
|
||||||
"screenSettings": "Settings",
|
"screenSettings": "Settings",
|
||||||
|
"screenAccountSettings": "Account Settings",
|
||||||
|
"screenFactorSettings": "Auth Factors",
|
||||||
|
"screenAccountWallet": "Wallet",
|
||||||
"screenNews": "News",
|
"screenNews": "News",
|
||||||
"screenAlbum": "Album",
|
"screenAlbum": "Album",
|
||||||
"screenChat": "Chat",
|
"screenChat": "Chat",
|
||||||
@@ -24,6 +27,7 @@
|
|||||||
"screenChatNew": "New Channel",
|
"screenChatNew": "New Channel",
|
||||||
"screenRealm": "Realm",
|
"screenRealm": "Realm",
|
||||||
"screenRealmManage": "Edit Realm",
|
"screenRealmManage": "Edit Realm",
|
||||||
|
"screenRealmDiscovery": "Realm Discovery",
|
||||||
"screenRealmNew": "New Realm",
|
"screenRealmNew": "New Realm",
|
||||||
"screenNotification": "Notification",
|
"screenNotification": "Notification",
|
||||||
"screenPostSearch": "Search Posts",
|
"screenPostSearch": "Search Posts",
|
||||||
@@ -104,8 +108,18 @@
|
|||||||
},
|
},
|
||||||
"loginEnterPassword": "Enter the code",
|
"loginEnterPassword": "Enter the code",
|
||||||
"loginSuccess": "Logged in as {}",
|
"loginSuccess": "Logged in as {}",
|
||||||
|
"authFactorDelete": "Delete Auth Factor",
|
||||||
|
"authFactorDeleteDescription": "Are you sure you want delete auth factor {}?",
|
||||||
"authFactorPassword": "Password",
|
"authFactorPassword": "Password",
|
||||||
|
"authFactorPasswordDescription": "The password you set when you registered.",
|
||||||
"authFactorEmail": "Email verification code",
|
"authFactorEmail": "Email verification code",
|
||||||
|
"authFactorEmailDescription": "An one-time code sent to the email address you set when you registered.",
|
||||||
|
"authFactorTOTP": "Time-based OTP",
|
||||||
|
"authFactorTOTPDescription": "A one-time code generated by a TOTP authenticator such as Google Authenticator or Authy.",
|
||||||
|
"authFactorInAppNotify": "In-app notification",
|
||||||
|
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
|
||||||
|
"authFactorAdd": "Add a factor",
|
||||||
|
"authFactorAddSubtitle": "Provide another way to login your account.",
|
||||||
"accountIntroTitle": "Hello there!",
|
"accountIntroTitle": "Hello there!",
|
||||||
"accountIntroSubtitle": "Pick an option below to get started.",
|
"accountIntroSubtitle": "Pick an option below to get started.",
|
||||||
"accountLogout": "Logout",
|
"accountLogout": "Logout",
|
||||||
@@ -114,8 +128,14 @@
|
|||||||
"accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.",
|
"accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.",
|
||||||
"accountPublishers": "Your publishers",
|
"accountPublishers": "Your publishers",
|
||||||
"accountPublishersSubtitle": "Manage your publish identities.",
|
"accountPublishersSubtitle": "Manage your publish identities.",
|
||||||
|
"accountSettings": "Account Settings",
|
||||||
|
"accountSettingsSubtitle": "Manage your account and make it yours.",
|
||||||
"accountProfileEdit": "Edit your profile",
|
"accountProfileEdit": "Edit your profile",
|
||||||
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
|
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
|
||||||
|
"accountWallet": "Wallet",
|
||||||
|
"accountWalletSubtitle": "View your balance and transactions.",
|
||||||
|
"factorSettings": "Auth Factors",
|
||||||
|
"factorSettingsSubtitle": "Manage your authentication factors.",
|
||||||
"accountProfileEditApplied": "Profile modification applied.",
|
"accountProfileEditApplied": "Profile modification applied.",
|
||||||
"publishersNew": "New Publisher",
|
"publishersNew": "New Publisher",
|
||||||
"publisherNewSubtitle": "Create a new publisher identity.",
|
"publisherNewSubtitle": "Create a new publisher identity.",
|
||||||
@@ -135,9 +155,12 @@
|
|||||||
"fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
|
"fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
|
||||||
"writePostTypeStory": "Post a story",
|
"writePostTypeStory": "Post a story",
|
||||||
"writePostTypeArticle": "Write an article",
|
"writePostTypeArticle": "Write an article",
|
||||||
|
"writePostTypeQuestion": "Ask a question",
|
||||||
|
"writePostTypeVideo": "Post a video",
|
||||||
"fieldPostPublisher": "Post publisher",
|
"fieldPostPublisher": "Post publisher",
|
||||||
"fieldPostContent": "What happened?!",
|
"fieldPostContent": "What happened?!",
|
||||||
"fieldPostTitle": "Title",
|
"fieldPostTitle": "Title",
|
||||||
|
"fieldPostQuestionReward": "Answer Rewards (Source Points)",
|
||||||
"fieldPostDescription": "Description",
|
"fieldPostDescription": "Description",
|
||||||
"fieldPostTags": "Tags",
|
"fieldPostTags": "Tags",
|
||||||
"fieldPostCategories": "Categories",
|
"fieldPostCategories": "Categories",
|
||||||
@@ -147,9 +170,9 @@
|
|||||||
"postPosted": "Post has been posted.",
|
"postPosted": "Post has been posted.",
|
||||||
"postPublishedAt": "Published At",
|
"postPublishedAt": "Published At",
|
||||||
"postPublishedUntil": "Published Until",
|
"postPublishedUntil": "Published Until",
|
||||||
"postEditingNotice": "You're about to editing a post that posted {}.",
|
"postEditingNotice": "You're about to editing a post that posted by {}.",
|
||||||
"postReplyingNotice": "You're about to reply to a post that posted {}.",
|
"postReplyingNotice": "You're about to reply to a post that posted by {}.",
|
||||||
"postRepostingNotice": "You're about to repost a post that posted {}.",
|
"postRepostingNotice": "You're about to repost a post that posted by {}.",
|
||||||
"postReact": "React",
|
"postReact": "React",
|
||||||
"postReactions": "Reactions of Post",
|
"postReactions": "Reactions of Post",
|
||||||
"postReactionUpvote": {
|
"postReactionUpvote": {
|
||||||
@@ -180,6 +203,9 @@
|
|||||||
"other": "{} comments"
|
"other": "{} comments"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "Appearance",
|
"settingsAppearance": "Appearance",
|
||||||
|
"settingsDisplayLanguage": "Display Language",
|
||||||
|
"settingsDisplayLanguageDescription": "Set the application language.",
|
||||||
|
"settingsDisplayLanguageSystem": "Follow System",
|
||||||
"settingsAppBarTransparent": "Transparent App Bar",
|
"settingsAppBarTransparent": "Transparent App Bar",
|
||||||
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
|
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
|
||||||
"settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
|
"settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
|
||||||
@@ -219,6 +245,8 @@
|
|||||||
"settingsMisc": "Misc",
|
"settingsMisc": "Misc",
|
||||||
"settingsMiscAbout": "About",
|
"settingsMiscAbout": "About",
|
||||||
"settingsMiscAboutDescription": "View the version information of Solian.",
|
"settingsMiscAboutDescription": "View the version information of Solian.",
|
||||||
|
"settingsAccountLanguage": "Account Language",
|
||||||
|
"settingsAccountLanguageDescription": "Set the language for email, notification, and other account-related content.",
|
||||||
"sensitiveContent": "Sensitive Content",
|
"sensitiveContent": "Sensitive Content",
|
||||||
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
||||||
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
||||||
@@ -305,6 +333,7 @@
|
|||||||
"addAttachmentFromRandomId": "Link via RID",
|
"addAttachmentFromRandomId": "Link via RID",
|
||||||
"attachmentDetailInfo": "Attachment details",
|
"attachmentDetailInfo": "Attachment details",
|
||||||
"attachmentPastedImage": "Pasted Image",
|
"attachmentPastedImage": "Pasted Image",
|
||||||
|
"attachmentInsertedImage": "Inserted Image",
|
||||||
"attachmentInsertLink": "Insert Link",
|
"attachmentInsertLink": "Insert Link",
|
||||||
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
|
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
|
||||||
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
||||||
@@ -391,7 +420,7 @@
|
|||||||
"callMessageEnded": "Call lasted {}",
|
"callMessageEnded": "Call lasted {}",
|
||||||
"callMessageStarted": "Call started",
|
"callMessageStarted": "Call started",
|
||||||
"dailyCheckIn": "Check In",
|
"dailyCheckIn": "Check In",
|
||||||
"dailyCheckInNone": "You haven't checked in today",
|
"dailyCheckInNone": "You haven't divined today",
|
||||||
"dailyCheckAction": "Check in right now!",
|
"dailyCheckAction": "Check in right now!",
|
||||||
"dailyCheckDetail": "Can't understand the symbol? Master, help me understand it!",
|
"dailyCheckDetail": "Can't understand the symbol? Master, help me understand it!",
|
||||||
"dailyCheckDetailTitle": "{}'s fortune details",
|
"dailyCheckDetailTitle": "{}'s fortune details",
|
||||||
@@ -519,6 +548,7 @@
|
|||||||
"termAcceptNextWithAgree": "By clicking the \"Next\", it means you agree to our terms and its updates.",
|
"termAcceptNextWithAgree": "By clicking the \"Next\", it means you agree to our terms and its updates.",
|
||||||
"unauthorized": "Unauthorized",
|
"unauthorized": "Unauthorized",
|
||||||
"unauthorizedDescription": "Login to explore the entire Solar Network.",
|
"unauthorizedDescription": "Login to explore the entire Solar Network.",
|
||||||
|
"projectDetail": "Project Details",
|
||||||
"serviceStatus": "Service Status",
|
"serviceStatus": "Service Status",
|
||||||
"termRelated": "Related Terms",
|
"termRelated": "Related Terms",
|
||||||
"appDetails": "App Details",
|
"appDetails": "App Details",
|
||||||
@@ -532,11 +562,15 @@
|
|||||||
"postImageShareAds": "Explore posts on the Solar Network",
|
"postImageShareAds": "Explore posts on the Solar Network",
|
||||||
"postShare": "Share",
|
"postShare": "Share",
|
||||||
"postShareImage": "Share via Image",
|
"postShareImage": "Share via Image",
|
||||||
|
"postGetInsight": "Get Insight",
|
||||||
|
"postGetInsightTitle": "AI Insight",
|
||||||
|
"postGetInsightDescription": "AI may make mistakes, check important information.",
|
||||||
"appInitializing": "Initializing",
|
"appInitializing": "Initializing",
|
||||||
"poweredBy": "Powered by {}",
|
"poweredBy": "Powered by {}",
|
||||||
"shareIntent": "Share",
|
"shareIntent": "Share",
|
||||||
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
||||||
"shareIntentPostStory": "Post a Story",
|
"shareIntentPostStory": "Post a Story",
|
||||||
|
"shareIntentSendChannel": "Share to Channel",
|
||||||
"updateAvailable": "Update Available",
|
"updateAvailable": "Update Available",
|
||||||
"updateOngoing": "Updating, please wait...",
|
"updateOngoing": "Updating, please wait...",
|
||||||
"custom": "Custom",
|
"custom": "Custom",
|
||||||
@@ -549,6 +583,8 @@
|
|||||||
"colorSchemeWhite": "White",
|
"colorSchemeWhite": "White",
|
||||||
"colorSchemeBlack": "Black",
|
"colorSchemeBlack": "Black",
|
||||||
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
||||||
|
"postFeaturedComment": "Featured Comment",
|
||||||
|
"postCategory": "Category",
|
||||||
"postCategoryTechnology": "Technology",
|
"postCategoryTechnology": "Technology",
|
||||||
"postCategoryGaming": "Gaming",
|
"postCategoryGaming": "Gaming",
|
||||||
"postCategoryLife": "Life",
|
"postCategoryLife": "Life",
|
||||||
@@ -565,5 +601,121 @@
|
|||||||
"newsReadingFromReader": "You're reading from HyperNet.Reader",
|
"newsReadingFromReader": "You're reading from HyperNet.Reader",
|
||||||
"newsReadingFromOriginal": "You're reading the original article",
|
"newsReadingFromOriginal": "You're reading the original article",
|
||||||
"newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.",
|
"newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.",
|
||||||
"newsToday": "Today's News"
|
"newsToday": "Today's News",
|
||||||
|
"totpPostSetup": "One More Thing",
|
||||||
|
"totpPostSetupDescription": "Scan the QR Code below with Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden or any of kind of authenticator app which supports TOTP.",
|
||||||
|
"totpNeverShare": "Never share this QR Code",
|
||||||
|
"needHelp": "Need Help?",
|
||||||
|
"needHelpLaunch": "Check out our Goatpedia!",
|
||||||
|
"walletCreate": "Create a Wallet",
|
||||||
|
"walletCreateSubtitle": "Create a wallet to start using Source Points",
|
||||||
|
"walletCreatePassword": "Set a payment password for your new wallet below",
|
||||||
|
"walletCurrencyShort": "SRC",
|
||||||
|
"walletCurrency": {
|
||||||
|
"one": "{} Source Point",
|
||||||
|
"other": "{} Source Points"
|
||||||
|
},
|
||||||
|
"aiThinkingProcess": "AI Thinking Process",
|
||||||
|
"accountSettingsApplied": "Account settings have been applied.",
|
||||||
|
"trayMenuExit": "Exit",
|
||||||
|
"postQuestionUnanswered": "Unanswered Question",
|
||||||
|
"postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
|
||||||
|
"postQuestionAnswered": "Answered Question",
|
||||||
|
"postQuestionAnswerSelect": "Select as Answer",
|
||||||
|
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
|
||||||
|
"postVideoUpload": "Upload Video",
|
||||||
|
"realmJoin": "Join Realm",
|
||||||
|
"realmCommunityHint": "This realm is a community realm, you can freely join.",
|
||||||
|
"realmCommunityPublicChannelsHint": "The public channels in this realm",
|
||||||
|
"realmCommunityPublishersHint": "The publishers in this realm",
|
||||||
|
"realmJoined": "Joined realm {}.",
|
||||||
|
"join": "Join",
|
||||||
|
"pollEditorNew": "New Poll",
|
||||||
|
"pollEditorEdit": "Edit Poll",
|
||||||
|
"pollEditorDelete": "Delete Poll",
|
||||||
|
"pollEditorDeleteDescription": "Are you sure you want to delete this poll? This operation is irreversible.",
|
||||||
|
"pollEditorUnlink": "Unlink Poll",
|
||||||
|
"pollOptionAdd": "Add Option",
|
||||||
|
"pollOptionName": "Option Name",
|
||||||
|
"pollLinkExisting": "Link existing poll",
|
||||||
|
"pollAnswered": "Answered the poll.",
|
||||||
|
"pollVotes": {
|
||||||
|
"one": "{} vote",
|
||||||
|
"other": "{} votes"
|
||||||
|
},
|
||||||
|
"publisherDelete": "Delete Publisher {}",
|
||||||
|
"publisherDeleteDescription": "Are you sure you want to delete this publisher? This operation is irreversible.",
|
||||||
|
"channelIsPublic": "Public Channel",
|
||||||
|
"channelIsPublicDescription": "The channel is public, anyone can join.",
|
||||||
|
"channelIsCommunity": "Community Channel",
|
||||||
|
"channelIsCommunityDescription": "Currently, community channel has nothing special yet.",
|
||||||
|
"realmIsPublic": "Public Realm",
|
||||||
|
"realmIsPublicDescription": "The realm is public, anyone can join.",
|
||||||
|
"realmIsCommunity": "Community Realm",
|
||||||
|
"realmIsCommunityDescription": "Community realm will be displayed on the discover page.",
|
||||||
|
"realmLeave": "Leave Realm",
|
||||||
|
"realmLeaveDescription": "Leave the current realm and delete the realm's identity.",
|
||||||
|
"checkInResultTier1": "Worst",
|
||||||
|
"checkInResultTier2": "Worse",
|
||||||
|
"checkInResultTier3": "Normal",
|
||||||
|
"checkInResultTier4": "Better",
|
||||||
|
"checkInResultTier5": "Best",
|
||||||
|
"flagPostAction": "Flag the Post",
|
||||||
|
"flagPost": "Flag objectionable content",
|
||||||
|
"flagPostDescription": "If flagged users takes 50% or more of the views, the post will be collapsed. You cannot revoke the action.",
|
||||||
|
"flaggedPost": "Post has been flagged.",
|
||||||
|
"postViews": {
|
||||||
|
"zero": "No views",
|
||||||
|
"one": "{} view",
|
||||||
|
"other": "{} views"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
"screenAccountProfileEdit": "编辑资料",
|
"screenAccountProfileEdit": "编辑资料",
|
||||||
"screenAbuseReport": "滥用检举",
|
"screenAbuseReport": "滥用检举",
|
||||||
"screenSettings": "设置",
|
"screenSettings": "设置",
|
||||||
|
"screenAccountSettings": "账号设置",
|
||||||
|
"screenFactorSettings": "验证因子",
|
||||||
|
"screenAccountWallet": "钱包",
|
||||||
"screenNews": "新闻",
|
"screenNews": "新闻",
|
||||||
"screenAlbum": "相册",
|
"screenAlbum": "相册",
|
||||||
"screenChat": "聊天",
|
"screenChat": "聊天",
|
||||||
@@ -22,6 +25,7 @@
|
|||||||
"screenChatNew": "新建聊天频道",
|
"screenChatNew": "新建聊天频道",
|
||||||
"screenRealm": "领域",
|
"screenRealm": "领域",
|
||||||
"screenRealmManage": "编辑领域",
|
"screenRealmManage": "编辑领域",
|
||||||
|
"screenRealmDiscovery": "发现领域",
|
||||||
"screenRealmNew": "新建领域",
|
"screenRealmNew": "新建领域",
|
||||||
"screenNotification": "通知",
|
"screenNotification": "通知",
|
||||||
"screenPostSearch": "搜索帖子",
|
"screenPostSearch": "搜索帖子",
|
||||||
@@ -88,8 +92,18 @@
|
|||||||
},
|
},
|
||||||
"loginEnterPassword": "验证代码",
|
"loginEnterPassword": "验证代码",
|
||||||
"loginSuccess": "登录为 {}",
|
"loginSuccess": "登录为 {}",
|
||||||
|
"authFactorDelete": "删除验证因子",
|
||||||
|
"authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
|
||||||
"authFactorPassword": "密码",
|
"authFactorPassword": "密码",
|
||||||
|
"authFactorPasswordDescription": "注册时选择设置的密码。",
|
||||||
"authFactorEmail": "电邮一次性验证码",
|
"authFactorEmail": "电邮一次性验证码",
|
||||||
|
"authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
|
||||||
|
"authFactorTOTP": "时序验证码",
|
||||||
|
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
|
||||||
|
"authFactorInAppNotify": "应用内通知验证码",
|
||||||
|
"authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
|
||||||
|
"authFactorAdd": "添加新验证因子",
|
||||||
|
"authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
|
||||||
"accountIntroTitle": "喜欢您来!",
|
"accountIntroTitle": "喜欢您来!",
|
||||||
"accountIntroSubtitle": "登陆以探索更广大的世界。",
|
"accountIntroSubtitle": "登陆以探索更广大的世界。",
|
||||||
"accountLogout": "退出登录",
|
"accountLogout": "退出登录",
|
||||||
@@ -98,8 +112,14 @@
|
|||||||
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
|
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
|
||||||
"accountPublishers": "你的发布者",
|
"accountPublishers": "你的发布者",
|
||||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||||
|
"accountSettings": "帐号设置",
|
||||||
|
"accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
|
||||||
"accountProfileEdit": "编辑资料",
|
"accountProfileEdit": "编辑资料",
|
||||||
"accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
|
"accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
|
||||||
|
"accountWallet": "钱包",
|
||||||
|
"accountWalletSubtitle": "查看你的余额和交易记录。",
|
||||||
|
"factorSettings": "验证因子",
|
||||||
|
"factorSettingsSubtitle": "管理你的登陆验证方式。",
|
||||||
"accountProfileEditApplied": "个人资料修改已被应用。",
|
"accountProfileEditApplied": "个人资料修改已被应用。",
|
||||||
"publishersNew": "新发布者",
|
"publishersNew": "新发布者",
|
||||||
"publisherNewSubtitle": "创建一个新的公共身份。",
|
"publisherNewSubtitle": "创建一个新的公共身份。",
|
||||||
@@ -119,9 +139,12 @@
|
|||||||
"fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
|
"fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
|
||||||
"writePostTypeStory": "发动态",
|
"writePostTypeStory": "发动态",
|
||||||
"writePostTypeArticle": "写文章",
|
"writePostTypeArticle": "写文章",
|
||||||
|
"writePostTypeQuestion": "提问题",
|
||||||
|
"writePostTypeVideo": "发视频",
|
||||||
"fieldPostPublisher": "帖子发布者",
|
"fieldPostPublisher": "帖子发布者",
|
||||||
"fieldPostContent": "发生什么事了?!",
|
"fieldPostContent": "发生什么事了?!",
|
||||||
"fieldPostTitle": "标题",
|
"fieldPostTitle": "标题",
|
||||||
|
"fieldPostQuestionReward": "回答奖励源点",
|
||||||
"fieldPostDescription": "描述",
|
"fieldPostDescription": "描述",
|
||||||
"fieldPostTags": "标签",
|
"fieldPostTags": "标签",
|
||||||
"fieldPostCategories": "分类",
|
"fieldPostCategories": "分类",
|
||||||
@@ -178,6 +201,9 @@
|
|||||||
"other": "{} 条评论"
|
"other": "{} 条评论"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "外观",
|
"settingsAppearance": "外观",
|
||||||
|
"settingsDisplayLanguage": "显示语言",
|
||||||
|
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
|
||||||
|
"settingsDisplayLanguageSystem": "跟随系统",
|
||||||
"settingsBackgroundImage": "背景图片",
|
"settingsBackgroundImage": "背景图片",
|
||||||
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
|
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
|
||||||
"settingsBackgroundImageClear": "清除现存背景图",
|
"settingsBackgroundImageClear": "清除现存背景图",
|
||||||
@@ -217,6 +243,8 @@
|
|||||||
"settingsMisc": "杂项",
|
"settingsMisc": "杂项",
|
||||||
"settingsMiscAbout": "关于",
|
"settingsMiscAbout": "关于",
|
||||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||||
|
"settingsAccountLanguage": "帐号偏好语言",
|
||||||
|
"settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。",
|
||||||
"sensitiveContent": "敏感内容",
|
"sensitiveContent": "敏感内容",
|
||||||
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
||||||
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
||||||
@@ -303,6 +331,7 @@
|
|||||||
"addAttachmentFromRandomId": "通过访问 ID 链接",
|
"addAttachmentFromRandomId": "通过访问 ID 链接",
|
||||||
"attachmentDetailInfo": "附件详细信息",
|
"attachmentDetailInfo": "附件详细信息",
|
||||||
"attachmentPastedImage": "粘贴的图片",
|
"attachmentPastedImage": "粘贴的图片",
|
||||||
|
"attachmentInsertedImage": "插入的图片",
|
||||||
"attachmentInsertLink": "插入连接",
|
"attachmentInsertLink": "插入连接",
|
||||||
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
||||||
@@ -517,6 +546,7 @@
|
|||||||
"termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。",
|
"termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。",
|
||||||
"unauthorized": "未登陆",
|
"unauthorized": "未登陆",
|
||||||
"unauthorizedDescription": "登陆以探索整个 Solar Network。",
|
"unauthorizedDescription": "登陆以探索整个 Solar Network。",
|
||||||
|
"projectDetail": "项目详情",
|
||||||
"serviceStatus": "服务状态",
|
"serviceStatus": "服务状态",
|
||||||
"termRelated": "相关条款",
|
"termRelated": "相关条款",
|
||||||
"appDetails": "应用程序详情",
|
"appDetails": "应用程序详情",
|
||||||
@@ -530,11 +560,15 @@
|
|||||||
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
|
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
|
||||||
"postShare": "分享",
|
"postShare": "分享",
|
||||||
"postShareImage": "分享帖图",
|
"postShareImage": "分享帖图",
|
||||||
|
"postGetInsight": "获取见解",
|
||||||
|
"postGetInsightTitle": "AI 见解",
|
||||||
|
"postGetInsightDescription": "AI 可能会出错,检查信息真实性。",
|
||||||
"appInitializing": "正在初始化",
|
"appInitializing": "正在初始化",
|
||||||
"poweredBy": "由 {} 提供支持",
|
"poweredBy": "由 {} 提供支持",
|
||||||
"shareIntent": "分享",
|
"shareIntent": "分享",
|
||||||
"shareIntentDescription": "您想对您分享的内容做些什么?",
|
"shareIntentDescription": "您想对您分享的内容做些什么?",
|
||||||
"shareIntentPostStory": "发布动态",
|
"shareIntentPostStory": "发布动态",
|
||||||
|
"shareIntentSendChannel": "分享到聊天频道",
|
||||||
"updateAvailable": "检测到更新可用",
|
"updateAvailable": "检测到更新可用",
|
||||||
"updateOngoing": "正在更新,请稍后……",
|
"updateOngoing": "正在更新,请稍后……",
|
||||||
"custom": "自定义",
|
"custom": "自定义",
|
||||||
@@ -547,6 +581,8 @@
|
|||||||
"colorSchemeWhite": "白色",
|
"colorSchemeWhite": "白色",
|
||||||
"colorSchemeBlack": "黑色",
|
"colorSchemeBlack": "黑色",
|
||||||
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
||||||
|
"postFeaturedComment": "精选评论",
|
||||||
|
"postCategory": "分类",
|
||||||
"postCategoryTechnology": "技术",
|
"postCategoryTechnology": "技术",
|
||||||
"postCategoryGaming": "游戏",
|
"postCategoryGaming": "游戏",
|
||||||
"postCategoryLife": "生活",
|
"postCategoryLife": "生活",
|
||||||
@@ -563,5 +599,121 @@
|
|||||||
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
|
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
|
||||||
"newsReadingFromOriginal": "你正在阅读原始文章",
|
"newsReadingFromOriginal": "你正在阅读原始文章",
|
||||||
"newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。",
|
"newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。",
|
||||||
"newsToday": "快讯"
|
"newsToday": "快讯",
|
||||||
|
"totpPostSetup": "还有一件事",
|
||||||
|
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的验证器扫描本 QR Code 来添加。",
|
||||||
|
"totpNeverShare": "永远不要分享这个 QR Code",
|
||||||
|
"needHelp": "需要帮助?",
|
||||||
|
"needHelpLaunch": "查看我们的山羊维基!",
|
||||||
|
"walletCreate": "创建钱包",
|
||||||
|
"walletCreateSubtitle": "创建于一个钱包来开始使用源点。",
|
||||||
|
"walletCreatePassword": "在下方设置你的付款密码",
|
||||||
|
"walletCurrencyShort": "源点",
|
||||||
|
"walletCurrency": {
|
||||||
|
"one": "{} 源点",
|
||||||
|
"other": "{} 源点"
|
||||||
|
},
|
||||||
|
"aiThinkingProcess": "AI 思考过程",
|
||||||
|
"accountSettingsApplied": "帐号设置已应用。",
|
||||||
|
"trayMenuExit": "退出",
|
||||||
|
"postQuestionUnanswered": "未解答的问题",
|
||||||
|
"postQuestionUnansweredWithReward": "未解答的问题,悬赏源点 {}",
|
||||||
|
"postQuestionAnswered": "已解答的问题",
|
||||||
|
"postQuestionAnswerTitle": "精选解答",
|
||||||
|
"postQuestionAnswerSelect": "选择解答",
|
||||||
|
"postQuestionAnswerSelected": "解答已选择,奖励已发放。",
|
||||||
|
"postVideoUpload": "上传视频",
|
||||||
|
"realmJoin": "加入领域",
|
||||||
|
"realmCommunityHint": "该领域是一个社区领域,你可以自由加入。",
|
||||||
|
"realmCommunityPublicChannelsHint": "该领域包含的公共频道",
|
||||||
|
"realmCommunityPublishersHint": "该领域的发布者",
|
||||||
|
"realmJoined": "已加入领域 {}。",
|
||||||
|
"join": "加入",
|
||||||
|
"pollEditorNew": "新投票",
|
||||||
|
"pollEditorEdit": "编辑投票",
|
||||||
|
"pollEditorDelete": "删除投票",
|
||||||
|
"pollEditorDeleteDescription": "你确定要删除这个投票吗?该操作不可撤销。",
|
||||||
|
"pollEditorUnlink": "解除链接",
|
||||||
|
"pollOptionAdd": "添加选项",
|
||||||
|
"pollOptionName": "选项名称",
|
||||||
|
"pollLinkExisting": "链接现有投票",
|
||||||
|
"pollAnswered": "答案已经反馈。",
|
||||||
|
"pollVotes": {
|
||||||
|
"one": "{} 票",
|
||||||
|
"other": "{} 票"
|
||||||
|
},
|
||||||
|
"publisherDelete": "删除发布者 {}",
|
||||||
|
"publisherDeleteDescription": "你确定要删除这个发布者吗?该操作不可撤销。",
|
||||||
|
"channelIsPublic": "公开频道",
|
||||||
|
"channelIsPublicDescription": "该频道是公开的,任何人都可以加入。",
|
||||||
|
"channelIsCommunity": "社区频道",
|
||||||
|
"channelIsCommunityDescription": "目前来说,社区频道还没有什么特别之处。",
|
||||||
|
"realmIsPublic": "公开领域",
|
||||||
|
"realmIsPublicDescription": "该领域是公开的,任何人都可以加入。",
|
||||||
|
"realmIsCommunity": "社区领域",
|
||||||
|
"realmIsCommunityDescription": "社区领域会显示在发现页面上。",
|
||||||
|
"realmLeave": "离开领域",
|
||||||
|
"realmLeaveDescription": "离开当前领域,并且删除领域中的身份。",
|
||||||
|
"checkInResultTier1": "大凶",
|
||||||
|
"checkInResultTier2": "凶",
|
||||||
|
"checkInResultTier3": "中平",
|
||||||
|
"checkInResultTier4": "吉",
|
||||||
|
"checkInResultTier5": "大吉",
|
||||||
|
"flagPostAction": "吹哨",
|
||||||
|
"flagPost": "吹哨不良内容",
|
||||||
|
"flagPostDescription": "吹哨不良内容,如果吹哨用户占浏览量的 50% 或以上,则帖子会被折叠。吹哨后不可撤销。",
|
||||||
|
"flaggedPost": "哨子已经吹响。",
|
||||||
|
"postViews": {
|
||||||
|
"zero": "{} 次浏览",
|
||||||
|
"one": "{} 次浏览",
|
||||||
|
"other": "{} 次浏览"
|
||||||
|
},
|
||||||
|
"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": "新建贴图包"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
"screenAccountProfileEdit": "編輯資料",
|
"screenAccountProfileEdit": "編輯資料",
|
||||||
"screenAbuseReport": "濫用檢舉",
|
"screenAbuseReport": "濫用檢舉",
|
||||||
"screenSettings": "設置",
|
"screenSettings": "設置",
|
||||||
|
"screenAccountSettings": "賬號設置",
|
||||||
|
"screenFactorSettings": "驗證因子",
|
||||||
|
"screenAccountWallet": "錢包",
|
||||||
"screenNews": "新聞",
|
"screenNews": "新聞",
|
||||||
"screenAlbum": "相冊",
|
"screenAlbum": "相冊",
|
||||||
"screenChat": "聊天",
|
"screenChat": "聊天",
|
||||||
@@ -22,6 +25,7 @@
|
|||||||
"screenChatNew": "新建聊天頻道",
|
"screenChatNew": "新建聊天頻道",
|
||||||
"screenRealm": "領域",
|
"screenRealm": "領域",
|
||||||
"screenRealmManage": "編輯領域",
|
"screenRealmManage": "編輯領域",
|
||||||
|
"screenRealmDiscovery": "發現領域",
|
||||||
"screenRealmNew": "新建領域",
|
"screenRealmNew": "新建領域",
|
||||||
"screenNotification": "通知",
|
"screenNotification": "通知",
|
||||||
"screenPostSearch": "搜索帖子",
|
"screenPostSearch": "搜索帖子",
|
||||||
@@ -88,8 +92,18 @@
|
|||||||
},
|
},
|
||||||
"loginEnterPassword": "驗證代碼",
|
"loginEnterPassword": "驗證代碼",
|
||||||
"loginSuccess": "登錄為 {}",
|
"loginSuccess": "登錄為 {}",
|
||||||
|
"authFactorDelete": "刪除驗證因子",
|
||||||
|
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||||
"authFactorPassword": "密碼",
|
"authFactorPassword": "密碼",
|
||||||
|
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||||
"authFactorEmail": "電郵一次性驗證碼",
|
"authFactorEmail": "電郵一次性驗證碼",
|
||||||
|
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||||
|
"authFactorTOTP": "時序驗證碼",
|
||||||
|
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||||
|
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||||
|
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||||
|
"authFactorAdd": "添加新驗證因子",
|
||||||
|
"authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。",
|
||||||
"accountIntroTitle": "喜歡您來!",
|
"accountIntroTitle": "喜歡您來!",
|
||||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||||
"accountLogout": "退出登錄",
|
"accountLogout": "退出登錄",
|
||||||
@@ -98,8 +112,14 @@
|
|||||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||||
"accountPublishers": "你的發佈者",
|
"accountPublishers": "你的發佈者",
|
||||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||||
|
"accountSettings": "帳號設置",
|
||||||
|
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||||
"accountProfileEdit": "編輯資料",
|
"accountProfileEdit": "編輯資料",
|
||||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
|
"accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
|
||||||
|
"accountWallet": "錢包",
|
||||||
|
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||||
|
"factorSettings": "驗證因子",
|
||||||
|
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||||
"publishersNew": "新發布者",
|
"publishersNew": "新發布者",
|
||||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||||
@@ -119,9 +139,12 @@
|
|||||||
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
||||||
"writePostTypeStory": "發動態",
|
"writePostTypeStory": "發動態",
|
||||||
"writePostTypeArticle": "寫文章",
|
"writePostTypeArticle": "寫文章",
|
||||||
|
"writePostTypeQuestion": "提問題",
|
||||||
|
"writePostTypeVideo": "發視頻",
|
||||||
"fieldPostPublisher": "帖子發佈者",
|
"fieldPostPublisher": "帖子發佈者",
|
||||||
"fieldPostContent": "發生什麼事了?!",
|
"fieldPostContent": "發生什麼事了?!",
|
||||||
"fieldPostTitle": "標題",
|
"fieldPostTitle": "標題",
|
||||||
|
"fieldPostQuestionReward": "回答獎勵源點",
|
||||||
"fieldPostDescription": "描述",
|
"fieldPostDescription": "描述",
|
||||||
"fieldPostTags": "標籤",
|
"fieldPostTags": "標籤",
|
||||||
"fieldPostCategories": "分類",
|
"fieldPostCategories": "分類",
|
||||||
@@ -178,6 +201,9 @@
|
|||||||
"other": "{} 條評論"
|
"other": "{} 條評論"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "外觀",
|
"settingsAppearance": "外觀",
|
||||||
|
"settingsDisplayLanguage": "顯示語言",
|
||||||
|
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||||
|
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||||
"settingsBackgroundImage": "背景圖片",
|
"settingsBackgroundImage": "背景圖片",
|
||||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||||
@@ -217,6 +243,8 @@
|
|||||||
"settingsMisc": "雜項",
|
"settingsMisc": "雜項",
|
||||||
"settingsMiscAbout": "關於",
|
"settingsMiscAbout": "關於",
|
||||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||||
|
"settingsAccountLanguage": "帳號偏好語言",
|
||||||
|
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
|
||||||
"sensitiveContent": "敏感內容",
|
"sensitiveContent": "敏感內容",
|
||||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||||
@@ -303,6 +331,7 @@
|
|||||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||||
"attachmentDetailInfo": "附件詳細信息",
|
"attachmentDetailInfo": "附件詳細信息",
|
||||||
"attachmentPastedImage": "粘貼的圖片",
|
"attachmentPastedImage": "粘貼的圖片",
|
||||||
|
"attachmentInsertedImage": "插入的圖片",
|
||||||
"attachmentInsertLink": "插入連接",
|
"attachmentInsertLink": "插入連接",
|
||||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||||
@@ -517,6 +546,7 @@
|
|||||||
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
|
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
|
||||||
"unauthorized": "未登陸",
|
"unauthorized": "未登陸",
|
||||||
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
|
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
|
||||||
|
"projectDetail": "項目詳情",
|
||||||
"serviceStatus": "服務狀態",
|
"serviceStatus": "服務狀態",
|
||||||
"termRelated": "相關條款",
|
"termRelated": "相關條款",
|
||||||
"appDetails": "應用程序詳情",
|
"appDetails": "應用程序詳情",
|
||||||
@@ -530,11 +560,15 @@
|
|||||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||||
"postShare": "分享",
|
"postShare": "分享",
|
||||||
"postShareImage": "分享帖圖",
|
"postShareImage": "分享帖圖",
|
||||||
|
"postGetInsight": "獲取見解",
|
||||||
|
"postGetInsightTitle": "AI 見解",
|
||||||
|
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||||
"appInitializing": "正在初始化",
|
"appInitializing": "正在初始化",
|
||||||
"poweredBy": "由 {} 提供支持",
|
"poweredBy": "由 {} 提供支持",
|
||||||
"shareIntent": "分享",
|
"shareIntent": "分享",
|
||||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||||
"shareIntentPostStory": "發佈動態",
|
"shareIntentPostStory": "發佈動態",
|
||||||
|
"shareIntentSendChannel": "分享到聊天頻道",
|
||||||
"updateAvailable": "檢測到更新可用",
|
"updateAvailable": "檢測到更新可用",
|
||||||
"updateOngoing": "正在更新,請稍後……",
|
"updateOngoing": "正在更新,請稍後……",
|
||||||
"custom": "自定義",
|
"custom": "自定義",
|
||||||
@@ -547,6 +581,8 @@
|
|||||||
"colorSchemeWhite": "白色",
|
"colorSchemeWhite": "白色",
|
||||||
"colorSchemeBlack": "黑色",
|
"colorSchemeBlack": "黑色",
|
||||||
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
||||||
|
"postFeaturedComment": "精選評論",
|
||||||
|
"postCategory": "分類",
|
||||||
"postCategoryTechnology": "技術",
|
"postCategoryTechnology": "技術",
|
||||||
"postCategoryGaming": "遊戲",
|
"postCategoryGaming": "遊戲",
|
||||||
"postCategoryLife": "生活",
|
"postCategoryLife": "生活",
|
||||||
@@ -563,5 +599,121 @@
|
|||||||
"newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
|
"newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
|
||||||
"newsReadingFromOriginal": "你正在閲讀原始文章",
|
"newsReadingFromOriginal": "你正在閲讀原始文章",
|
||||||
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
|
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
|
||||||
"newsToday": "快訊"
|
"newsToday": "快訊",
|
||||||
|
"totpPostSetup": "還有一件事",
|
||||||
|
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。",
|
||||||
|
"totpNeverShare": "永遠不要分享這個 QR Code",
|
||||||
|
"needHelp": "需要幫助?",
|
||||||
|
"needHelpLaunch": "查看我們的山羊維基!",
|
||||||
|
"walletCreate": "創建錢包",
|
||||||
|
"walletCreateSubtitle": "創建於一個錢包來開始使用源點。",
|
||||||
|
"walletCreatePassword": "在下方設置你的付款密碼",
|
||||||
|
"walletCurrencyShort": "源點",
|
||||||
|
"walletCurrency": {
|
||||||
|
"one": "{} 源點",
|
||||||
|
"other": "{} 源點"
|
||||||
|
},
|
||||||
|
"aiThinkingProcess": "AI 思考過程",
|
||||||
|
"accountSettingsApplied": "帳號設置已應用。",
|
||||||
|
"trayMenuExit": "退出",
|
||||||
|
"postQuestionUnanswered": "未解答的問題",
|
||||||
|
"postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
|
||||||
|
"postQuestionAnswered": "已解答的問題",
|
||||||
|
"postQuestionAnswerTitle": "精選解答",
|
||||||
|
"postQuestionAnswerSelect": "選擇解答",
|
||||||
|
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
|
||||||
|
"postVideoUpload": "上傳視頻",
|
||||||
|
"realmJoin": "加入領域",
|
||||||
|
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
|
||||||
|
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
|
||||||
|
"realmCommunityPublishersHint": "該領域的發佈者",
|
||||||
|
"realmJoined": "已加入領域 {}。",
|
||||||
|
"join": "加入",
|
||||||
|
"pollEditorNew": "新投票",
|
||||||
|
"pollEditorEdit": "編輯投票",
|
||||||
|
"pollEditorDelete": "刪除投票",
|
||||||
|
"pollEditorDeleteDescription": "你確定要刪除這個投票嗎?該操作不可撤銷。",
|
||||||
|
"pollEditorUnlink": "解除鏈接",
|
||||||
|
"pollOptionAdd": "添加選項",
|
||||||
|
"pollOptionName": "選項名稱",
|
||||||
|
"pollLinkExisting": "鏈接現有投票",
|
||||||
|
"pollAnswered": "答案已經反饋。",
|
||||||
|
"pollVotes": {
|
||||||
|
"one": "{} 票",
|
||||||
|
"other": "{} 票"
|
||||||
|
},
|
||||||
|
"publisherDelete": "刪除發佈者 {}",
|
||||||
|
"publisherDeleteDescription": "你確定要刪除這個發佈者嗎?該操作不可撤銷。",
|
||||||
|
"channelIsPublic": "公開頻道",
|
||||||
|
"channelIsPublicDescription": "該頻道是公開的,任何人都可以加入。",
|
||||||
|
"channelIsCommunity": "社區頻道",
|
||||||
|
"channelIsCommunityDescription": "目前來説,社區頻道還沒有什麼特別之處。",
|
||||||
|
"realmIsPublic": "公開領域",
|
||||||
|
"realmIsPublicDescription": "該領域是公開的,任何人都可以加入。",
|
||||||
|
"realmIsCommunity": "社區領域",
|
||||||
|
"realmIsCommunityDescription": "社區領域會顯示在發現頁面上。",
|
||||||
|
"realmLeave": "離開領域",
|
||||||
|
"realmLeaveDescription": "離開當前領域,並且刪除領域中的身份。",
|
||||||
|
"checkInResultTier1": "大凶",
|
||||||
|
"checkInResultTier2": "兇",
|
||||||
|
"checkInResultTier3": "中平",
|
||||||
|
"checkInResultTier4": "吉",
|
||||||
|
"checkInResultTier5": "大吉",
|
||||||
|
"flagPostAction": "吹哨",
|
||||||
|
"flagPost": "吹哨不良內容",
|
||||||
|
"flagPostDescription": "吹哨不良內容,如果吹哨用户佔瀏覽量的 50% 或以上,則帖子會被摺疊。吹哨後不可撤銷。",
|
||||||
|
"flaggedPost": "哨子已經吹響。",
|
||||||
|
"postViews": {
|
||||||
|
"zero": "{} 次瀏覽",
|
||||||
|
"one": "{} 次瀏覽",
|
||||||
|
"other": "{} 次瀏覽"
|
||||||
|
},
|
||||||
|
"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": "新建貼圖包"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
"screenAccountProfileEdit": "編輯資料",
|
"screenAccountProfileEdit": "編輯資料",
|
||||||
"screenAbuseReport": "濫用檢舉",
|
"screenAbuseReport": "濫用檢舉",
|
||||||
"screenSettings": "設置",
|
"screenSettings": "設置",
|
||||||
|
"screenAccountSettings": "賬號設置",
|
||||||
|
"screenFactorSettings": "驗證因子",
|
||||||
|
"screenAccountWallet": "錢包",
|
||||||
"screenNews": "新聞",
|
"screenNews": "新聞",
|
||||||
"screenAlbum": "相冊",
|
"screenAlbum": "相冊",
|
||||||
"screenChat": "聊天",
|
"screenChat": "聊天",
|
||||||
@@ -22,6 +25,7 @@
|
|||||||
"screenChatNew": "新建聊天頻道",
|
"screenChatNew": "新建聊天頻道",
|
||||||
"screenRealm": "領域",
|
"screenRealm": "領域",
|
||||||
"screenRealmManage": "編輯領域",
|
"screenRealmManage": "編輯領域",
|
||||||
|
"screenRealmDiscovery": "發現領域",
|
||||||
"screenRealmNew": "新建領域",
|
"screenRealmNew": "新建領域",
|
||||||
"screenNotification": "通知",
|
"screenNotification": "通知",
|
||||||
"screenPostSearch": "搜索帖子",
|
"screenPostSearch": "搜索帖子",
|
||||||
@@ -88,8 +92,18 @@
|
|||||||
},
|
},
|
||||||
"loginEnterPassword": "驗證代碼",
|
"loginEnterPassword": "驗證代碼",
|
||||||
"loginSuccess": "登錄為 {}",
|
"loginSuccess": "登錄為 {}",
|
||||||
|
"authFactorDelete": "刪除驗證因子",
|
||||||
|
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
|
||||||
"authFactorPassword": "密碼",
|
"authFactorPassword": "密碼",
|
||||||
|
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
|
||||||
"authFactorEmail": "電郵一次性驗證碼",
|
"authFactorEmail": "電郵一次性驗證碼",
|
||||||
|
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
|
||||||
|
"authFactorTOTP": "時序驗證碼",
|
||||||
|
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
|
||||||
|
"authFactorInAppNotify": "應用內通知驗證碼",
|
||||||
|
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
|
||||||
|
"authFactorAdd": "添加新驗證因子",
|
||||||
|
"authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。",
|
||||||
"accountIntroTitle": "喜歡您來!",
|
"accountIntroTitle": "喜歡您來!",
|
||||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||||
"accountLogout": "退出登錄",
|
"accountLogout": "退出登錄",
|
||||||
@@ -98,8 +112,14 @@
|
|||||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||||
"accountPublishers": "你的發佈者",
|
"accountPublishers": "你的發佈者",
|
||||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||||
|
"accountSettings": "帳號設置",
|
||||||
|
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
|
||||||
"accountProfileEdit": "編輯資料",
|
"accountProfileEdit": "編輯資料",
|
||||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
|
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
|
||||||
|
"accountWallet": "錢包",
|
||||||
|
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
|
||||||
|
"factorSettings": "驗證因子",
|
||||||
|
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
|
||||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||||
"publishersNew": "新發布者",
|
"publishersNew": "新發布者",
|
||||||
"publisherNewSubtitle": "創建一個新的公共身份。",
|
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||||
@@ -119,9 +139,12 @@
|
|||||||
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
||||||
"writePostTypeStory": "發動態",
|
"writePostTypeStory": "發動態",
|
||||||
"writePostTypeArticle": "寫文章",
|
"writePostTypeArticle": "寫文章",
|
||||||
|
"writePostTypeQuestion": "提問題",
|
||||||
|
"writePostTypeVideo": "發視頻",
|
||||||
"fieldPostPublisher": "帖子發佈者",
|
"fieldPostPublisher": "帖子發佈者",
|
||||||
"fieldPostContent": "發生什麼事了?!",
|
"fieldPostContent": "發生什麼事了?!",
|
||||||
"fieldPostTitle": "標題",
|
"fieldPostTitle": "標題",
|
||||||
|
"fieldPostQuestionReward": "回答獎勵源點",
|
||||||
"fieldPostDescription": "描述",
|
"fieldPostDescription": "描述",
|
||||||
"fieldPostTags": "標籤",
|
"fieldPostTags": "標籤",
|
||||||
"fieldPostCategories": "分類",
|
"fieldPostCategories": "分類",
|
||||||
@@ -178,6 +201,9 @@
|
|||||||
"other": "{} 條評論"
|
"other": "{} 條評論"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "外觀",
|
"settingsAppearance": "外觀",
|
||||||
|
"settingsDisplayLanguage": "顯示語言",
|
||||||
|
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||||
|
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||||
"settingsBackgroundImage": "背景圖片",
|
"settingsBackgroundImage": "背景圖片",
|
||||||
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||||
@@ -217,6 +243,8 @@
|
|||||||
"settingsMisc": "雜項",
|
"settingsMisc": "雜項",
|
||||||
"settingsMiscAbout": "關於",
|
"settingsMiscAbout": "關於",
|
||||||
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||||
|
"settingsAccountLanguage": "帳號偏好語言",
|
||||||
|
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
|
||||||
"sensitiveContent": "敏感內容",
|
"sensitiveContent": "敏感內容",
|
||||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||||
@@ -303,6 +331,7 @@
|
|||||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||||
"attachmentDetailInfo": "附件詳細信息",
|
"attachmentDetailInfo": "附件詳細信息",
|
||||||
"attachmentPastedImage": "粘貼的圖片",
|
"attachmentPastedImage": "粘貼的圖片",
|
||||||
|
"attachmentInsertedImage": "插入的圖片",
|
||||||
"attachmentInsertLink": "插入連接",
|
"attachmentInsertLink": "插入連接",
|
||||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||||
@@ -517,6 +546,7 @@
|
|||||||
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
|
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
|
||||||
"unauthorized": "未登陸",
|
"unauthorized": "未登陸",
|
||||||
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
|
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
|
||||||
|
"projectDetail": "項目詳情",
|
||||||
"serviceStatus": "服務狀態",
|
"serviceStatus": "服務狀態",
|
||||||
"termRelated": "相關條款",
|
"termRelated": "相關條款",
|
||||||
"appDetails": "應用程序詳情",
|
"appDetails": "應用程序詳情",
|
||||||
@@ -530,11 +560,15 @@
|
|||||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||||
"postShare": "分享",
|
"postShare": "分享",
|
||||||
"postShareImage": "分享帖圖",
|
"postShareImage": "分享帖圖",
|
||||||
|
"postGetInsight": "獲取見解",
|
||||||
|
"postGetInsightTitle": "AI 見解",
|
||||||
|
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
|
||||||
"appInitializing": "正在初始化",
|
"appInitializing": "正在初始化",
|
||||||
"poweredBy": "由 {} 提供支持",
|
"poweredBy": "由 {} 提供支持",
|
||||||
"shareIntent": "分享",
|
"shareIntent": "分享",
|
||||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||||
"shareIntentPostStory": "發佈動態",
|
"shareIntentPostStory": "發佈動態",
|
||||||
|
"shareIntentSendChannel": "分享到聊天頻道",
|
||||||
"updateAvailable": "檢測到更新可用",
|
"updateAvailable": "檢測到更新可用",
|
||||||
"updateOngoing": "正在更新,請稍後……",
|
"updateOngoing": "正在更新,請稍後……",
|
||||||
"custom": "自定義",
|
"custom": "自定義",
|
||||||
@@ -547,6 +581,8 @@
|
|||||||
"colorSchemeWhite": "白色",
|
"colorSchemeWhite": "白色",
|
||||||
"colorSchemeBlack": "黑色",
|
"colorSchemeBlack": "黑色",
|
||||||
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
||||||
|
"postFeaturedComment": "精選評論",
|
||||||
|
"postCategory": "分類",
|
||||||
"postCategoryTechnology": "技術",
|
"postCategoryTechnology": "技術",
|
||||||
"postCategoryGaming": "遊戲",
|
"postCategoryGaming": "遊戲",
|
||||||
"postCategoryLife": "生活",
|
"postCategoryLife": "生活",
|
||||||
@@ -563,5 +599,121 @@
|
|||||||
"newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章",
|
"newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章",
|
||||||
"newsReadingFromOriginal": "你正在閱讀原始文章",
|
"newsReadingFromOriginal": "你正在閱讀原始文章",
|
||||||
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
|
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
|
||||||
"newsToday": "快訊"
|
"newsToday": "快訊",
|
||||||
|
"totpPostSetup": "還有一件事",
|
||||||
|
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。",
|
||||||
|
"totpNeverShare": "永遠不要分享這個 QR Code",
|
||||||
|
"needHelp": "需要幫助?",
|
||||||
|
"needHelpLaunch": "查看我們的山羊維基!",
|
||||||
|
"walletCreate": "創建錢包",
|
||||||
|
"walletCreateSubtitle": "創建於一個錢包來開始使用源點。",
|
||||||
|
"walletCreatePassword": "在下方設置你的付款密碼",
|
||||||
|
"walletCurrencyShort": "源點",
|
||||||
|
"walletCurrency": {
|
||||||
|
"one": "{} 源點",
|
||||||
|
"other": "{} 源點"
|
||||||
|
},
|
||||||
|
"aiThinkingProcess": "AI 思考過程",
|
||||||
|
"accountSettingsApplied": "帳號設置已應用。",
|
||||||
|
"trayMenuExit": "退出",
|
||||||
|
"postQuestionUnanswered": "未解答的問題",
|
||||||
|
"postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
|
||||||
|
"postQuestionAnswered": "已解答的問題",
|
||||||
|
"postQuestionAnswerTitle": "精選解答",
|
||||||
|
"postQuestionAnswerSelect": "選擇解答",
|
||||||
|
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
|
||||||
|
"postVideoUpload": "上傳視頻",
|
||||||
|
"realmJoin": "加入領域",
|
||||||
|
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
|
||||||
|
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
|
||||||
|
"realmCommunityPublishersHint": "該領域的發佈者",
|
||||||
|
"realmJoined": "已加入領域 {}。",
|
||||||
|
"join": "加入",
|
||||||
|
"pollEditorNew": "新投票",
|
||||||
|
"pollEditorEdit": "編輯投票",
|
||||||
|
"pollEditorDelete": "刪除投票",
|
||||||
|
"pollEditorDeleteDescription": "你確定要刪除這個投票嗎?該操作不可撤銷。",
|
||||||
|
"pollEditorUnlink": "解除鏈接",
|
||||||
|
"pollOptionAdd": "添加選項",
|
||||||
|
"pollOptionName": "選項名稱",
|
||||||
|
"pollLinkExisting": "鏈接現有投票",
|
||||||
|
"pollAnswered": "答案已經反饋。",
|
||||||
|
"pollVotes": {
|
||||||
|
"one": "{} 票",
|
||||||
|
"other": "{} 票"
|
||||||
|
},
|
||||||
|
"publisherDelete": "刪除發佈者 {}",
|
||||||
|
"publisherDeleteDescription": "你確定要刪除這個發佈者嗎?該操作不可撤銷。",
|
||||||
|
"channelIsPublic": "公開頻道",
|
||||||
|
"channelIsPublicDescription": "該頻道是公開的,任何人都可以加入。",
|
||||||
|
"channelIsCommunity": "社區頻道",
|
||||||
|
"channelIsCommunityDescription": "目前來說,社區頻道還沒有什麼特別之處。",
|
||||||
|
"realmIsPublic": "公開領域",
|
||||||
|
"realmIsPublicDescription": "該領域是公開的,任何人都可以加入。",
|
||||||
|
"realmIsCommunity": "社區領域",
|
||||||
|
"realmIsCommunityDescription": "社區領域會顯示在發現頁面上。",
|
||||||
|
"realmLeave": "離開領域",
|
||||||
|
"realmLeaveDescription": "離開當前領域,並且刪除領域中的身份。",
|
||||||
|
"checkInResultTier1": "大凶",
|
||||||
|
"checkInResultTier2": "兇",
|
||||||
|
"checkInResultTier3": "中平",
|
||||||
|
"checkInResultTier4": "吉",
|
||||||
|
"checkInResultTier5": "大吉",
|
||||||
|
"flagPostAction": "吹哨",
|
||||||
|
"flagPost": "吹哨不良內容",
|
||||||
|
"flagPostDescription": "吹哨不良內容,如果吹哨用戶佔瀏覽量的 50% 或以上,則帖子會被摺疊。吹哨後不可撤銷。",
|
||||||
|
"flaggedPost": "哨子已經吹響。",
|
||||||
|
"postViews": {
|
||||||
|
"zero": "{} 次瀏覽",
|
||||||
|
"one": "{} 次瀏覽",
|
||||||
|
"other": "{} 次瀏覽"
|
||||||
|
},
|
||||||
|
"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": "新建貼圖包"
|
||||||
}
|
}
|
||||||
|
|||||||
14
debian/debian.yml
vendored
Normal file
14
debian/debian.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
flutter_app:
|
||||||
|
command: surface
|
||||||
|
arch: x64
|
||||||
|
parent: /usr/local/lib
|
||||||
|
nonInteractive: false
|
||||||
|
|
||||||
|
control:
|
||||||
|
Package: solian
|
||||||
|
Version: 2.3.2
|
||||||
|
Architecture: amd64
|
||||||
|
Priority: optional
|
||||||
|
Depends: mpv keybinder-3.0
|
||||||
|
Maintainer: Solsynth LLC
|
||||||
|
Description: The Solar Network Desktop Application
|
||||||
9
debian/gui/surface.desktop
vendored
Normal file
9
debian/gui/surface.desktop
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Version=2.3.2
|
||||||
|
Name=Solian
|
||||||
|
GenericName=Solian
|
||||||
|
Comment=The Solar Network Desktop Application
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Social Networking
|
||||||
|
Keywords=social;social network;chat;solar network
|
||||||
23
debian/gui/surface.svg
vendored
Normal file
23
debian/gui/surface.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 232 KiB |
134
ios/Podfile.lock
134
ios/Podfile.lock
@@ -2,7 +2,6 @@ PODS:
|
|||||||
- Alamofire (5.10.2)
|
- Alamofire (5.10.2)
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
|
||||||
- croppy (0.0.1):
|
- croppy (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
@@ -43,58 +42,58 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- file_saver (0.0.1):
|
- file_saver (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Firebase/Analytics (11.6.0):
|
- Firebase/Analytics (11.8.0):
|
||||||
- Firebase/Core
|
- Firebase/Core
|
||||||
- Firebase/Core (11.6.0):
|
- Firebase/Core (11.8.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseAnalytics (~> 11.6.0)
|
- FirebaseAnalytics (~> 11.8.0)
|
||||||
- Firebase/CoreOnly (11.6.0):
|
- Firebase/CoreOnly (11.8.0):
|
||||||
- FirebaseCore (~> 11.6.0)
|
- FirebaseCore (~> 11.8.0)
|
||||||
- Firebase/Messaging (11.6.0):
|
- Firebase/Messaging (11.8.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 11.6.0)
|
- FirebaseMessaging (~> 11.8.0)
|
||||||
- firebase_analytics (11.4.0):
|
- firebase_analytics (11.4.3):
|
||||||
- Firebase/Analytics (= 11.6.0)
|
- Firebase/Analytics (= 11.8.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_core (3.10.0):
|
- firebase_core (3.12.0):
|
||||||
- Firebase/CoreOnly (= 11.6.0)
|
- Firebase/CoreOnly (= 11.8.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_messaging (15.2.0):
|
- firebase_messaging (15.2.3):
|
||||||
- Firebase/Messaging (= 11.6.0)
|
- Firebase/Messaging (= 11.8.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- FirebaseAnalytics (11.6.0):
|
- FirebaseAnalytics (11.8.0):
|
||||||
- FirebaseAnalytics/AdIdSupport (= 11.6.0)
|
- FirebaseAnalytics/AdIdSupport (= 11.8.0)
|
||||||
- FirebaseCore (~> 11.6.0)
|
- FirebaseCore (~> 11.8.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseAnalytics/AdIdSupport (11.6.0):
|
- FirebaseAnalytics/AdIdSupport (11.8.0):
|
||||||
- FirebaseCore (~> 11.6.0)
|
- FirebaseCore (~> 11.8.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleAppMeasurement (= 11.6.0)
|
- GoogleAppMeasurement (= 11.8.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseCore (11.6.0):
|
- FirebaseCore (11.8.1):
|
||||||
- FirebaseCoreInternal (~> 11.6.0)
|
- FirebaseCoreInternal (~> 11.8.0)
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- GoogleUtilities/Logger (~> 8.0)
|
- GoogleUtilities/Logger (~> 8.0)
|
||||||
- FirebaseCoreInternal (11.6.0):
|
- FirebaseCoreInternal (11.8.0):
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- FirebaseInstallations (11.6.0):
|
- FirebaseInstallations (11.8.0):
|
||||||
- FirebaseCore (~> 11.6.0)
|
- FirebaseCore (~> 11.8.0)
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseMessaging (11.6.0):
|
- FirebaseMessaging (11.8.0):
|
||||||
- FirebaseCore (~> 11.6.0)
|
- FirebaseCore (~> 11.8.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleDataTransport (~> 10.0)
|
- GoogleDataTransport (~> 10.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
@@ -123,21 +122,21 @@ PODS:
|
|||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- GoogleAppMeasurement (11.6.0):
|
- GoogleAppMeasurement (11.8.0):
|
||||||
- GoogleAppMeasurement/AdIdSupport (= 11.6.0)
|
- GoogleAppMeasurement/AdIdSupport (= 11.8.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/AdIdSupport (11.6.0):
|
- GoogleAppMeasurement/AdIdSupport (11.8.0):
|
||||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
|
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.8.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
|
- GoogleAppMeasurement/WithoutAdIdSupport (11.8.0):
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
@@ -179,8 +178,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.1.3)
|
- Kingfisher (8.2.0)
|
||||||
- livekit_client (2.3.5):
|
- livekit_client (2.3.6):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
@@ -211,9 +210,9 @@ PODS:
|
|||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
- screen_brightness_ios (0.1.0):
|
- screen_brightness_ios (0.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SDWebImage (5.20.0):
|
- SDWebImage (5.20.1):
|
||||||
- SDWebImage/Core (= 5.20.0)
|
- SDWebImage/Core (= 5.20.1)
|
||||||
- SDWebImage/Core (5.20.0)
|
- SDWebImage/Core (5.20.1)
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
@@ -222,6 +221,25 @@ PODS:
|
|||||||
- sqflite_darwin (0.0.4):
|
- sqflite_darwin (0.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- 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)
|
- SwiftyGif (5.4.5)
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -237,7 +255,7 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Alamofire
|
- Alamofire
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
@@ -269,6 +287,7 @@ DEPENDENCIES:
|
|||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
|
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- video_compress (from `.symlinks/plugins/video_compress/ios`)
|
- video_compress (from `.symlinks/plugins/video_compress/ios`)
|
||||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||||
@@ -295,12 +314,13 @@ SPEC REPOS:
|
|||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
|
- sqlite3
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
- WebRTC-SDK
|
- WebRTC-SDK
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
croppy:
|
croppy:
|
||||||
:path: ".symlinks/plugins/croppy/ios"
|
:path: ".symlinks/plugins/croppy/ios"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
@@ -361,6 +381,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
sqflite_darwin:
|
sqflite_darwin:
|
||||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||||
|
sqlite3_flutter_libs:
|
||||||
|
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
video_compress:
|
video_compress:
|
||||||
@@ -374,37 +396,37 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||||
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
|
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||||
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
|
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
|
||||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||||
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
|
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
||||||
firebase_analytics: 07bd7cfbac54bfcdccf2bb2530f9a65486f7ef3f
|
firebase_analytics: 7ec1166af61987fa968766eb11587c562a5650ee
|
||||||
firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10
|
firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682
|
||||||
firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c
|
firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d
|
||||||
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
|
FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b
|
||||||
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
|
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
||||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
||||||
FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
|
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
||||||
FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
|
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||||
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
|
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
||||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||||
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
||||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||||
GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
|
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||||
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
|
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
|
||||||
livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
|
livekit_client: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81
|
||||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||||
@@ -418,10 +440,12 @@ SPEC CHECKSUMS:
|
|||||||
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
|
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||||
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
|
SDWebImage: 33d0f23bddeb5d209ae959153883247be6703713
|
||||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
|
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||||
|
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||||
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
|
|||||||
@@ -123,48 +123,59 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let imageIdentifier = metadata["image"] as? String {
|
if let imageIdentifier = metadata["image"] as? String {
|
||||||
attachMedia(to: content, withIdentifier: imageIdentifier, fileType: UTType.jpeg, doScaleDown: true)
|
attachMedia(to: content, withIdentifier: [imageIdentifier], fileType: UTType.jpeg, doScaleDown: true)
|
||||||
} else if let avatarIdentifier = metadata["avatar"] as? String {
|
} else if let avatarIdentifier = metadata["avatar"] as? String {
|
||||||
attachMedia(to: content, withIdentifier: avatarIdentifier, fileType: UTType.jpeg, doScaleDown: true)
|
attachMedia(to: content, withIdentifier: [avatarIdentifier], fileType: UTType.jpeg, doScaleDown: true)
|
||||||
|
} else if let imagesIdentifier = metadata["images"] as? Array<String> {
|
||||||
|
attachMedia(to: content, withIdentifier: imagesIdentifier, fileType: UTType.jpeg, doScaleDown: true)
|
||||||
} else {
|
} else {
|
||||||
contentHandler?(content)
|
contentHandler?(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: String, fileType type: UTType?, doScaleDown scaleDown: Bool = false) {
|
private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: Array<String>, fileType type: UTType?, doScaleDown scaleDown: Bool = false) {
|
||||||
let attachmentUrl = getAttachmentUrl(for: identifier)
|
let attachmentUrls = identifier.compactMap { element in
|
||||||
|
return getAttachmentUrl(for: element)
|
||||||
guard let remoteUrl = URL(string: attachmentUrl) else {
|
}
|
||||||
print("Invalid URL for attachment: \(attachmentUrl)")
|
|
||||||
|
guard !attachmentUrls.isEmpty else {
|
||||||
|
print("Invalid URLs for attachments: \(attachmentUrls)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetSize = 800
|
let targetSize = 800
|
||||||
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
||||||
|
|
||||||
KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [
|
for attachmentUrl in attachmentUrls {
|
||||||
.processor(scaleProcessor)
|
guard let remoteUrl = URL(string: attachmentUrl) else {
|
||||||
] : nil) { [weak self] result in
|
print("Invalid URL for attachment: \(attachmentUrl)")
|
||||||
guard let self = self else { return }
|
continue // Skip this URL and move to the next one
|
||||||
|
}
|
||||||
switch result {
|
|
||||||
case .success(let retrievalResult):
|
KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [
|
||||||
// The image is either retrieved from cache or downloaded
|
.processor(scaleProcessor)
|
||||||
let tempDirectory = FileManager.default.temporaryDirectory
|
] : nil) { [weak self] result in
|
||||||
let cachedFileUrl = tempDirectory.appendingPathComponent(identifier)
|
guard let self = self else { return }
|
||||||
|
|
||||||
do {
|
switch result {
|
||||||
// Write the image data to a temporary file for UNNotificationAttachment
|
case .success(let retrievalResult):
|
||||||
try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
|
// The image is either retrieved from cache or downloaded
|
||||||
self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: identifier)
|
let tempDirectory = FileManager.default.temporaryDirectory
|
||||||
} catch {
|
let cachedFileUrl = tempDirectory.appendingPathComponent(UUID().uuidString) // Unique identifier for each file
|
||||||
print("Failed to write media to temporary file: \(error.localizedDescription)")
|
|
||||||
|
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)
|
self.contentHandler?(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
|
||||||
print("Failed to retrieve image: \(error.localizedDescription)")
|
|
||||||
self.contentHandler?(content)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ struct CheckInProvider: TimelineProvider {
|
|||||||
|
|
||||||
func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) {
|
func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) {
|
||||||
let prefs = UserDefaults(suiteName: "group.solsynth.solian")
|
let prefs = UserDefaults(suiteName: "group.solsynth.solian")
|
||||||
|
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
|
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
|
||||||
|
|
||||||
let jsonDecoder = JSONDecoder()
|
let jsonDecoder = JSONDecoder()
|
||||||
jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)
|
jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)
|
||||||
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
|
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
let checkInRaw = prefs?.string(forKey: "pas_check_in_record")
|
let checkInRaw = prefs?.string(forKey: "pas_check_in_record")
|
||||||
var checkIn: SolarCheckInRecord?
|
var checkIn: SolarCheckInRecord?
|
||||||
if let checkInRaw = checkInRaw {
|
if let checkInRaw = checkInRaw {
|
||||||
@@ -31,7 +31,7 @@ struct CheckInProvider: TimelineProvider {
|
|||||||
checkIn = nil
|
checkIn = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry = CheckInEntry(
|
let entry = CheckInEntry(
|
||||||
date: Date(),
|
date: Date(),
|
||||||
checkIn: checkIn
|
checkIn: checkIn
|
||||||
@@ -54,11 +54,11 @@ struct CheckInEntry: TimelineEntry {
|
|||||||
|
|
||||||
struct CheckInWidgetEntryView : View {
|
struct CheckInWidgetEntryView : View {
|
||||||
var entry: CheckInProvider.Entry
|
var entry: CheckInProvider.Entry
|
||||||
|
|
||||||
private let resultTierSymbols: [String] = ["大凶", "凶", "中平", "吉", "大吉"]
|
private let resultTierSymbols: [String] = ["Bad", "Poor", "Medium", "Good", "Great"]
|
||||||
|
|
||||||
func checkIn() -> Void {}
|
func checkIn() -> Void {}
|
||||||
|
|
||||||
func seeDetail() -> Void {}
|
func seeDetail() -> Void {}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -68,9 +68,9 @@ struct CheckInWidgetEntryView : View {
|
|||||||
Text(resultTierSymbols[checkIn.resultTier]).font(.system(size: 27, weight: .bold))
|
Text(resultTierSymbols[checkIn.resultTier]).font(.system(size: 27, weight: .bold))
|
||||||
Text("+\(checkIn.resultExperience) EXP").font(.system(size: 15, design: .monospaced))
|
Text("+\(checkIn.resultExperience) EXP").font(.system(size: 15, design: .monospaced))
|
||||||
}.padding(.horizontal, 4)
|
}.padding(.horizontal, 4)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(
|
Text(
|
||||||
@@ -82,7 +82,7 @@ struct CheckInWidgetEntryView : View {
|
|||||||
format: .dateTime.day().month()
|
format: .dateTime.day().month()
|
||||||
).font(.system(size: 13))
|
).font(.system(size: 13))
|
||||||
}.padding(.leading, 4)
|
}.padding(.leading, 4)
|
||||||
|
|
||||||
Button("See Detail", systemImage: "arrow.right", action: seeDetail)
|
Button("See Detail", systemImage: "arrow.right", action: seeDetail)
|
||||||
.labelStyle(.iconOnly)
|
.labelStyle(.iconOnly)
|
||||||
.buttonBorderShape(.circle)
|
.buttonBorderShape(.circle)
|
||||||
@@ -91,11 +91,11 @@ struct CheckInWidgetEntryView : View {
|
|||||||
} else {
|
} else {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("Check In").font(.system(size: 19, weight: .bold))
|
Text("Check In").font(.system(size: 19, weight: .bold))
|
||||||
Text("You haven't check in today").font(.system(size: 15))
|
Text("You haven't divined today").font(.system(size: 15))
|
||||||
}.padding(.horizontal, 4)
|
}.padding(.horizontal, 4)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack(alignment: .bottom) {
|
HStack(alignment: .bottom) {
|
||||||
Button("Check In", systemImage: "checkmark", action: checkIn).labelStyle(.iconOnly).buttonBorderShape(.circle).frame(maxWidth: .infinity, alignment: .trailing)
|
Button("Check In", systemImage: "checkmark", action: checkIn).labelStyle(.iconOnly).buttonBorderShape(.circle).frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/database/database.dart';
|
||||||
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
@@ -16,13 +18,13 @@ import 'package:surface/types/websocket.dart';
|
|||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class ChatMessageController extends ChangeNotifier {
|
class ChatMessageController extends ChangeNotifier {
|
||||||
static const kChatMessageBoxPrefix = 'nex_chat_messages_';
|
|
||||||
static const kSingleBatchLoadLimit = 100;
|
static const kSingleBatchLoadLimit = 100;
|
||||||
|
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
late final UserDirectoryProvider _ud;
|
late final UserDirectoryProvider _ud;
|
||||||
late final WebSocketProvider _ws;
|
late final WebSocketProvider _ws;
|
||||||
late final SnAttachmentProvider _attach;
|
late final SnAttachmentProvider _attach;
|
||||||
|
late final DatabaseProvider _dt;
|
||||||
|
|
||||||
StreamSubscription? _wsSubscription;
|
StreamSubscription? _wsSubscription;
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
_ud = context.read<UserDirectoryProvider>();
|
_ud = context.read<UserDirectoryProvider>();
|
||||||
_ws = context.read<WebSocketProvider>();
|
_ws = context.read<WebSocketProvider>();
|
||||||
_attach = context.read<SnAttachmentProvider>();
|
_attach = context.read<SnAttachmentProvider>();
|
||||||
|
_dt = context.read<DatabaseProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isPending = true;
|
bool isPending = true;
|
||||||
@@ -38,9 +41,9 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
|
|
||||||
int? messageTotal;
|
int? messageTotal;
|
||||||
|
|
||||||
bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!;
|
bool get isAllLoaded =>
|
||||||
|
messageTotal != null && messages.length >= messageTotal!;
|
||||||
|
|
||||||
String? _boxKey;
|
|
||||||
SnChannel? channel;
|
SnChannel? channel;
|
||||||
SnChannelMember? profile;
|
SnChannelMember? profile;
|
||||||
|
|
||||||
@@ -51,27 +54,19 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
/// Stored as a list of nonce to provide the loading state
|
/// Stored as a list of nonce to provide the loading state
|
||||||
final List<String> unconfirmedMessages = List.empty(growable: true);
|
final List<String> unconfirmedMessages = List.empty(growable: true);
|
||||||
|
|
||||||
Box<SnChatMessage>? get _box => (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
|
|
||||||
|
|
||||||
final List<SnChannelMember> typingMembers = List.empty(growable: true);
|
final List<SnChannelMember> typingMembers = List.empty(growable: true);
|
||||||
final Map<int, Timer> typingInactiveTimer = {};
|
final Map<int, Timer> typingInactiveTimer = {};
|
||||||
|
|
||||||
Future<void> initialize(SnChannel chan) async {
|
Future<void> initialize(SnChannel chan) async {
|
||||||
channel = chan;
|
channel = chan;
|
||||||
|
|
||||||
// Initialize local data
|
|
||||||
_boxKey = '$kChatMessageBoxPrefix${chan.id}';
|
|
||||||
await Hive.openBox<SnChatMessage>(_boxKey!);
|
|
||||||
|
|
||||||
// Fetch channel profile
|
// Fetch channel profile
|
||||||
final resp = await _sn.client.get(
|
final resp = await _sn.client.get(
|
||||||
'/cgi/im/channels/${chan.keyPath}/me',
|
'/cgi/im/channels/${chan.keyPath}/me',
|
||||||
);
|
);
|
||||||
profile = SnChannelMember.fromJson(
|
profile = SnChannelMember.fromJson(resp.data);
|
||||||
resp.data as Map<String, dynamic>,
|
|
||||||
);
|
|
||||||
|
|
||||||
_wsSubscription = _ws.stream.stream.listen((event) {
|
_wsSubscription = _ws.pk.stream.listen((event) {
|
||||||
switch (event.method) {
|
switch (event.method) {
|
||||||
case 'events.new':
|
case 'events.new':
|
||||||
if (event.payload?['channel_id'] != channel?.id) break;
|
if (event.payload?['channel_id'] != channel?.id) break;
|
||||||
@@ -87,7 +82,8 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
typingInactiveTimer[member.id]?.cancel();
|
typingInactiveTimer[member.id]?.cancel();
|
||||||
typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () {
|
typingInactiveTimer[member.id] =
|
||||||
|
Timer(const Duration(seconds: 3), () {
|
||||||
typingMembers.removeWhere((x) => x.id == member.id);
|
typingMembers.removeWhere((x) => x.id == member.id);
|
||||||
typingInactiveTimer.remove(member.id);
|
typingInactiveTimer.remove(member.id);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -129,10 +125,16 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
|
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
|
||||||
if (_box == null) return;
|
await _dt.db.snLocalChatMessage.insertAll(
|
||||||
await _box!.putAll({
|
messages.map(
|
||||||
for (final message in messages) message.id: message,
|
(ele) => SnLocalChatMessageCompanion.insert(
|
||||||
});
|
id: Value(ele.id),
|
||||||
|
content: ele,
|
||||||
|
channelId: channel!.id,
|
||||||
|
createdAt: Value(ele.createdAt),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onConflict: DoNothing());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addUnconfirmedMessage(SnChatMessage message) async {
|
Future<void> _addUnconfirmedMessage(SnChatMessage message) async {
|
||||||
@@ -184,8 +186,21 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
await _applyMessage(message);
|
await _applyMessage(message);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
if (_box == null) return;
|
if (isCheckedUpdate) {
|
||||||
await _box!.put(message.id, message);
|
await _dt.db.snLocalChatMessage.insertOne(
|
||||||
|
SnLocalChatMessageCompanion.insert(
|
||||||
|
id: Value(message.id),
|
||||||
|
content: message,
|
||||||
|
channelId: channel!.id,
|
||||||
|
createdAt: Value(message.createdAt),
|
||||||
|
),
|
||||||
|
onConflict: DoUpdate((_) => SnLocalChatMessageCompanion.custom(
|
||||||
|
content: Constant(jsonEncode(message.toJson())),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
incomeStrandedQueue.add(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _applyMessage(SnChatMessage message) async {
|
Future<void> _applyMessage(SnChatMessage message) async {
|
||||||
@@ -194,7 +209,8 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'messages.edit':
|
case 'messages.edit':
|
||||||
if (message.relatedEventId != null) {
|
if (message.relatedEventId != null) {
|
||||||
final idx = messages.indexWhere((x) => x.id == message.relatedEventId);
|
final idx =
|
||||||
|
messages.indexWhere((x) => x.id == message.relatedEventId);
|
||||||
if (idx != -1) {
|
if (idx != -1) {
|
||||||
final newBody = message.body;
|
final newBody = message.body;
|
||||||
newBody.remove('related_event');
|
newBody.remove('related_event');
|
||||||
@@ -202,16 +218,24 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
body: newBody,
|
body: newBody,
|
||||||
updatedAt: message.updatedAt,
|
updatedAt: message.updatedAt,
|
||||||
);
|
);
|
||||||
if (_box!.containsKey(message.relatedEventId)) {
|
if (message.relatedEventId != null) {
|
||||||
await _box!.put(message.relatedEventId, messages[idx]);
|
await (_dt.db.snLocalChatMessage.update()
|
||||||
|
..where((e) => e.id.equals(message.relatedEventId!)))
|
||||||
|
.write(
|
||||||
|
SnLocalChatMessageCompanion.custom(
|
||||||
|
content: Constant(jsonEncode(messages[idx].toJson())),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'messages.delete':
|
case 'messages.delete':
|
||||||
if (message.relatedEventId != null) {
|
if (message.relatedEventId != null) {
|
||||||
messages.removeWhere((x) => x.id == message.relatedEventId);
|
messages.removeWhere((x) => x.id == message.relatedEventId);
|
||||||
if (_box!.containsKey(message.relatedEventId)) {
|
if (message.relatedEventId != null) {
|
||||||
await _box!.delete(message.relatedEventId);
|
await (_dt.db.snLocalChatMessage.delete()
|
||||||
|
..where((e) => e.id.equals(message.relatedEventId!)))
|
||||||
|
.go();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,7 +257,8 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
'algorithm': 'plain',
|
'algorithm': 'plain',
|
||||||
if (quoteId != null) 'quote_event': quoteId,
|
if (quoteId != null) 'quote_event': quoteId,
|
||||||
if (relatedId != null) 'related_event': relatedId,
|
if (relatedId != null) 'related_event': relatedId,
|
||||||
if (attachments != null && attachments.isNotEmpty) 'attachments': attachments,
|
if (attachments != null && attachments.isNotEmpty)
|
||||||
|
'attachments': attachments,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock the message locally
|
// Mock the message locally
|
||||||
@@ -287,20 +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.
|
/// Check the local storage is up to date with the server.
|
||||||
/// If the local storage is not up to date, it will be updated.
|
/// If the local storage is not up to date, it will be updated.
|
||||||
Future<void> checkUpdate() async {
|
Future<void> checkUpdate() async {
|
||||||
if (_box == null) return;
|
|
||||||
if (_box!.isEmpty) return;
|
|
||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
|
final mostRecentMessage = await (_dt.db.snLocalChatMessage.select()
|
||||||
|
..limit(1)
|
||||||
|
..orderBy([
|
||||||
|
(e) =>
|
||||||
|
OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
|
||||||
|
]))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (mostRecentMessage == null) {
|
||||||
|
// Initial load
|
||||||
|
await loadMessages(take: 20);
|
||||||
|
isCheckedUpdate = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final resp = await _sn.client.get(
|
final resp = await _sn.client.get(
|
||||||
'/cgi/im/channels/${channel!.keyPath}/events/update',
|
'/cgi/im/channels/${channel!.keyPath}/events/update',
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
'pivot': _box!.values.last.id,
|
'pivot': mostRecentMessage.content.id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (resp.data['up_to_date'] == true) return;
|
if (resp.data['up_to_date'] == true) return;
|
||||||
@@ -316,6 +355,12 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
} finally {
|
} finally {
|
||||||
await loadMessages();
|
await loadMessages();
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
||||||
|
isCheckedUpdate = true;
|
||||||
|
_saveMessageToLocal(incomeStrandedQueue).then((_) {
|
||||||
|
incomeStrandedQueue.clear();
|
||||||
|
});
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,13 +369,18 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
/// If it was not found in local storage we will look it up in remote
|
/// If it was not found in local storage we will look it up in remote
|
||||||
Future<SnChatMessage?> getMessage(int id) async {
|
Future<SnChatMessage?> getMessage(int id) async {
|
||||||
SnChatMessage? out;
|
SnChatMessage? out;
|
||||||
if (_box != null && _box!.containsKey(id)) {
|
final local = await (_dt.db.snLocalChatMessage.select()
|
||||||
out = _box!.get(id);
|
..limit(1)
|
||||||
|
..where((e) => e.id.equals(id)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (local != null) {
|
||||||
|
out = local.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (out == null) {
|
if (out == null) {
|
||||||
try {
|
try {
|
||||||
final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
final resp = await _sn.client
|
||||||
|
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
|
||||||
out = SnChatMessage.fromJson(resp.data);
|
out = SnChatMessage.fromJson(resp.data);
|
||||||
_saveMessageToLocal([out]);
|
_saveMessageToLocal([out]);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -364,16 +414,21 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
bool forceLocal = false,
|
bool forceLocal = false,
|
||||||
bool forceRemote = false,
|
bool forceRemote = false,
|
||||||
}) async {
|
}) async {
|
||||||
|
final localTotal = await _dt.db.snLocalChatMessage
|
||||||
|
.count(where: (e) => e.channelId.equals(channel!.id))
|
||||||
|
.getSingle();
|
||||||
|
|
||||||
late List<SnChatMessage> out;
|
late List<SnChatMessage> out;
|
||||||
if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) {
|
if ((localTotal >= take + offset || forceLocal) && !forceRemote) {
|
||||||
out = _box!.keys
|
final result = await (_dt.db.snLocalChatMessage.select()
|
||||||
.toList()
|
..where((e) => e.channelId.equals(channel!.id))
|
||||||
.cast<int>()
|
..orderBy([
|
||||||
.sorted((a, b) => b.compareTo(a))
|
(e) =>
|
||||||
.skip(offset)
|
OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
|
||||||
.take(take)
|
])
|
||||||
.map((key) => _box!.get(key)!)
|
..limit(take, offset: offset))
|
||||||
.toList();
|
.get();
|
||||||
|
out = result.map((e) => e.content).toList();
|
||||||
} else {
|
} else {
|
||||||
final resp = await _sn.client.get(
|
final resp = await _sn.client.get(
|
||||||
'/cgi/im/channels/${channel!.keyPath}/events',
|
'/cgi/im/channels/${channel!.keyPath}/events',
|
||||||
@@ -408,7 +463,8 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
quoteEvent: quoteEvent,
|
quoteEvent: quoteEvent,
|
||||||
attachments: attachments
|
attachments: attachments
|
||||||
.where(
|
.where(
|
||||||
(ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
(ele) =>
|
||||||
|
out[i].body['attachments']?.contains(ele?.rid) ?? false,
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
@@ -416,7 +472,10 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Preload sender accounts
|
// Preload sender accounts
|
||||||
final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet();
|
final accountId = out
|
||||||
|
.where((ele) => ele.sender.accountId >= 0)
|
||||||
|
.map((ele) => ele.sender.accountId)
|
||||||
|
.toSet();
|
||||||
await _ud.listAccount(accountId);
|
await _ud.listAccount(accountId);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
@@ -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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_box?.close();
|
|
||||||
_wsSubscription?.cancel();
|
_wsSubscription?.cancel();
|
||||||
|
if (_readEventDebounce?.isActive ?? false) {
|
||||||
|
_sendReadEvent();
|
||||||
|
}
|
||||||
|
_readEventDebounce?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import 'package:surface/providers/post.dart';
|
|||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
|
import 'package:surface/types/poll.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
|
import 'package:surface/types/realm.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
import 'package:video_compress/video_compress.dart';
|
import 'package:video_compress/video_compress.dart';
|
||||||
@@ -144,6 +146,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
static const Map<String, String> kTitleMap = {
|
static const Map<String, String> kTitleMap = {
|
||||||
'stories': 'writePostTypeStory',
|
'stories': 'writePostTypeStory',
|
||||||
'articles': 'writePostTypeArticle',
|
'articles': 'writePostTypeArticle',
|
||||||
|
'questions': 'writePostTypeQuestion',
|
||||||
|
'videos': 'writePostTypeVideo',
|
||||||
};
|
};
|
||||||
|
|
||||||
static const kAttachmentProgressWeight = 0.9;
|
static const kAttachmentProgressWeight = 0.9;
|
||||||
@@ -153,6 +157,16 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
final TextEditingController titleController = TextEditingController();
|
final TextEditingController titleController = TextEditingController();
|
||||||
final TextEditingController descriptionController = TextEditingController();
|
final TextEditingController descriptionController = TextEditingController();
|
||||||
final TextEditingController aliasController = TextEditingController();
|
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;
|
bool _temporarySaveActive = false;
|
||||||
|
|
||||||
@@ -168,6 +182,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
});
|
});
|
||||||
contentController.addListener(() {
|
contentController.addListener(() {
|
||||||
_temporaryPlanSave();
|
_temporaryPlanSave();
|
||||||
|
notifyListeners();
|
||||||
});
|
});
|
||||||
if (doLoadFromTemporary) _temporaryLoad();
|
if (doLoadFromTemporary) _temporaryLoad();
|
||||||
}
|
}
|
||||||
@@ -183,6 +198,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
bool isLoading = false, isBusy = false;
|
bool isLoading = false, isBusy = false;
|
||||||
double? progress;
|
double? progress;
|
||||||
|
|
||||||
|
SnRealm? realm;
|
||||||
SnPublisher? publisher;
|
SnPublisher? publisher;
|
||||||
SnPost? editingPost, repostingPost, replyingPost;
|
SnPost? editingPost, repostingPost, replyingPost;
|
||||||
|
|
||||||
@@ -194,6 +210,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
PostWriteMedia? thumbnail;
|
PostWriteMedia? thumbnail;
|
||||||
List<PostWriteMedia> attachments = List.empty(growable: true);
|
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||||
DateTime? publishedAt, publishedUntil;
|
DateTime? publishedAt, publishedUntil;
|
||||||
|
SnAttachment? videoAttachment;
|
||||||
|
SnPoll? poll;
|
||||||
|
|
||||||
Future<void> fetchRelatedPost(
|
Future<void> fetchRelatedPost(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
@@ -214,6 +232,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
descriptionController.text = post.body['description'] ?? '';
|
descriptionController.text = post.body['description'] ?? '';
|
||||||
contentController.text = post.body['content'] ?? '';
|
contentController.text = post.body['content'] ?? '';
|
||||||
aliasController.text = post.alias ?? '';
|
aliasController.text = post.alias ?? '';
|
||||||
|
rewardController.text = post.body['reward']?.toString() ?? '';
|
||||||
|
videoAttachment = post.preload?.video;
|
||||||
publishedAt = post.publishedAt;
|
publishedAt = post.publishedAt;
|
||||||
publishedUntil = post.publishedUntil;
|
publishedUntil = post.publishedUntil;
|
||||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||||
@@ -222,10 +242,14 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
||||||
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||||
|
poll = post.preload?.poll;
|
||||||
|
|
||||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||||
thumbnail = PostWriteMedia(post.preload!.thumbnail);
|
thumbnail = PostWriteMedia(post.preload!.thumbnail);
|
||||||
}
|
}
|
||||||
|
if (post.preload?.realm != null) {
|
||||||
|
realm = post.preload!.realm!;
|
||||||
|
}
|
||||||
|
|
||||||
editingPost = post;
|
editingPost = post;
|
||||||
}
|
}
|
||||||
@@ -347,6 +371,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||||
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||||
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
||||||
|
if (rewardController.text.isNotEmpty) 'reward': rewardController.text,
|
||||||
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
|
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
|
||||||
'attachments':
|
'attachments':
|
||||||
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
|
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
|
||||||
@@ -359,6 +384,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||||
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
||||||
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
||||||
|
if (poll != null) 'poll': poll!.toJson(),
|
||||||
|
if (realm != null) 'realm': realm!.toJson(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -375,6 +402,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
aliasController.text = data['alias'] ?? '';
|
aliasController.text = data['alias'] ?? '';
|
||||||
titleController.text = data['title'] ?? '';
|
titleController.text = data['title'] ?? '';
|
||||||
descriptionController.text = data['description'] ?? '';
|
descriptionController.text = data['description'] ?? '';
|
||||||
|
rewardController.text = data['reward']?.toString() ?? '';
|
||||||
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||||
attachments
|
attachments
|
||||||
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
|
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
|
||||||
@@ -387,6 +415,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||||
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||||
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
||||||
|
poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null;
|
||||||
|
realm = data['realm'] != null ? SnRealm.fromJson(data['realm']) : null;
|
||||||
temporaryRestored = true;
|
temporaryRestored = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
});
|
});
|
||||||
@@ -473,6 +503,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
progress = kAttachmentProgressWeight;
|
progress = kAttachmentProgressWeight;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
|
final reward = double.tryParse(rewardController.text);
|
||||||
|
|
||||||
// Posting the content
|
// Posting the content
|
||||||
try {
|
try {
|
||||||
final baseProgressVal = progress!;
|
final baseProgressVal = progress!;
|
||||||
@@ -498,6 +530,10 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||||
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
||||||
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
||||||
|
if (reward != null) 'reward': reward,
|
||||||
|
if (videoAttachment != null) 'video': videoAttachment!.rid,
|
||||||
|
if (poll != null) 'poll': poll!.id,
|
||||||
|
if (realm != null) 'realm': realm!.id,
|
||||||
},
|
},
|
||||||
onSendProgress: (count, total) {
|
onSendProgress: (count, total) {
|
||||||
progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
||||||
@@ -544,17 +580,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setThumbnail(int? idx) {
|
void setThumbnail(SnAttachment? value) {
|
||||||
if (idx == null) {
|
thumbnail = value == null ? null : PostWriteMedia(value);
|
||||||
attachments.add(thumbnail!);
|
|
||||||
thumbnail = null;
|
|
||||||
} else {
|
|
||||||
if (thumbnail != null) {
|
|
||||||
attachments.add(thumbnail!);
|
|
||||||
}
|
|
||||||
thumbnail = attachments[idx];
|
|
||||||
attachments.removeAt(idx);
|
|
||||||
}
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,6 +633,11 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setRealm(SnRealm? value) {
|
||||||
|
realm = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void setProgress(double? value) {
|
void setProgress(double? value) {
|
||||||
progress = value;
|
progress = value;
|
||||||
_temporaryPlanSave();
|
_temporaryPlanSave();
|
||||||
@@ -624,6 +656,16 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setVideoAttachment(SnAttachment? value) {
|
||||||
|
videoAttachment = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPoll(SnPoll? value) {
|
||||||
|
poll = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
publishedAt = null;
|
publishedAt = null;
|
||||||
publishedUntil = null;
|
publishedUntil = null;
|
||||||
|
|||||||
74
lib/database/chat.dart
Normal file
74
lib/database/chat.dart
Normal 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)();
|
||||||
|
}
|
||||||
28
lib/database/database.dart
Normal file
28
lib/database/database.dart
Normal 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'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
880
lib/database/database.g.dart
Normal file
880
lib/database/database.g.dart
Normal 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);
|
||||||
|
}
|
||||||
8
lib/database/drift_worker.dart
Normal file
8
lib/database/drift_worker.dart
Normal 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();
|
||||||
185
lib/main.dart
185
lib/main.dart
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:croppy/croppy.dart';
|
import 'package:croppy/croppy.dart';
|
||||||
@@ -10,8 +11,9 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
|
|||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.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:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
@@ -21,6 +23,7 @@ import 'package:surface/firebase_options.dart';
|
|||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/chat_call.dart';
|
import 'package:surface/providers/chat_call.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/link_preview.dart';
|
import 'package:surface/providers/link_preview.dart';
|
||||||
import 'package:surface/providers/navigation.dart';
|
import 'package:surface/providers/navigation.dart';
|
||||||
import 'package:surface/providers/notification.dart';
|
import 'package:surface/providers/notification.dart';
|
||||||
@@ -28,6 +31,7 @@ import 'package:surface/providers/post.dart';
|
|||||||
import 'package:surface/providers/relationship.dart';
|
import 'package:surface/providers/relationship.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/sn_realm.dart';
|
||||||
import 'package:surface/providers/sn_sticker.dart';
|
import 'package:surface/providers/sn_sticker.dart';
|
||||||
import 'package:surface/providers/special_day.dart';
|
import 'package:surface/providers/special_day.dart';
|
||||||
import 'package:surface/providers/theme.dart';
|
import 'package:surface/providers/theme.dart';
|
||||||
@@ -36,13 +40,14 @@ import 'package:surface/providers/userinfo.dart';
|
|||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
import 'package:surface/providers/widget.dart';
|
import 'package:surface/providers/widget.dart';
|
||||||
import 'package:surface/router.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:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
import 'package:workmanager/workmanager.dart';
|
import 'package:workmanager/workmanager.dart';
|
||||||
import 'package:in_app_review/in_app_review.dart';
|
import 'package:in_app_review/in_app_review.dart';
|
||||||
|
import 'package:image_picker_android/image_picker_android.dart';
|
||||||
|
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void appBackgroundDispatcher() {
|
void appBackgroundDispatcher() {
|
||||||
@@ -63,20 +68,6 @@ void appBackgroundDispatcher() {
|
|||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await EasyLocalization.ensureInitialized();
|
|
||||||
|
|
||||||
await Hive.initFlutter();
|
|
||||||
Hive.registerAdapter(SnChannelImplAdapter());
|
|
||||||
Hive.registerAdapter(SnRealmImplAdapter());
|
|
||||||
Hive.registerAdapter(SnChannelMemberImplAdapter());
|
|
||||||
Hive.registerAdapter(SnChatMessageImplAdapter());
|
|
||||||
|
|
||||||
await Firebase.initializeApp(
|
|
||||||
options: DefaultFirebaseOptions.currentPlatform,
|
|
||||||
);
|
|
||||||
|
|
||||||
GoRouter.optionURLReflectsImperativeAPIs = true;
|
|
||||||
usePathUrlStrategy();
|
|
||||||
|
|
||||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||||
doWhenWindowReady(() {
|
doWhenWindowReady(() {
|
||||||
@@ -87,6 +78,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)) {
|
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||||
Workmanager().initialize(
|
Workmanager().initialize(
|
||||||
appBackgroundDispatcher,
|
appBackgroundDispatcher,
|
||||||
@@ -103,6 +105,14 @@ void main() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!kIsWeb && Platform.isAndroid) {
|
||||||
|
final ImagePickerPlatform imagePickerImplementation =
|
||||||
|
ImagePickerPlatform.instance;
|
||||||
|
if (imagePickerImplementation is ImagePickerAndroid) {
|
||||||
|
imagePickerImplementation.useAndroidPhotoPicker = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
runApp(const SolianApp());
|
runApp(const SolianApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +135,9 @@ class SolianApp extends StatelessWidget {
|
|||||||
assetLoader: JsonAssetLoader(),
|
assetLoader: JsonAssetLoader(),
|
||||||
child: MultiProvider(
|
child: MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
|
// Infrastructure layer
|
||||||
|
Provider(create: (ctx) => DatabaseProvider(ctx)),
|
||||||
|
|
||||||
// System extensions layer
|
// System extensions layer
|
||||||
Provider(create: (ctx) => HomeWidgetProvider(ctx)),
|
Provider(create: (ctx) => HomeWidgetProvider(ctx)),
|
||||||
|
|
||||||
@@ -139,6 +152,7 @@ class SolianApp extends StatelessWidget {
|
|||||||
Provider(create: (ctx) => SnNetworkProvider(ctx)),
|
Provider(create: (ctx) => SnNetworkProvider(ctx)),
|
||||||
Provider(create: (ctx) => UserDirectoryProvider(ctx)),
|
Provider(create: (ctx) => UserDirectoryProvider(ctx)),
|
||||||
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
||||||
|
Provider(create: (ctx) => SnRealmProvider(ctx)),
|
||||||
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
||||||
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
||||||
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
|
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
|
||||||
@@ -156,8 +170,8 @@ class SolianApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
breakpoints: [
|
breakpoints: [
|
||||||
const Breakpoint(start: 0, end: 450, name: MOBILE),
|
const Breakpoint(start: 0, end: 600, name: MOBILE),
|
||||||
const Breakpoint(start: 451, end: 800, name: TABLET),
|
const Breakpoint(start: 601, end: 800, name: TABLET),
|
||||||
const Breakpoint(start: 801, end: 1920, name: DESKTOP),
|
const Breakpoint(start: 801, end: 1920, name: DESKTOP),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -206,13 +220,14 @@ class _AppSplashScreen extends StatefulWidget {
|
|||||||
State<_AppSplashScreen> createState() => _AppSplashScreenState();
|
State<_AppSplashScreen> createState() => _AppSplashScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppSplashScreenState extends State<_AppSplashScreen> {
|
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||||
void _tryRequestRating() async {
|
void _tryRequestRating() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (prefs.containsKey('first_boot_time')) {
|
if (prefs.containsKey('first_boot_time')) {
|
||||||
final rawTime = prefs.getString('first_boot_time');
|
final rawTime = prefs.getString('first_boot_time');
|
||||||
final time = DateTime.tryParse(rawTime ?? '');
|
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;
|
final inAppReview = InAppReview.instance;
|
||||||
if (prefs.getBool('rating_requested') == true) return;
|
if (prefs.getBool('rating_requested') == true) return;
|
||||||
if (await inAppReview.isAvailable()) {
|
if (await inAppReview.isAvailable()) {
|
||||||
@@ -240,13 +255,18 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
).get(
|
).get(
|
||||||
'https://git.solsynth.dev/api/v1/repos/HyperNet/Surface/tags?page=1&limit=1',
|
'https://git.solsynth.dev/api/v1/repos/HyperNet/Surface/tags?page=1&limit=1',
|
||||||
);
|
);
|
||||||
final remoteVersionString = (resp.data as List).firstOrNull?['name'] ?? '0.0.0+0';
|
final remoteVersionString =
|
||||||
|
(resp.data as List).firstOrNull?['name'] ?? '0.0.0+0';
|
||||||
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
|
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
|
||||||
final localVersion = Version.parse(localVersionString.split('+').first);
|
final localVersion = Version.parse(localVersionString.split('+').first);
|
||||||
final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0;
|
final remoteBuildNumber =
|
||||||
final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0;
|
int.tryParse(remoteVersionString.split('+').last) ?? 0;
|
||||||
|
final localBuildNumber =
|
||||||
|
int.tryParse(localVersionString.split('+').last) ?? 0;
|
||||||
log("[Update] Local: $localVersionString, Remote: $remoteVersionString");
|
log("[Update] Local: $localVersionString, Remote: $remoteVersionString");
|
||||||
if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) {
|
if ((remoteVersion > localVersion ||
|
||||||
|
remoteBuildNumber > localBuildNumber) &&
|
||||||
|
mounted) {
|
||||||
final config = context.read<ConfigProvider>();
|
final config = context.read<ConfigProvider>();
|
||||||
config.setUpdate(remoteVersionString);
|
config.setUpdate(remoteVersionString);
|
||||||
log("[Update] Update available: $remoteVersionString");
|
log("[Update] Update available: $remoteVersionString");
|
||||||
@@ -281,6 +301,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
final notify = context.read<NotificationProvider>();
|
final notify = context.read<NotificationProvider>();
|
||||||
notify.listen();
|
notify.listen();
|
||||||
await notify.registerPushNotifications();
|
await notify.registerPushNotifications();
|
||||||
|
if (!mounted) return;
|
||||||
|
final sticker = context.read<SnStickerProvider>();
|
||||||
|
await sticker.listSticker();
|
||||||
|
log('[Bootstrap] Everything initialized!');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await context.showErrorDialog(err);
|
await context.showErrorDialog(err);
|
||||||
@@ -291,9 +315,64 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
await widgetUpdateRandomPost();
|
await widgetUpdateRandomPost();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _hotkeyInitialization() async {
|
||||||
|
if (kIsWeb) return;
|
||||||
|
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
HotKey quitHotKey = HotKey(
|
||||||
|
key: PhysicalKeyboardKey.keyQ,
|
||||||
|
modifiers: [HotKeyModifier.meta],
|
||||||
|
scope: HotKeyScope.inapp,
|
||||||
|
);
|
||||||
|
await hotKeyManager.register(quitHotKey, keyUpHandler: (_) {
|
||||||
|
_appLifecycleListener?.dispose();
|
||||||
|
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _trayInitialization() async {
|
||||||
|
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||||
|
|
||||||
|
final icon = Platform.isWindows
|
||||||
|
? 'assets/icon/tray-icon.ico'
|
||||||
|
: 'assets/icon/tray-icon.png';
|
||||||
|
final appVersion = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
|
trayManager.addListener(this);
|
||||||
|
await trayManager.setIcon(icon);
|
||||||
|
|
||||||
|
Menu menu = Menu(
|
||||||
|
items: [
|
||||||
|
MenuItem(
|
||||||
|
key: 'version_label',
|
||||||
|
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
|
||||||
|
disabled: true,
|
||||||
|
),
|
||||||
|
MenuItem.separator(),
|
||||||
|
MenuItem(
|
||||||
|
key: 'exit',
|
||||||
|
label: 'trayMenuExit'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
await trayManager.setContextMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLifecycleListener? _appLifecycleListener;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
||||||
|
_appLifecycleListener = AppLifecycleListener(
|
||||||
|
onExitRequested: _onExitRequested,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_trayInitialization();
|
||||||
|
_hotkeyInitialization();
|
||||||
_initialize().then((_) {
|
_initialize().then((_) {
|
||||||
_postInitialization();
|
_postInitialization();
|
||||||
_tryRequestRating();
|
_tryRequestRating();
|
||||||
@@ -301,6 +380,50 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<AppExitResponse> _onExitRequested() async {
|
||||||
|
appWindow.hide();
|
||||||
|
return AppExitResponse.cancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onTrayIconMouseDown() {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
context.read<NotificationProvider>().clearTray();
|
||||||
|
appWindow.show();
|
||||||
|
} else {
|
||||||
|
trayManager.popUpContextMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onTrayIconRightMouseDown() {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
trayManager.popUpContextMenu();
|
||||||
|
} else {
|
||||||
|
context.read<NotificationProvider>().clearTray();
|
||||||
|
appWindow.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onTrayMenuItemClick(MenuItem menuItem) {
|
||||||
|
switch (menuItem.key) {
|
||||||
|
case 'exit':
|
||||||
|
_appLifecycleListener?.dispose();
|
||||||
|
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
|
||||||
|
trayManager.removeListener(this);
|
||||||
|
hotKeyManager.unregisterAll();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cfg = context.read<ConfigProvider>();
|
final cfg = context.read<ConfigProvider>();
|
||||||
@@ -311,8 +434,16 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: SizeChangedLayoutNotifier(
|
child: OrientationBuilder(
|
||||||
child: widget.child,
|
builder: (context, orientation) {
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
cfg.calcDrawerSize(context);
|
||||||
|
});
|
||||||
|
return SizeChangedLayoutNotifier(
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,54 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:surface/controllers/chat_message_controller.dart';
|
import 'package:surface/database/database.dart';
|
||||||
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/sn_realm.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
|
||||||
|
|
||||||
class ChatChannelProvider extends ChangeNotifier {
|
class ChatChannelProvider extends ChangeNotifier {
|
||||||
static const kChatChannelBoxName = 'nex_chat_channels';
|
static const kChatChannelBoxName = 'nex_chat_channels';
|
||||||
|
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
late final UserDirectoryProvider _ud;
|
late final UserDirectoryProvider _ud;
|
||||||
|
late final DatabaseProvider _dt;
|
||||||
Box<SnChannel>? get _channelBox => Hive.box<SnChannel>(kChatChannelBoxName);
|
late final SnRealmProvider _rels;
|
||||||
|
|
||||||
ChatChannelProvider(BuildContext context) {
|
ChatChannelProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
_ud = context.read<UserDirectoryProvider>();
|
_ud = context.read<UserDirectoryProvider>();
|
||||||
_initializeLocalData();
|
_dt = context.read<DatabaseProvider>();
|
||||||
}
|
_rels = context.read<SnRealmProvider>();
|
||||||
|
|
||||||
Future<void> _initializeLocalData() async {
|
|
||||||
await Hive.openBox<SnChannel>(kChatChannelBoxName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
|
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
|
||||||
if (_channelBox == null) return;
|
await Future.wait(
|
||||||
await _channelBox!.putAll({
|
channels.map(
|
||||||
for (final channel in channels) channel.key: channel,
|
(ele) => _dt.db.snLocalChatChannel.insertOne(
|
||||||
});
|
SnLocalChatChannelCompanion.insert(
|
||||||
|
id: Value(ele.id),
|
||||||
|
alias: ele.key,
|
||||||
|
content: ele,
|
||||||
|
createdAt: Value(ele.createdAt),
|
||||||
|
),
|
||||||
|
onConflict: DoUpdate(
|
||||||
|
(_) => SnLocalChatChannelCompanion.custom(
|
||||||
|
content: Constant(jsonEncode(ele.toJson())),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SnChannel>> _fetchChannelsFromServer({
|
Future<List<SnChannel>> _fetchChannelsFromServer({
|
||||||
String scope = 'global',
|
|
||||||
bool direct = false,
|
|
||||||
bool doNotSave = false,
|
bool doNotSave = false,
|
||||||
}) async {
|
}) async {
|
||||||
final resp = await _sn.client.get(
|
final resp = await _sn.client.get('/cgi/im/channels/me/available');
|
||||||
'/cgi/im/channels/$scope/me/available',
|
|
||||||
queryParameters: {
|
|
||||||
'direct': direct,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final out = List<SnChannel>.from(
|
final out = List<SnChannel>.from(
|
||||||
resp.data?.map((e) => SnChannel.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnChannel.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
@@ -54,18 +60,21 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
/// It will use the local storage as much as possible.
|
/// It will use the local storage as much as possible.
|
||||||
/// The alias should include the scope, formatted as `scope:alias`.
|
/// The alias should include the scope, formatted as `scope:alias`.
|
||||||
Future<SnChannel> getChannel(String key) async {
|
Future<SnChannel> getChannel(String key) async {
|
||||||
if (_channelBox != null) {
|
final local = await (_dt.db.snLocalChatChannel.select()
|
||||||
final local = _channelBox!.get(key);
|
..where((e) => e.alias.equals(key)))
|
||||||
if (local != null) return local;
|
.getSingleOrNull();
|
||||||
|
if (local != null) {
|
||||||
|
final out = local.content;
|
||||||
|
return out.copyWith(realm: await _rels.getRealm(out.realmId!));
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp = await _sn.client.get('/cgi/im/channels/$key');
|
var resp =
|
||||||
|
await _sn.client.get('/cgi/im/channels/${key.replaceAll(':', '/')}');
|
||||||
var out = SnChannel.fromJson(resp.data);
|
var out = SnChannel.fromJson(resp.data);
|
||||||
|
|
||||||
// Preload realm of the channel
|
// Preload realm of the channel
|
||||||
if (out.realmId != null) {
|
if (out.realmId != null) {
|
||||||
resp = await _sn.client.get('/cgi/id/realms/${out.realmId}');
|
out = out.copyWith(realm: await _rels.getRealm(out.realmId!));
|
||||||
out = out.copyWith(realm: SnRealm.fromJson(resp.data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_saveChannelToLocal([out]);
|
_saveChannelToLocal([out]);
|
||||||
@@ -77,66 +86,63 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
/// And the second time is when the data was fetched from the server.
|
/// And the second time is when the data was fetched from the server.
|
||||||
/// But there is some exception that will only cause one of them to be emitted.
|
/// But there is some exception that will only cause one of them to be emitted.
|
||||||
/// Like the local storage is broken or the server is down.
|
/// Like the local storage is broken or the server is down.
|
||||||
Stream<List<SnChannel>> fetchChannels() async* {
|
Stream<List<SnChannel>> fetchChannels(
|
||||||
if (_channelBox != null) yield _channelBox!.values.toList();
|
{bool noRemote = false, bool noLocal = false}) async* {
|
||||||
|
if (!noLocal) {
|
||||||
var resp = await _sn.client.get('/cgi/id/realms/me/available');
|
final local = await (_dt.db.snLocalChatChannel.select()
|
||||||
final realms = List<SnRealm>.from(
|
..orderBy([
|
||||||
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
(e) =>
|
||||||
);
|
OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
|
||||||
final realmMap = {
|
]))
|
||||||
for (final realm in realms) realm.alias: realm,
|
.get();
|
||||||
};
|
final out = local.map((e) => e.content).toList();
|
||||||
|
for (var idx = 0; idx < out.length; idx++) {
|
||||||
final scopeToFetch = {'global', ...realms.map((e) => e.alias)};
|
final channel = out[idx];
|
||||||
|
if (channel.realmId != null) {
|
||||||
final List<SnChannel> result = List.empty(growable: true);
|
out[idx] = out[idx].copyWith(
|
||||||
final directMessages = await _fetchChannelsFromServer(
|
realm: await _rels.getRealm(channel.realmId!),
|
||||||
scope: scopeToFetch.first,
|
);
|
||||||
direct: true,
|
}
|
||||||
);
|
}
|
||||||
result.addAll(directMessages);
|
yield out;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
yield result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SnChatMessage>> getLastMessages(
|
Future<List<SnChatMessage>> getLastMessages(
|
||||||
Iterable<SnChannel> channels,
|
Iterable<SnChannel> channels,
|
||||||
) async {
|
) async {
|
||||||
final result = List<SnChatMessage>.empty(growable: true);
|
final result = List<Future<SnLocalChatMessageData?>>.empty(growable: true);
|
||||||
for (final channel in channels) {
|
for (final channel in channels) {
|
||||||
final channelBox = await Hive.openBox<SnChatMessage>(
|
final out = (_dt.db.snLocalChatMessage.select()
|
||||||
'${ChatMessageController.kChatMessageBoxPrefix}${channel.id}',
|
..where((e) => e.channelId.equals(channel.id))
|
||||||
);
|
..orderBy([
|
||||||
final lastMessage =
|
(e) =>
|
||||||
channelBox.isNotEmpty ? channelBox.values.reduce((a, b) => a.createdAt.isAfter(b.createdAt) ? a : b) : null;
|
OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
|
||||||
if (lastMessage != null) result.add(lastMessage);
|
])
|
||||||
channelBox.close();
|
..limit(1))
|
||||||
|
.getSingleOrNull();
|
||||||
|
result.add(out);
|
||||||
}
|
}
|
||||||
await _ud.listAccount(result.map((ele) => ele.sender.accountId).toSet());
|
final out = (await Future.wait(result))
|
||||||
return result;
|
.where((e) => e != null)
|
||||||
}
|
.map((e) => e!.content)
|
||||||
|
.toList();
|
||||||
@override
|
await _ud.listAccount(out.map((ele) => ele.sender.accountId).toSet());
|
||||||
void dispose() {
|
return out;
|
||||||
_channelBox?.close();
|
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
|
|||||||
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
|
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
|
||||||
const kAppExpandPostLink = 'app_expand_post_link';
|
const kAppExpandPostLink = 'app_expand_post_link';
|
||||||
const kAppExpandChatLink = 'app_expand_chat_link';
|
const kAppExpandChatLink = 'app_expand_chat_link';
|
||||||
|
const kAppRealmCompactView = 'app_realm_compact_view';
|
||||||
|
|
||||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||||
'settingsImageQualityLowest': FilterQuality.none,
|
'settingsImageQualityLowest': FilterQuality.none,
|
||||||
@@ -45,8 +46,8 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
bool newDrawerIsCollapsed = false;
|
bool newDrawerIsCollapsed = false;
|
||||||
bool newDrawerIsExpanded = false;
|
bool newDrawerIsExpanded = false;
|
||||||
if (withMediaQuery) {
|
if (withMediaQuery) {
|
||||||
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450;
|
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600;
|
||||||
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451;
|
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 601;
|
||||||
} else {
|
} else {
|
||||||
final rpb = ResponsiveBreakpoints.of(context);
|
final rpb = ResponsiveBreakpoints.of(context);
|
||||||
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
|
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
|
||||||
@@ -72,6 +73,13 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get realmCompactView {
|
||||||
|
return prefs.getBool(kAppRealmCompactView) ?? false;
|
||||||
|
}
|
||||||
|
set realmCompactView(bool value) {
|
||||||
|
prefs.setBool(kAppRealmCompactView, value);
|
||||||
|
}
|
||||||
|
|
||||||
set serverUrl(String url) {
|
set serverUrl(String url) {
|
||||||
prefs.setString(kNetworkServerStoreKey, url);
|
prefs.setString(kNetworkServerStoreKey, url);
|
||||||
_home.saveWidgetData("nex_server_url", url);
|
_home.saveWidgetData("nex_server_url", url);
|
||||||
|
|||||||
31
lib/providers/database.dart
Normal file
31
lib/providers/database.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,11 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
screen: 'news',
|
screen: 'news',
|
||||||
label: 'screenNews',
|
label: 'screenNews',
|
||||||
),
|
),
|
||||||
|
AppNavDestination(
|
||||||
|
icon: Icon(Symbols.emoji_emotions, weight: 400, opticalSize: 20),
|
||||||
|
screen: 'stickers',
|
||||||
|
label: 'screenStickers',
|
||||||
|
),
|
||||||
AppNavDestination(
|
AppNavDestination(
|
||||||
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
||||||
screen: 'album',
|
screen: 'album',
|
||||||
@@ -88,7 +93,8 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
List<AppNavDestination> destinations = [];
|
List<AppNavDestination> destinations = [];
|
||||||
|
|
||||||
int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
|
int get pinnedDestinationCount =>
|
||||||
|
destinations.where((ele) => ele.isPinned).length;
|
||||||
|
|
||||||
NavigationProvider() {
|
NavigationProvider() {
|
||||||
buildDestinations(kDefaultPinnedDestination);
|
buildDestinations(kDefaultPinnedDestination);
|
||||||
@@ -117,13 +123,17 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isIndexInRange(int min, int max) {
|
bool isIndexInRange(int min, int max) {
|
||||||
return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
|
return _currentIndex != null &&
|
||||||
|
_currentIndex! >= min &&
|
||||||
|
_currentIndex! < max;
|
||||||
}
|
}
|
||||||
|
|
||||||
void autoDetectIndex(GoRouter? state) {
|
void autoDetectIndex(GoRouter? state) {
|
||||||
if (state == null) return;
|
if (state == null) return;
|
||||||
final idx = destinations.indexWhere(
|
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;
|
_currentIndex = idx == -1 ? null : idx;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:surface/providers/sn_network.dart';
|
|||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
import 'package:surface/types/notification.dart';
|
import 'package:surface/types/notification.dart';
|
||||||
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
|
|
||||||
class NotificationProvider extends ChangeNotifier {
|
class NotificationProvider extends ChangeNotifier {
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
@@ -71,22 +72,48 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int showingCount = 0;
|
||||||
|
int showingTrayCount = 0;
|
||||||
List<SnNotification> notifications = List.empty(growable: true);
|
List<SnNotification> notifications = List.empty(growable: true);
|
||||||
|
|
||||||
void listen() {
|
void listen() {
|
||||||
_ws.stream.stream.listen((event) {
|
_ws.pk.stream.listen((event) {
|
||||||
if (event.method == 'notifications.new') {
|
if (event.method == 'notifications.new') {
|
||||||
final notification = SnNotification.fromJson(event.payload!);
|
final notification = SnNotification.fromJson(event.payload!);
|
||||||
|
if (showingCount < 0) showingCount = 0;
|
||||||
|
showingCount++;
|
||||||
|
showingTrayCount++;
|
||||||
notifications.add(notification);
|
notifications.add(notification);
|
||||||
|
Future.delayed(const Duration(seconds: 5), () {
|
||||||
|
if (showingCount >= 0) showingCount--;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
updateTray();
|
||||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||||
if (doHaptic) HapticFeedback.lightImpact();
|
if (doHaptic) HapticFeedback.mediumImpact();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearTray() {
|
||||||
|
showingTrayCount = 0;
|
||||||
|
updateTray();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTray() {
|
||||||
|
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||||
|
if (showingTrayCount == 0) {
|
||||||
|
trayManager.setTitle('');
|
||||||
|
} else {
|
||||||
|
trayManager.setTitle(' $showingTrayCount');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
|
showingCount = 0;
|
||||||
notifications.clear();
|
notifications.clear();
|
||||||
|
updateTray();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,28 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/sn_realm.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
|
import 'package:surface/types/poll.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
|
import 'package:surface/types/realm.dart';
|
||||||
|
|
||||||
class SnPostContentProvider {
|
class SnPostContentProvider {
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
late final UserDirectoryProvider _ud;
|
late final UserDirectoryProvider _ud;
|
||||||
late final SnAttachmentProvider _attach;
|
late final SnAttachmentProvider _attach;
|
||||||
|
late final SnRealmProvider _realm;
|
||||||
|
|
||||||
SnPostContentProvider(BuildContext context) {
|
SnPostContentProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
_ud = context.read<UserDirectoryProvider>();
|
_ud = context.read<UserDirectoryProvider>();
|
||||||
_attach = context.read<SnAttachmentProvider>();
|
_attach = context.read<SnAttachmentProvider>();
|
||||||
|
_realm = context.read<SnRealmProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnPoll> _fetchPoll(int id) async {
|
||||||
|
final resp = await _sn.client.get('/cgi/co/polls/$id');
|
||||||
|
return SnPoll.fromJson(resp.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
|
Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
|
||||||
@@ -23,6 +33,9 @@ class SnPostContentProvider {
|
|||||||
if (out[i].body['thumbnail'] != null) {
|
if (out[i].body['thumbnail'] != null) {
|
||||||
rids.add(out[i].body['thumbnail']);
|
rids.add(out[i].body['thumbnail']);
|
||||||
}
|
}
|
||||||
|
if (out[i].body['video'] != null) {
|
||||||
|
rids.add(out[i].body['video']);
|
||||||
|
}
|
||||||
if (out[i].repostTo != null) {
|
if (out[i].repostTo != null) {
|
||||||
out[i] = out[i].copyWith(
|
out[i] = out[i].copyWith(
|
||||||
repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
|
repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
|
||||||
@@ -32,10 +45,22 @@ class SnPostContentProvider {
|
|||||||
|
|
||||||
final attachments = await _attach.getMultiple(rids.toList());
|
final attachments = await _attach.getMultiple(rids.toList());
|
||||||
for (var i = 0; i < out.length; i++) {
|
for (var i = 0; i < out.length; i++) {
|
||||||
|
SnPoll? poll;
|
||||||
|
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(
|
out[i] = out[i].copyWith(
|
||||||
preload: SnPostPreload(
|
preload: SnPostPreload(
|
||||||
thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
|
thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
|
||||||
attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
||||||
|
video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull,
|
||||||
|
poll: poll,
|
||||||
|
realm: realm,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,6 +78,9 @@ class SnPostContentProvider {
|
|||||||
if (out.body['thumbnail'] != null) {
|
if (out.body['thumbnail'] != null) {
|
||||||
rids.add(out.body['thumbnail']);
|
rids.add(out.body['thumbnail']);
|
||||||
}
|
}
|
||||||
|
if (out.body['video'] != null) {
|
||||||
|
rids.add(out.body['video']);
|
||||||
|
}
|
||||||
if (out.repostTo != null) {
|
if (out.repostTo != null) {
|
||||||
out = out.copyWith(
|
out = out.copyWith(
|
||||||
repostTo: await _preloadRelatedDataSingle(out.repostTo!),
|
repostTo: await _preloadRelatedDataSingle(out.repostTo!),
|
||||||
@@ -60,10 +88,23 @@ class SnPostContentProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final attachments = await _attach.getMultiple(rids.toList());
|
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(
|
out = out.copyWith(
|
||||||
preload: SnPostPreload(
|
preload: SnPostPreload(
|
||||||
thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
|
thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
|
||||||
attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
||||||
|
video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull,
|
||||||
|
poll: poll,
|
||||||
|
realm: realm,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -85,6 +126,8 @@ class SnPostContentProvider {
|
|||||||
String? author,
|
String? author,
|
||||||
Iterable<String>? categories,
|
Iterable<String>? categories,
|
||||||
Iterable<String>? tags,
|
Iterable<String>? tags,
|
||||||
|
String? realm,
|
||||||
|
String? channel,
|
||||||
}) async {
|
}) async {
|
||||||
final resp = await _sn.client.get('/cgi/co/posts', queryParameters: {
|
final resp = await _sn.client.get('/cgi/co/posts', queryParameters: {
|
||||||
'take': take,
|
'take': take,
|
||||||
@@ -93,6 +136,8 @@ class SnPostContentProvider {
|
|||||||
if (author != null) 'author': author,
|
if (author != null) 'author': author,
|
||||||
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
||||||
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
|
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
|
||||||
|
if (realm != null) 'realm': realm,
|
||||||
|
if (channel != null) 'channel': channel,
|
||||||
});
|
});
|
||||||
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
||||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||||
|
|||||||
37
lib/providers/sn_realm.dart
Normal file
37
lib/providers/sn_realm.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,11 @@ class SnStickerProvider {
|
|||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
final Map<String, SnSticker?> _cache = {};
|
final Map<String, SnSticker?> _cache = {};
|
||||||
|
|
||||||
|
final Map<int, List<SnSticker>> stickersByPack = {};
|
||||||
|
|
||||||
|
List<SnSticker> get stickers =>
|
||||||
|
_cache.values.where((ele) => ele != null).cast<SnSticker>().toList();
|
||||||
|
|
||||||
SnStickerProvider(BuildContext context) {
|
SnStickerProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
}
|
}
|
||||||
@@ -17,6 +22,22 @@ class SnStickerProvider {
|
|||||||
return _cache.containsKey(alias) && _cache[alias] == null;
|
return _cache.containsKey(alias) && _cache[alias] == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _cacheSticker(SnSticker sticker) {
|
||||||
|
_cache['${sticker.pack.prefix}:${sticker.alias}'] = sticker;
|
||||||
|
if (stickersByPack[sticker.pack.id] == null) {
|
||||||
|
stickersByPack[sticker.pack.id] = List.empty(growable: true);
|
||||||
|
}
|
||||||
|
if (!stickersByPack[sticker.pack.id]!.contains(sticker)) {
|
||||||
|
stickersByPack[sticker.pack.id]!.add(sticker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void putSticker(Iterable<SnSticker> sticker) {
|
||||||
|
for (final ele in sticker) {
|
||||||
|
_cacheSticker(ele);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<SnSticker?> lookupSticker(String alias) async {
|
Future<SnSticker?> lookupSticker(String alias) async {
|
||||||
if (_cache.containsKey(alias)) {
|
if (_cache.containsKey(alias)) {
|
||||||
return _cache[alias];
|
return _cache[alias];
|
||||||
@@ -25,7 +46,7 @@ class SnStickerProvider {
|
|||||||
try {
|
try {
|
||||||
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
|
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
|
||||||
final sticker = SnSticker.fromJson(resp.data);
|
final sticker = SnSticker.fromJson(resp.data);
|
||||||
_cache[alias] = sticker;
|
_cacheSticker(sticker);
|
||||||
|
|
||||||
return sticker;
|
return sticker;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -35,4 +56,18 @@ class SnStickerProvider {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> listSticker() async {
|
||||||
|
try {
|
||||||
|
final resp = await _sn.client.get('/cgi/uc/stickers');
|
||||||
|
final data = resp.data;
|
||||||
|
final stickers = List.from(data).map((ele) => SnSticker.fromJson(ele));
|
||||||
|
for (final sticker in stickers) {
|
||||||
|
_cacheSticker(sticker);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log('[Sticker] Failed to list stickers: $err');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,32 @@ class UserDirectoryProvider {
|
|||||||
final Map<int, SnAccount> _cache = {};
|
final Map<int, SnAccount> _cache = {};
|
||||||
|
|
||||||
Future<List<SnAccount?>> listAccount(Iterable<dynamic> id) async {
|
Future<List<SnAccount?>> listAccount(Iterable<dynamic> id) async {
|
||||||
final out = await Future.wait(
|
final out = List<SnAccount?>.generate(id.length, (e) => null);
|
||||||
id.map((e) => getAccount(e)),
|
final plannedQuery = <int>{};
|
||||||
);
|
for (var idx = 0; idx < out.length; idx++) {
|
||||||
|
var item = id.elementAt(idx);
|
||||||
|
if (item is String && _idCache.containsKey(item)) {
|
||||||
|
item = _idCache[item];
|
||||||
|
}
|
||||||
|
if (_cache.containsKey(item)) {
|
||||||
|
out[idx] = _cache[item];
|
||||||
|
} else {
|
||||||
|
plannedQuery.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final resp = await _sn.client.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
|
||||||
|
final respDecoded = resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList();
|
||||||
|
var sideIdx = 0;
|
||||||
|
for (var idx = 0; idx < out.length; idx++) {
|
||||||
|
if (out[idx] != null) continue;
|
||||||
|
if (respDecoded.length <= sideIdx) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out[idx] = respDecoded[sideIdx];
|
||||||
|
_cache[respDecoded[sideIdx].id] = out[idx]!;
|
||||||
|
_idCache[respDecoded[sideIdx].name] = respDecoded[sideIdx].id;
|
||||||
|
sideIdx++;
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,4 +53,11 @@ class UserProvider extends ChangeNotifier {
|
|||||||
user = null;
|
user = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setLanguage(String? value) {
|
||||||
|
if (value == null) return;
|
||||||
|
if (user == null) return;
|
||||||
|
user = user!.copyWith(language: value);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
late final UserProvider _ua;
|
late final UserProvider _ua;
|
||||||
|
|
||||||
StreamController<WebSocketPackage> stream = StreamController.broadcast();
|
StreamController<WebSocketPackage> pk = StreamController.broadcast();
|
||||||
|
Stream<dynamic>? _wsStream;
|
||||||
|
|
||||||
WebSocketProvider(BuildContext context) {
|
WebSocketProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
@@ -33,23 +34,33 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
await connect();
|
await connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Completer<void>? _connectCompleter;
|
||||||
|
|
||||||
Future<void> connect({noRetry = false}) async {
|
Future<void> connect({noRetry = false}) async {
|
||||||
|
if (_connectCompleter != null) {
|
||||||
|
await _connectCompleter!.future;
|
||||||
|
_connectCompleter = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_ua.isAuthorized) return;
|
if (!_ua.isAuthorized) return;
|
||||||
if (isConnected || conn != null) {
|
if (isConnected || conn != null) {
|
||||||
disconnect();
|
disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
final atk = await _sn.getFreshAtk();
|
|
||||||
final uri = Uri.parse(
|
|
||||||
'${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk',
|
|
||||||
);
|
|
||||||
|
|
||||||
isBusy = true;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
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);
|
conn = WebSocketChannel.connect(uri);
|
||||||
await conn!.ready;
|
await conn!.ready;
|
||||||
|
_wsStream = conn!.stream.asBroadcastStream();
|
||||||
listen();
|
listen();
|
||||||
log('[WebSocket] Connected to server!');
|
log('[WebSocket] Connected to server!');
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
@@ -70,6 +81,8 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
} finally {
|
} finally {
|
||||||
isBusy = false;
|
isBusy = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
_connectCompleter!.complete();
|
||||||
|
_connectCompleter = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,11 +96,12 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void listen() {
|
void listen() {
|
||||||
conn?.stream.listen(
|
if (_wsStream == null) return;
|
||||||
|
_wsStream!.listen(
|
||||||
(event) {
|
(event) {
|
||||||
final packet = WebSocketPackage.fromJson(jsonDecode(event));
|
final packet = WebSocketPackage.fromJson(jsonDecode(event));
|
||||||
log('Websocket incoming message: ${packet.method} ${packet.message}');
|
log('Websocket incoming message: ${packet.method} ${packet.message}');
|
||||||
stream.sink.add(packet);
|
pk.sink.add(packet);
|
||||||
},
|
},
|
||||||
onDone: () {
|
onDone: () {
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class HomeWidgetProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> widgetUpdateRandomPost() async {
|
Future<void> widgetUpdateRandomPost() async {
|
||||||
|
if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) return;
|
||||||
final snc = await SnNetworkProvider.createOffContextClient();
|
final snc = await SnNetworkProvider.createOffContextClient();
|
||||||
final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1');
|
final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1');
|
||||||
final post = SnPost.fromJson(resp.data['data'][0]);
|
final post = SnPost.fromJson(resp.data['data'][0]);
|
||||||
|
|||||||
133
lib/router.dart
133
lib/router.dart
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:surface/screens/abuse_report.dart';
|
import 'package:surface/screens/abuse_report.dart';
|
||||||
import 'package:surface/screens/account.dart';
|
import 'package:surface/screens/account.dart';
|
||||||
|
import 'package:surface/screens/account/account_settings.dart';
|
||||||
|
import 'package:surface/screens/account/factor_settings.dart';
|
||||||
import 'package:surface/screens/account/profile_page.dart';
|
import 'package:surface/screens/account/profile_page.dart';
|
||||||
import 'package:surface/screens/account/profile_edit.dart';
|
import 'package:surface/screens/account/profile_edit.dart';
|
||||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||||
@@ -29,14 +31,18 @@ import 'package:surface/screens/post/post_search.dart';
|
|||||||
import 'package:surface/screens/realm.dart';
|
import 'package:surface/screens/realm.dart';
|
||||||
import 'package:surface/screens/realm/manage.dart';
|
import 'package:surface/screens/realm/manage.dart';
|
||||||
import 'package:surface/screens/realm/realm_detail.dart';
|
import 'package:surface/screens/realm/realm_detail.dart';
|
||||||
|
import 'package:surface/screens/realm/realm_discovery.dart';
|
||||||
import 'package:surface/screens/settings.dart';
|
import 'package:surface/screens/settings.dart';
|
||||||
import 'package:surface/screens/sharing.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/types/post.dart';
|
||||||
import 'package:surface/widgets/about.dart';
|
import 'package:surface/widgets/about.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
Widget _fadeThroughTransition(
|
Widget _fadeThroughTransition(BuildContext context, Animation<double> animation,
|
||||||
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
Animation<double> secondaryAnimation, Widget child) {
|
||||||
return FadeThroughTransition(
|
return FadeThroughTransition(
|
||||||
animation: animation,
|
animation: animation,
|
||||||
secondaryAnimation: secondaryAnimation,
|
secondaryAnimation: secondaryAnimation,
|
||||||
@@ -70,7 +76,7 @@ final _appRoutes = [
|
|||||||
postRepostId: int.tryParse(
|
postRepostId: int.tryParse(
|
||||||
state.uri.queryParameters['reposting'] ?? '',
|
state.uri.queryParameters['reposting'] ?? '',
|
||||||
),
|
),
|
||||||
extraProps: state.extra as PostEditorExtraProps?,
|
extraProps: state.extra as PostEditorExtra?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
@@ -78,13 +84,15 @@ final _appRoutes = [
|
|||||||
name: 'postSearch',
|
name: 'postSearch',
|
||||||
builder: (context, state) => PostSearchScreen(
|
builder: (context, state) => PostSearchScreen(
|
||||||
initialTags: state.uri.queryParameters['tags']?.split(','),
|
initialTags: state.uri.queryParameters['tags']?.split(','),
|
||||||
initialCategories: state.uri.queryParameters['categories']?.split(','),
|
initialCategories:
|
||||||
|
state.uri.queryParameters['categories']?.split(','),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/publishers/:name',
|
path: '/publishers/:name',
|
||||||
name: 'postPublisher',
|
name: 'postPublisher',
|
||||||
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
|
builder: (context, state) =>
|
||||||
|
PostPublisherScreen(name: state.pathParameters['name']!),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/:slug',
|
path: '/:slug',
|
||||||
@@ -97,10 +105,55 @@ final _appRoutes = [
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/account',
|
path: '/account',
|
||||||
name: 'account',
|
name: 'account',
|
||||||
builder: (context, state) => const AccountScreen(),
|
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(
|
GoRoute(
|
||||||
path: '/chat',
|
path: '/chat',
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
@@ -112,6 +165,7 @@ final _appRoutes = [
|
|||||||
builder: (context, state) => ChatRoomScreen(
|
builder: (context, state) => ChatRoomScreen(
|
||||||
scope: state.pathParameters['scope']!,
|
scope: state.pathParameters['scope']!,
|
||||||
alias: state.pathParameters['alias']!,
|
alias: state.pathParameters['alias']!,
|
||||||
|
extra: state.extra as ChatRoomScreenExtra?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
@@ -147,11 +201,6 @@ final _appRoutes = [
|
|||||||
child: const RealmScreen(),
|
child: const RealmScreen(),
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
|
||||||
path: '/:alias',
|
|
||||||
name: 'realmDetail',
|
|
||||||
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/manage',
|
path: '/manage',
|
||||||
name: 'realmManage',
|
name: 'realmManage',
|
||||||
@@ -159,6 +208,17 @@ final _appRoutes = [
|
|||||||
editingRealmAlias: state.uri.queryParameters['editing'],
|
editingRealmAlias: state.uri.queryParameters['editing'],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/discovery',
|
||||||
|
name: 'realmDiscovery',
|
||||||
|
builder: (context, state) => const RealmDiscoveryScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/:alias',
|
||||||
|
name: 'realmDetail',
|
||||||
|
builder: (context, state) =>
|
||||||
|
RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
@@ -173,7 +233,21 @@ final _appRoutes = [
|
|||||||
hash: state.pathParameters['hash']!,
|
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(
|
GoRoute(
|
||||||
path: '/album',
|
path: '/album',
|
||||||
@@ -205,35 +279,6 @@ final _appRoutes = [
|
|||||||
name: 'abuseReport',
|
name: 'abuseReport',
|
||||||
builder: (context, state) => AbuseReportScreen(),
|
builder: (context, state) => AbuseReportScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
path: '/account/profile/edit',
|
|
||||||
name: 'accountProfileEdit',
|
|
||||||
builder: (context, state) => ProfileEditScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/account/publishers',
|
|
||||||
name: 'accountPublishers',
|
|
||||||
builder: (context, state) => PublisherScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/account/publishers/new',
|
|
||||||
name: 'accountPublisherNew',
|
|
||||||
builder: (context, state) => AccountPublisherNewScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/account/publishers/edit/:name',
|
|
||||||
name: 'accountPublisherEdit',
|
|
||||||
builder: (context, state) => AccountPublisherEditScreen(
|
|
||||||
name: state.pathParameters['name']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/account/:name',
|
|
||||||
name: 'accountProfilePage',
|
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
|
||||||
child: UserScreen(name: state.pathParameters['name']!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
|
|||||||
@@ -74,7 +74,10 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
|||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
if (_isBusy)
|
if (_isBusy)
|
||||||
const CircularProgressIndicator().padding(all: 24).center()
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
).center()
|
||||||
else
|
else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
@@ -13,6 +15,7 @@ import 'package:surface/widgets/account/account_image.dart';
|
|||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
class AccountScreen extends StatelessWidget {
|
class AccountScreen extends StatelessWidget {
|
||||||
const AccountScreen({super.key});
|
const AccountScreen({super.key});
|
||||||
@@ -20,11 +23,52 @@ class AccountScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text("screenAccount").tr(),
|
title: Text(
|
||||||
|
"screenAccount",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(1, 1),
|
||||||
|
blurRadius: 5.0,
|
||||||
|
color: Color.fromARGB(255, 0, 0, 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
||||||
|
? Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner),
|
||||||
|
fit: BoxFit.cover),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 56 + MediaQuery.of(context).padding.top,
|
||||||
|
child: ClipRect(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(
|
||||||
|
sigmaX: 10,
|
||||||
|
sigmaY: 10,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(
|
||||||
|
clampDouble(10 * 0.1, 0, 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.settings, fill: 1),
|
icon: const Icon(Symbols.settings, fill: 1),
|
||||||
@@ -36,7 +80,9 @@ class AccountScreen extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: ua.isAuthorized ? _AuthorizedAccountScreen() : _UnauthorizedAccountScreen(),
|
child: ua.isAuthorized
|
||||||
|
? _AuthorizedAccountScreen()
|
||||||
|
: _UnauthorizedAccountScreen(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -72,27 +118,20 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
textBaseline: TextBaseline.alphabetic,
|
textBaseline: TextBaseline.alphabetic,
|
||||||
children: [
|
children: [
|
||||||
Text(ua.user!.nick).textStyle(Theme.of(context).textTheme.titleLarge!),
|
Text(ua.user!.nick)
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
const Gap(4),
|
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!),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).padding(all: 20),
|
}).padding(all: 20),
|
||||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||||
ListTile(
|
|
||||||
title: Text('accountProfileEdit').tr(),
|
|
||||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
leading: const Icon(Symbols.contact_page),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed('accountProfileEdit');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('accountPublishers').tr(),
|
title: Text('accountPublishers').tr(),
|
||||||
subtitle: Text('accountPublishersSubtitle').tr(),
|
subtitle: Text('accountPublishersSubtitle').tr(),
|
||||||
@@ -113,6 +152,36 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
|||||||
GoRouter.of(context).pushNamed('abuseReport');
|
GoRouter.of(context).pushNamed('abuseReport');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('factorSettings').tr(),
|
||||||
|
subtitle: Text('factorSettingsSubtitle').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.lock),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed('factorSettings');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('accountWallet').tr(),
|
||||||
|
subtitle: Text('accountWalletSubtitle').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.wallet),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed('accountWallet');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('accountSettings').tr(),
|
||||||
|
subtitle: Text('accountSettingsSubtitle').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.manage_accounts),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed('accountSettings');
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('accountLogout').tr(),
|
title: Text('accountLogout').tr(),
|
||||||
subtitle: Text('accountLogoutSubtitle').tr(),
|
subtitle: Text('accountLogoutSubtitle').tr(),
|
||||||
@@ -130,35 +199,7 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
|||||||
ua.logoutUser();
|
ua.logoutUser();
|
||||||
final ws = context.read<WebSocketProvider>();
|
final ws = context.read<WebSocketProvider>();
|
||||||
ws.disconnect();
|
ws.disconnect();
|
||||||
await Hive.deleteFromDisk();
|
context.read<DatabaseProvider>().removeDatabase();
|
||||||
await Hive.initFlutter();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text('accountDeletion'.tr()),
|
|
||||||
subtitle: Text('accountDeletionActionDescription'.tr()),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
leading: const Icon(Symbols.person_cancel),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
context
|
|
||||||
.showConfirmDialog(
|
|
||||||
'accountDeletion'.tr(),
|
|
||||||
'accountDeletionDescription'.tr(),
|
|
||||||
)
|
|
||||||
.then((value) {
|
|
||||||
if (!value || !context.mounted) return;
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
sn.client.post('/cgi/id/users/me/deletion').then((value) {
|
|
||||||
if (context.mounted) {
|
|
||||||
context.showSnackbar('accountDeletionSubmitted'.tr());
|
|
||||||
}
|
|
||||||
}).catchError((err) {
|
|
||||||
if (context.mounted) {
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -184,7 +225,9 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
|
|||||||
child: Icon(Symbols.waving_hand, size: 28),
|
child: Icon(Symbols.waving_hand, size: 28),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('accountIntroTitle').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
|
Text('accountIntroTitle')
|
||||||
|
.tr()
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
Text('accountIntroSubtitle').tr(),
|
Text('accountIntroSubtitle').tr(),
|
||||||
],
|
],
|
||||||
).padding(all: 20),
|
).padding(all: 20),
|
||||||
|
|||||||
126
lib/screens/account/account_settings.dart
Normal file
126
lib/screens/account/account_settings.dart
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.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/userinfo.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:intl/locale.dart';
|
||||||
|
|
||||||
|
class AccountSettingsScreen extends StatelessWidget {
|
||||||
|
const AccountSettingsScreen({super.key});
|
||||||
|
|
||||||
|
Future<void> _setAccountLanguage(BuildContext context, Locale? value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
await sn.client.put('/cgi/id/users/me/language', data: {
|
||||||
|
'language': value.toString(),
|
||||||
|
});
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showSnackbar('accountSettingsApplied'.tr());
|
||||||
|
await ua.refreshUser();
|
||||||
|
} catch (err) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ua = context.watch<UserProvider>();
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: PageBackButton(),
|
||||||
|
title: Text('screenAccountSettings').tr(),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text('settingsAccountLanguage').tr(),
|
||||||
|
subtitle: Text('settingsAccountLanguageDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
leading: const Icon(Symbols.translate),
|
||||||
|
trailing: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<Locale?>(
|
||||||
|
isExpanded: true,
|
||||||
|
items: [
|
||||||
|
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
|
||||||
|
return DropdownMenuItem<Locale?>(
|
||||||
|
value: Locale.parse(ele.toString()),
|
||||||
|
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'),
|
||||||
|
onChanged: (Locale? value) {
|
||||||
|
if (value == null) return;
|
||||||
|
_setAccountLanguage(context, value);
|
||||||
|
ua.setLanguage(value.toString());
|
||||||
|
},
|
||||||
|
buttonStyleData: const ButtonStyleData(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 5,
|
||||||
|
),
|
||||||
|
height: 40,
|
||||||
|
width: 160,
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('accountProfileEdit').tr(),
|
||||||
|
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.contact_page),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed('accountProfileEdit');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('accountDeletion'.tr()),
|
||||||
|
subtitle: Text('accountDeletionActionDescription'.tr()),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.person_cancel),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
context
|
||||||
|
.showConfirmDialog(
|
||||||
|
'accountDeletion'.tr(),
|
||||||
|
'accountDeletionDescription'.tr(),
|
||||||
|
)
|
||||||
|
.then((value) {
|
||||||
|
if (!value || !context.mounted) return;
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
sn.client.post('/cgi/id/users/me/deletion').then((value) {
|
||||||
|
if (context.mounted) {
|
||||||
|
context.showSnackbar('accountDeletionSubmitted'.tr());
|
||||||
|
}
|
||||||
|
}).catchError((err) {
|
||||||
|
if (context.mounted) {
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
294
lib/screens/account/factor_settings.dart
Normal file
294
lib/screens/account/factor_settings.dart
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
|
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:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/auth.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
|
final Map<int, (String, String, IconData)> kFactorTypes = {
|
||||||
|
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
|
||||||
|
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
|
||||||
|
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
|
||||||
|
3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active),
|
||||||
|
};
|
||||||
|
|
||||||
|
class FactorSettingsScreen extends StatefulWidget {
|
||||||
|
const FactorSettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FactorSettingsScreen> createState() => _FactorSettingsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
List<SnAuthFactor>? _factors;
|
||||||
|
|
||||||
|
Future<void> _fetchFactors() async {
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/id/users/me/factors');
|
||||||
|
_factors = List<SnAuthFactor>.from(
|
||||||
|
resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [],
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchFactors();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: PageBackButton(),
|
||||||
|
title: Text('screenFactorSettings').tr(),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
LoadingIndicator(
|
||||||
|
isActive: _isBusy,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('authFactorAdd').tr(),
|
||||||
|
subtitle: Text('authFactorAddSubtitle').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.add),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _FactorNewDialog(
|
||||||
|
currentlyHave: _factors!,
|
||||||
|
),
|
||||||
|
).then((val) {
|
||||||
|
if (val == true) _fetchFactors();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: MediaQuery.removePadding(
|
||||||
|
context: context,
|
||||||
|
removeTop: true,
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: _fetchFactors,
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _factors?.length ?? 0,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final ele = _factors![idx];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(kFactorTypes[ele.type]!.$1).tr(),
|
||||||
|
subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 12),
|
||||||
|
leading: Icon(kFactorTypes[ele.type]!.$3),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Symbols.close),
|
||||||
|
onPressed: ele.type > 0
|
||||||
|
? () {
|
||||||
|
context
|
||||||
|
.showConfirmDialog(
|
||||||
|
'authFactorDelete'.tr(),
|
||||||
|
'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]),
|
||||||
|
)
|
||||||
|
.then((val) async {
|
||||||
|
if (!val) return;
|
||||||
|
try {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.delete('/cgi/id/users/me/factors/${ele.id}');
|
||||||
|
_fetchFactors();
|
||||||
|
} catch (err) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FactorNewDialog extends StatefulWidget {
|
||||||
|
final List<SnAuthFactor> currentlyHave;
|
||||||
|
|
||||||
|
const _FactorNewDialog({required this.currentlyHave});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_FactorNewDialog> createState() => _FactorNewDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FactorNewDialogState extends State<_FactorNewDialog> {
|
||||||
|
int? _factorType;
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.post('/cgi/id/users/me/factors', data: {
|
||||||
|
'type': _factorType,
|
||||||
|
});
|
||||||
|
final factor = SnAuthFactor.fromJson(resp.data);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (factor.type == 2) {
|
||||||
|
await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _FactorTotpFactorDialog(factor: factor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('authFactorAdd').tr(),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<int>(
|
||||||
|
hint: Text(
|
||||||
|
'Select Item',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
value: _factorType,
|
||||||
|
items: kFactorTypes.entries.map(
|
||||||
|
(ele) {
|
||||||
|
final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key);
|
||||||
|
return DropdownMenuItem<int>(
|
||||||
|
enabled: !contains,
|
||||||
|
value: ele.key,
|
||||||
|
child: Text(
|
||||||
|
ele.value.$1.tr(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
).opacity(contains ? 0.75 : 1),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
onChanged: (val) => setState(() {
|
||||||
|
_factorType = val;
|
||||||
|
}),
|
||||||
|
buttonStyleData: ButtonStyleData(
|
||||||
|
height: 50,
|
||||||
|
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => Navigator.of(context).pop(),
|
||||||
|
child: Text('dialogCancel').tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _submit(),
|
||||||
|
child: Text('dialogConfirm').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FactorTotpFactorDialog extends StatelessWidget {
|
||||||
|
final SnAuthFactor factor;
|
||||||
|
|
||||||
|
const _FactorTotpFactorDialog({required this.factor});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'totpPostSetup',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
).tr().width(280),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'totpPostSetupDescription',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
).tr().width(280),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
QrImageView(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
data: factor.config!['url'],
|
||||||
|
errorCorrectionLevel: QrErrorCorrectLevel.H,
|
||||||
|
version: QrVersions.auto,
|
||||||
|
size: 160,
|
||||||
|
gapless: true,
|
||||||
|
eyeStyle: QrEyeStyle(
|
||||||
|
eyeShape: QrEyeShape.circle,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
dataModuleStyle: QrDataModuleStyle(
|
||||||
|
dataModuleShape: QrDataModuleShape.square,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'totpNeverShare',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
).tr().bold().width(280),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -178,6 +178,10 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: PageBackButton(),
|
||||||
|
title: Text('screenAccountPublisherEdit').tr(),
|
||||||
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -45,6 +45,33 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _deletePublisher(SnPublisher publisher) async {
|
||||||
|
final confirm = await context.showConfirmDialog(
|
||||||
|
'publisherDelete'.tr(args: ['#${publisher.name}']),
|
||||||
|
'publisherDeleteDescription'.tr(),
|
||||||
|
);
|
||||||
|
if (!confirm) return;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await context
|
||||||
|
.read<SnNetworkProvider>()
|
||||||
|
.client
|
||||||
|
.delete('/cgi/co/publishers/${publisher.name}');
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('publisherDeleted'.tr(args: ['#${publisher.name}']));
|
||||||
|
_publishers.remove(publisher);
|
||||||
|
_fetchPublishers();
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -118,6 +145,18 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete),
|
||||||
|
const Gap(16),
|
||||||
|
Text('delete').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_deletePublisher(publisher);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import 'package:dismissible_page/dismissible_page.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
@@ -27,9 +30,23 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
|||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
int? _totalCount;
|
int? _totalCount;
|
||||||
|
|
||||||
|
SnAttachmentBilling? _billing;
|
||||||
|
|
||||||
final List<SnAttachment> _attachments = List.empty(growable: true);
|
final List<SnAttachment> _attachments = List.empty(growable: true);
|
||||||
final List<String> _heroTags = List.empty(growable: true);
|
final List<String> _heroTags = List.empty(growable: true);
|
||||||
|
|
||||||
|
Future<void> _fetchBillingStatus() async {
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/uc/billing');
|
||||||
|
final out = SnAttachmentBilling.fromJson(resp.data);
|
||||||
|
setState(() => _billing = out);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _fetchAttachments() async {
|
Future<void> _fetchAttachments() async {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
@@ -62,6 +79,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_fetchBillingStatus();
|
||||||
_fetchAttachments();
|
_fetchAttachments();
|
||||||
_scrollController.addListener(() {
|
_scrollController.addListener(() {
|
||||||
if (_scrollController.position.atEdge) {
|
if (_scrollController.position.atEdge) {
|
||||||
@@ -91,6 +109,48 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
|||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenAlbum').tr(),
|
title: Text('screenAlbum').tr(),
|
||||||
),
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Card(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: _billing?.includedRatio ?? 0,
|
||||||
|
strokeWidth: 8,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
).padding(all: 12),
|
||||||
|
const Gap(24),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('attachmentBillingUploaded').tr().bold(),
|
||||||
|
Text(
|
||||||
|
(_billing?.currentBytes ?? 0).formatBytes(decimals: 4),
|
||||||
|
style: GoogleFonts.robotoMono(),
|
||||||
|
),
|
||||||
|
Text('attachmentBillingDiscount').tr().bold(),
|
||||||
|
Text(
|
||||||
|
'${(_billing?.discountFileSize ?? 0).formatBytes(decimals: 2)} · ${((_billing?.includedRatio ?? 0) * 100).toStringAsFixed(2)}%',
|
||||||
|
style: GoogleFonts.robotoMono(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: 'attachmentBillingHint'.tr(),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Symbols.info),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24, vertical: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
SliverMasonryGrid.extent(
|
SliverMasonryGrid.extent(
|
||||||
childCount: _attachments.length,
|
childCount: _attachments.length,
|
||||||
maxCrossAxisExtent: 320,
|
maxCrossAxisExtent: 320,
|
||||||
@@ -123,8 +183,10 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
|||||||
),
|
),
|
||||||
if (_isBusy)
|
if (_isBusy)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child:
|
child: Padding(
|
||||||
const CircularProgressIndicator().padding(all: 24).center(),
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
).center(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/screens/account/factor_settings.dart';
|
||||||
import 'package:surface/types/auth.dart';
|
import 'package:surface/types/auth.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
@@ -14,11 +15,6 @@ import 'package:url_launcher/url_launcher_string.dart';
|
|||||||
|
|
||||||
import '../../providers/websocket.dart';
|
import '../../providers/websocket.dart';
|
||||||
|
|
||||||
final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
|
|
||||||
0: ('authFactorPassword'.tr(), Symbols.password, false),
|
|
||||||
1: ('authFactorEmail'.tr(), Symbols.email, true),
|
|
||||||
};
|
|
||||||
|
|
||||||
class LoginScreen extends StatefulWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({super.key});
|
const LoginScreen({super.key});
|
||||||
|
|
||||||
@@ -212,7 +208,9 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
|
|||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
autofillHints: [
|
autofillHints: [
|
||||||
(_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode
|
widget.factor!.type == 0
|
||||||
|
? AutofillHints.password
|
||||||
|
: AutofillHints.oneTimeCode
|
||||||
],
|
],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
@@ -267,7 +265,8 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
|||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
int? _factorPicked;
|
int? _factorPicked;
|
||||||
|
|
||||||
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
Color get _unFocusColor =>
|
||||||
|
Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
||||||
|
|
||||||
void _performGetFactorCode() async {
|
void _performGetFactorCode() async {
|
||||||
if (_factorPicked == null) return;
|
if (_factorPicked == null) return;
|
||||||
@@ -328,11 +327,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
secondary: Icon(
|
secondary: Icon(
|
||||||
_factorLabelMap[x.type]?.$2 ?? Symbols.question_mark,
|
kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
|
kFactorTypes[x.type]?.$1 ?? 'unknown',
|
||||||
),
|
).tr(),
|
||||||
enabled: !widget.ticket!.factorTrail.contains(x.id),
|
enabled: !widget.ticket!.factorTrail.contains(x.id),
|
||||||
value: _factorPicked == x.id,
|
value: _factorPicked == x.id,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -408,11 +407,14 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final lookupResp = await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
final lookupResp =
|
||||||
|
await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
||||||
await sn.client.post('/cgi/id/users/me/password-reset', data: {
|
await sn.client.post('/cgi/id/users/me/password-reset', data: {
|
||||||
'user_id': lookupResp.data['id'],
|
'user_id': lookupResp.data['id'],
|
||||||
});
|
});
|
||||||
if (mounted) context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
if (mounted) {
|
||||||
|
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (mounted) context.showErrorDialog(err);
|
if (mounted) context.showErrorDialog(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -437,7 +439,8 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
|||||||
widget.onTicket(result.ticket);
|
widget.onTicket(result.ticket);
|
||||||
|
|
||||||
// Pull factors
|
// Pull factors
|
||||||
final factorResp = await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
final factorResp =
|
||||||
|
await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
||||||
'ticketId': result.ticket!.id.toString(),
|
'ticketId': result.ticket!.id.toString(),
|
||||||
});
|
});
|
||||||
widget.onFactor(
|
widget.onFactor(
|
||||||
@@ -531,7 +534,10 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
|||||||
'termAcceptNextWithAgree'.tr(),
|
'termAcceptNextWithAgree'.tr(),
|
||||||
textAlign: TextAlign.end,
|
textAlign: TextAlign.end,
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha((255 * 0.75).round()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Material(
|
Material(
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
'nick': nickname,
|
'nick': nickname,
|
||||||
'email': email,
|
'email': email,
|
||||||
'password': password,
|
'password': password,
|
||||||
|
'language': EasyLocalization.of(context)!.currentLocale.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|||||||
@@ -5,21 +5,23 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/screens/chat/room.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/account/account_select.dart';
|
import 'package:surface/widgets/account/account_select.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_background.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/unauthorized_hint.dart';
|
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import '../providers/sn_network.dart';
|
|
||||||
import '../providers/userinfo.dart';
|
|
||||||
|
|
||||||
class ChatScreen extends StatefulWidget {
|
class ChatScreen extends StatefulWidget {
|
||||||
const ChatScreen({super.key});
|
const ChatScreen({super.key});
|
||||||
|
|
||||||
@@ -34,8 +36,18 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
List<SnChannel>? _channels;
|
List<SnChannel>? _channels;
|
||||||
Map<int, SnChatMessage>? _lastMessages;
|
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>();
|
final ua = context.read<UserProvider>();
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
@@ -43,12 +55,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final chan = context.read<ChatChannelProvider>();
|
final chan = context.read<ChatChannelProvider>();
|
||||||
chan.fetchChannels().listen((channels) async {
|
chan.fetchChannels(noRemote: noRemote).listen((channels) async {
|
||||||
final lastMessages = await chan.getLastMessages(channels);
|
final lastMessages = await chan.getLastMessages(channels);
|
||||||
_lastMessages = {for (final val in lastMessages) val.channelId: val};
|
_lastMessages = {for (final val in lastMessages) val.channelId: val};
|
||||||
channels.sort((a, b) {
|
channels.sort((a, b) {
|
||||||
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) {
|
if (_lastMessages!.containsKey(a.id) &&
|
||||||
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt);
|
_lastMessages!.containsKey(b.id)) {
|
||||||
|
return _lastMessages![b.id]!
|
||||||
|
.createdAt
|
||||||
|
.compareTo(_lastMessages![a.id]!.createdAt);
|
||||||
}
|
}
|
||||||
if (_lastMessages!.containsKey(a.id)) return -1;
|
if (_lastMessages!.containsKey(a.id)) return -1;
|
||||||
if (_lastMessages!.containsKey(b.id)) return 1;
|
if (_lastMessages!.containsKey(b.id)) return 1;
|
||||||
@@ -86,7 +101,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
void _newDirectMessage() async {
|
void _newDirectMessage() async {
|
||||||
final user = await showModalBottomSheet(
|
final user = await showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AccountSelect(title: 'channelNewDirectMessage'.tr()),
|
builder: (context) =>
|
||||||
|
AccountSelect(title: 'channelNewDirectMessage'.tr()),
|
||||||
);
|
);
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -98,7 +114,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
await sn.client.post('/cgi/im/channels/global/dm', data: {
|
await sn.client.post('/cgi/im/channels/global/dm', data: {
|
||||||
'alias': uuid.v4().replaceAll('-', '').substring(0, 12),
|
'alias': uuid.v4().replaceAll('-', '').substring(0, 12),
|
||||||
'name': 'DM',
|
'name': 'DM',
|
||||||
'description': 'A direct message channel between @${ua.user?.name} and @${user.name}',
|
'description':
|
||||||
|
'A direct message channel between @${ua.user?.name} and @${user.name}',
|
||||||
'related_user': user.id,
|
'related_user': user.id,
|
||||||
});
|
});
|
||||||
_fabKey.currentState!.toggle();
|
_fabKey.currentState!.toggle();
|
||||||
@@ -109,10 +126,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SnChannel? _focusChannel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_refreshChannels();
|
_refreshChannels();
|
||||||
|
_fetchWhatsNew();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenChat').tr(),
|
title: Text('screenChat').tr(),
|
||||||
@@ -144,20 +167,27 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
type: ExpandableFabType.up,
|
type: ExpandableFabType.up,
|
||||||
childrenAnimation: ExpandableFabAnimation.none,
|
childrenAnimation: ExpandableFabAnimation.none,
|
||||||
overlayStyle: ExpandableFabOverlayStyle(
|
overlayStyle: ExpandableFabOverlayStyle(
|
||||||
color: Theme.of(context).colorScheme.surface.withAlpha((255 * 0.5).round()),
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surface
|
||||||
|
.withAlpha((255 * 0.5).round()),
|
||||||
),
|
),
|
||||||
openButtonBuilder: RotateFloatingActionButtonBuilder(
|
openButtonBuilder: RotateFloatingActionButtonBuilder(
|
||||||
child: const Icon(Symbols.add, size: 28),
|
child: const Icon(Symbols.add, size: 28),
|
||||||
fabSize: ExpandableFabSize.regular,
|
fabSize: ExpandableFabSize.regular,
|
||||||
foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
foregroundColor:
|
||||||
backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
),
|
),
|
||||||
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
||||||
child: const Icon(Symbols.close, size: 28),
|
child: const Icon(Symbols.close, size: 28),
|
||||||
fabSize: ExpandableFabSize.regular,
|
fabSize: ExpandableFabSize.regular,
|
||||||
foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
foregroundColor:
|
||||||
backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
@@ -200,7 +230,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
removeTop: true,
|
removeTop: true,
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: () => Future.sync(() => _refreshChannels()),
|
onRefresh: () => Future.wait([
|
||||||
|
Future.sync(() => _refreshChannels()),
|
||||||
|
_fetchWhatsNew(),
|
||||||
|
]),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: _channels?.length ?? 0,
|
itemCount: _channels?.length ?? 0,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
@@ -208,13 +241,29 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final lastMessage = _lastMessages?[channel.id];
|
final lastMessage = _lastMessages?[channel.id];
|
||||||
|
|
||||||
if (channel.type == 1) {
|
if (channel.type == 1) {
|
||||||
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
final otherMember =
|
||||||
(ele) => ele?.accountId != ua.user?.id,
|
channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||||
orElse: () => null,
|
(ele) => ele?.accountId != ua.user?.id,
|
||||||
);
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
title: 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
|
subtitle: lastMessage != null
|
||||||
? Text(
|
? Text(
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
@@ -222,17 +271,22 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
'channelDirectMessageDescription'.tr(args: [
|
channel.description,
|
||||||
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
|
||||||
]),
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
content: ud
|
||||||
|
.getAccountFromCache(otherMember?.accountId)
|
||||||
|
?.avatar,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (doExpand) {
|
||||||
|
setState(() => _focusChannel = channel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'chatRoom',
|
'chatRoom',
|
||||||
pathParameters: {
|
pathParameters: {
|
||||||
@@ -240,14 +294,23 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
'alias': channel.alias,
|
'alias': channel.alias,
|
||||||
},
|
},
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (mounted) _refreshChannels();
|
if (mounted) _refreshChannels(noRemote: true);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListTile(
|
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
|
subtitle: lastMessage != null
|
||||||
? Text(
|
? Text(
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
@@ -259,12 +322,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: null,
|
content: null,
|
||||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (doExpand) {
|
||||||
|
setState(() => _focusChannel = channel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'chatRoom',
|
'chatRoom',
|
||||||
pathParameters: {
|
pathParameters: {
|
||||||
@@ -272,7 +340,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
'alias': channel.alias,
|
'alias': channel.alias,
|
||||||
},
|
},
|
||||||
).then((value) {
|
).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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,12 +155,16 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
text: TextSpan(children: [
|
text: TextSpan(children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'call'.tr(),
|
text: 'call'.tr(),
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: call.lastDuration.toString(),
|
text: call.lastDuration.toString(),
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import 'package:surface/providers/channel.dart';
|
|||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/types/account.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/account/account_select.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
@@ -20,6 +22,7 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
|||||||
class ChannelDetailScreen extends StatefulWidget {
|
class ChannelDetailScreen extends StatefulWidget {
|
||||||
final String scope;
|
final String scope;
|
||||||
final String alias;
|
final String alias;
|
||||||
|
|
||||||
const ChannelDetailScreen({
|
const ChannelDetailScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.scope,
|
required this.scope,
|
||||||
@@ -55,8 +58,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client
|
final resp = await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/members/me');
|
||||||
.get('/cgi/im/channels/${_channel!.keyPath}/members/me');
|
|
||||||
_profile = SnChannelMember.fromJson(resp.data);
|
_profile = SnChannelMember.fromJson(resp.data);
|
||||||
_notifyLevel = _profile!.notify;
|
_notifyLevel = _profile!.notify;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -102,7 +104,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.delete(
|
await sn.client.delete(
|
||||||
'/cgi/im/channels/${_channel!.realm?.alias ?? 'global'}/${_channel!.id}/members/me',
|
'/cgi/im/channels/${_channel!.realm?.alias ?? 'global'}/${_channel!.alias}/me',
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.pop(context, false);
|
Navigator.pop(context, false);
|
||||||
@@ -143,6 +145,25 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _addMember(SnAccount related) async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.post(
|
||||||
|
'/cgi/im/channels/${_channel!.keyPath}/members',
|
||||||
|
data: {'related': related.name},
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('channelMemberAdded'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _showChannelProfileDetail() {
|
void _showChannelProfileDetail() {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -166,13 +187,16 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showMemberAdd() {
|
void _showMemberAdd() async {
|
||||||
showModalBottomSheet(
|
final user = await showModalBottomSheet<SnAccount?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _NewChannelMemberWidget(
|
builder: (context) => AccountSelect(
|
||||||
channel: _channel!,
|
title: 'channelMemberAdd'.tr(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (user == null) return;
|
||||||
|
_addMember(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -221,11 +245,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('channelDetailPersonalRegion')
|
Text('channelDetailPersonalRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||||
.bold()
|
|
||||||
.fontSize(17)
|
|
||||||
.tr()
|
|
||||||
.padding(horizontal: 20, bottom: 4),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.notifications),
|
leading: const Icon(Symbols.notifications),
|
||||||
trailing: DropdownButtonHideUnderline(
|
trailing: DropdownButtonHideUnderline(
|
||||||
@@ -264,8 +284,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content:
|
content: ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
||||||
ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
|
||||||
radius: 18,
|
radius: 18,
|
||||||
),
|
),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
@@ -284,8 +303,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
title: Text('channelActionLeave').tr(),
|
title: Text('channelActionLeave').tr(),
|
||||||
subtitle: Text('channelActionLeaveDescription').tr(),
|
subtitle: Text('channelActionLeaveDescription').tr(),
|
||||||
contentPadding:
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
onTap: _leaveChannel,
|
onTap: _leaveChannel,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -293,11 +311,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('channelDetailMemberRegion')
|
Text('channelDetailMemberRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||||
.bold()
|
|
||||||
.fontSize(17)
|
|
||||||
.tr()
|
|
||||||
.padding(horizontal: 20, bottom: 4),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.group),
|
leading: const Icon(Symbols.group),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
@@ -319,11 +333,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('channelDetailAdminRegion')
|
Text('channelDetailAdminRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||||
.bold()
|
|
||||||
.fontSize(17)
|
|
||||||
.tr()
|
|
||||||
.padding(horizontal: 20, bottom: 4),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.edit),
|
leading: const Icon(Symbols.edit),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
@@ -362,18 +372,17 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
class _ChannelProfileDetailDialog extends StatefulWidget {
|
class _ChannelProfileDetailDialog extends StatefulWidget {
|
||||||
final SnChannel channel;
|
final SnChannel channel;
|
||||||
final SnChannelMember current;
|
final SnChannelMember current;
|
||||||
|
|
||||||
const _ChannelProfileDetailDialog({
|
const _ChannelProfileDetailDialog({
|
||||||
required this.channel,
|
required this.channel,
|
||||||
required this.current,
|
required this.current,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_ChannelProfileDetailDialog> createState() =>
|
State<_ChannelProfileDetailDialog> createState() => _ChannelProfileDetailDialogState();
|
||||||
_ChannelProfileDetailDialogState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelProfileDetailDialogState
|
class _ChannelProfileDetailDialogState extends State<_ChannelProfileDetailDialog> {
|
||||||
extends State<_ChannelProfileDetailDialog> {
|
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
|
|
||||||
final TextEditingController _nickController = TextEditingController();
|
final TextEditingController _nickController = TextEditingController();
|
||||||
@@ -444,11 +453,11 @@ class _ChannelProfileDetailDialogState
|
|||||||
|
|
||||||
class _ChannelMemberListWidget extends StatefulWidget {
|
class _ChannelMemberListWidget extends StatefulWidget {
|
||||||
final SnChannel channel;
|
final SnChannel channel;
|
||||||
|
|
||||||
const _ChannelMemberListWidget({required this.channel});
|
const _ChannelMemberListWidget({required this.channel});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_ChannelMemberListWidget> createState() =>
|
State<_ChannelMemberListWidget> createState() => _ChannelMemberListWidgetState();
|
||||||
_ChannelMemberListWidgetState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||||
@@ -463,12 +472,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
try {
|
try {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get(
|
final resp = await sn.client.get('/cgi/im/channels/${widget.channel.keyPath}/members', queryParameters: {
|
||||||
'/cgi/im/channels/${widget.channel.keyPath}/members',
|
'take': 10,
|
||||||
queryParameters: {
|
'offset': _members.length,
|
||||||
'take': 10,
|
});
|
||||||
'offset': 0,
|
|
||||||
});
|
|
||||||
final out = List<SnChannelMember>.from(
|
final out = List<SnChannelMember>.from(
|
||||||
resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
|
resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
@@ -526,9 +533,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.group, size: 24),
|
const Icon(Symbols.group, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('channelMemberManage')
|
Text('channelMemberManage').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
.tr()
|
|
||||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -539,8 +544,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
},
|
},
|
||||||
child: InfiniteList(
|
child: InfiniteList(
|
||||||
itemCount: _members.length,
|
itemCount: _members.length,
|
||||||
hasReachedMax:
|
hasReachedMax: _totalCount != null && _members.length >= _totalCount!,
|
||||||
_totalCount != null && _members.length >= _totalCount!,
|
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
onFetchData: _fetchMembers,
|
onFetchData: _fetchMembers,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -551,8 +555,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
ud.getAccountFromCache(member.accountId)?.name ??
|
ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
||||||
'unknown'.tr(),
|
|
||||||
),
|
),
|
||||||
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
@@ -562,8 +565,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed:
|
onPressed: _isUpdating ? null : () => _deleteMember(member),
|
||||||
_isUpdating ? null : () => _deleteMember(member),
|
|
||||||
icon: const Icon(Symbols.person_remove),
|
icon: const Icon(Symbols.person_remove),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -578,83 +580,3 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NewChannelMemberWidget extends StatefulWidget {
|
|
||||||
final SnChannel channel;
|
|
||||||
const _NewChannelMemberWidget({required this.channel});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_NewChannelMemberWidget> createState() =>
|
|
||||||
_NewChannelMemberWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NewChannelMemberWidgetState extends State<_NewChannelMemberWidget> {
|
|
||||||
bool _isBusy = false;
|
|
||||||
|
|
||||||
final TextEditingController _relatedController = TextEditingController();
|
|
||||||
|
|
||||||
Future<void> _performAction() async {
|
|
||||||
if (_relatedController.text.isEmpty) return;
|
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client.post(
|
|
||||||
'/cgi/im/channels/${widget.channel.keyPath}/members',
|
|
||||||
data: {
|
|
||||||
'related': _relatedController.text,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!mounted) return;
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
context.showSnackbar('channelMemberAdded'.tr());
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isBusy = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_relatedController.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return StyledWidget(Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'channelMemberAdd',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
).tr(),
|
|
||||||
const Gap(12),
|
|
||||||
TextField(
|
|
||||||
controller: _relatedController,
|
|
||||||
readOnly: _isBusy,
|
|
||||||
autocorrect: false,
|
|
||||||
autofocus: true,
|
|
||||||
textCapitalization: TextCapitalization.none,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'fieldMemberRelatedName'.tr(),
|
|
||||||
suffix: SizedBox(
|
|
||||||
height: 24,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: _isBusy ? null : () => _performAction(),
|
|
||||||
icon: Icon(Symbols.send),
|
|
||||||
visualDensity:
|
|
||||||
const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)).padding(all: 24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@@ -17,6 +18,7 @@ import 'package:uuid/uuid.dart';
|
|||||||
|
|
||||||
class ChatManageScreen extends StatefulWidget {
|
class ChatManageScreen extends StatefulWidget {
|
||||||
final String? editingChannelAlias;
|
final String? editingChannelAlias;
|
||||||
|
|
||||||
const ChatManageScreen({super.key, this.editingChannelAlias});
|
const ChatManageScreen({super.key, this.editingChannelAlias});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -33,6 +35,11 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
List<SnRealm>? _realms;
|
List<SnRealm>? _realms;
|
||||||
SnRealm? _belongToRealm;
|
SnRealm? _belongToRealm;
|
||||||
|
|
||||||
|
SnChannel? _editingChannel;
|
||||||
|
|
||||||
|
bool _isPublic = false;
|
||||||
|
bool _isCommunity = false;
|
||||||
|
|
||||||
Future<void> _fetchRealms() async {
|
Future<void> _fetchRealms() async {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
try {
|
try {
|
||||||
@@ -41,6 +48,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
_realms = List<SnRealm>.from(
|
_realms = List<SnRealm>.from(
|
||||||
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
|
if (_editingChannel != null) {
|
||||||
|
_belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (mounted) context.showErrorDialog(err);
|
if (mounted) context.showErrorDialog(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -48,8 +58,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SnChannel? _editingChannel;
|
|
||||||
|
|
||||||
Future<void> _fetchChannel() async {
|
Future<void> _fetchChannel() async {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
@@ -62,6 +70,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
_aliasController.text = _editingChannel!.alias;
|
_aliasController.text = _editingChannel!.alias;
|
||||||
_nameController.text = _editingChannel!.name;
|
_nameController.text = _editingChannel!.name;
|
||||||
_descriptionController.text = _editingChannel!.description;
|
_descriptionController.text = _editingChannel!.description;
|
||||||
|
_isPublic = _editingChannel!.isPublic;
|
||||||
|
_isCommunity = _editingChannel!.isCommunity;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -83,6 +93,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
: uuid.v4().replaceAll('-', '').substring(0, 12),
|
: uuid.v4().replaceAll('-', '').substring(0, 12),
|
||||||
'name': _nameController.text,
|
'name': _nameController.text,
|
||||||
'description': _descriptionController.text,
|
'description': _descriptionController.text,
|
||||||
|
'is_public': _isPublic,
|
||||||
|
'is_community': _isCommunity,
|
||||||
|
if (_editingChannel != null && _belongToRealm == null)
|
||||||
|
'new_belongs_realm': 'global'
|
||||||
|
else if (_editingChannel != null && _belongToRealm?.id != _editingChannel?.realm?.id)
|
||||||
|
'new_belongs_realm': _belongToRealm!.alias,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -124,9 +140,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: widget.editingChannelAlias != null
|
title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(),
|
||||||
? Text('screenChatManage').tr()
|
|
||||||
: Text('screenChatNew').tr(),
|
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -138,8 +152,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
leadingPadding: const EdgeInsets.only(left: 10, right: 20),
|
leadingPadding: const EdgeInsets.only(left: 10, right: 20),
|
||||||
dividerColor: Colors.transparent,
|
dividerColor: Colors.transparent,
|
||||||
content: Text(
|
content: Text(
|
||||||
'channelEditingNotice'
|
'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']),
|
||||||
.tr(args: ['#${_editingChannel!.alias}']),
|
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -179,15 +192,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(item.name).textStyle(Theme.of(context)
|
Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!),
|
||||||
.textTheme
|
|
||||||
.bodyMedium!),
|
|
||||||
Text(
|
Text(
|
||||||
item.description,
|
item.description,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
).textStyle(
|
).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
Theme.of(context).textTheme.bodySmall!),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -203,8 +213,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 16,
|
radius: 16,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
foregroundColor:
|
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||||
Theme.of(context).colorScheme.onSurface,
|
|
||||||
child: const Icon(Symbols.clear),
|
child: const Icon(Symbols.clear),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
@@ -213,9 +222,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('fieldChatBelongToRealmUnset')
|
Text('fieldChatBelongToRealmUnset').tr().textStyle(
|
||||||
.tr()
|
|
||||||
.textStyle(
|
|
||||||
Theme.of(context).textTheme.bodyMedium!,
|
Theme.of(context).textTheme.bodyMedium!,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -231,10 +238,10 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
},
|
},
|
||||||
buttonStyleData: const ButtonStyleData(
|
buttonStyleData: const ButtonStyleData(
|
||||||
padding: EdgeInsets.only(right: 16),
|
padding: EdgeInsets.only(right: 16),
|
||||||
height: 60,
|
height: 48,
|
||||||
),
|
),
|
||||||
menuItemStyleData: const MenuItemStyleData(
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
height: 60,
|
height: 48,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -250,8 +257,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
helperText: 'fieldChatAliasHint'.tr(),
|
helperText: 'fieldChatAliasHint'.tr(),
|
||||||
helperMaxLines: 2,
|
helperMaxLines: 2,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) =>
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
TextField(
|
TextField(
|
||||||
@@ -260,8 +266,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldChatName'.tr(),
|
labelText: 'fieldChatName'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) =>
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
TextField(
|
TextField(
|
||||||
@@ -272,8 +277,24 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldChatDescription'.tr(),
|
labelText: 'fieldChatDescription'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) =>
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
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),
|
const Gap(12),
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@@ -9,9 +10,12 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/chat_message_controller.dart';
|
import 'package:surface/controllers/chat_message_controller.dart';
|
||||||
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/chat_call.dart';
|
import 'package:surface/providers/chat_call.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/user_directory.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
||||||
@@ -23,14 +27,20 @@ import 'package:surface/widgets/loading_indicator.dart';
|
|||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
import '../../providers/user_directory.dart';
|
class ChatRoomScreenExtra {
|
||||||
import '../../providers/userinfo.dart';
|
final String? initialText;
|
||||||
|
final List<PostWriteMedia>? initialAttachments;
|
||||||
|
|
||||||
|
ChatRoomScreenExtra({this.initialText, this.initialAttachments});
|
||||||
|
}
|
||||||
|
|
||||||
class ChatRoomScreen extends StatefulWidget {
|
class ChatRoomScreen extends StatefulWidget {
|
||||||
final String scope;
|
final String scope;
|
||||||
final String alias;
|
final String alias;
|
||||||
|
final ChatRoomScreenExtra? extra;
|
||||||
|
|
||||||
const ChatRoomScreen({super.key, required this.scope, required this.alias});
|
const ChatRoomScreen(
|
||||||
|
{super.key, required this.scope, required this.alias, this.extra});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatRoomScreen> createState() => _ChatRoomScreenState();
|
State<ChatRoomScreen> createState() => _ChatRoomScreenState();
|
||||||
@@ -49,6 +59,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
|
|
||||||
StreamSubscription? _wsSubscription;
|
StreamSubscription? _wsSubscription;
|
||||||
|
|
||||||
|
// TODO fetch user identity and ask them to join the channel or not
|
||||||
Future<void> _fetchChannel() async {
|
Future<void> _fetchChannel() async {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
@@ -177,12 +188,29 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
_messageController = ChatMessageController(context);
|
_messageController = ChatMessageController(context);
|
||||||
_fetchChannel().then((_) async {
|
_fetchChannel().then((_) async {
|
||||||
await _messageController.initialize(_channel!);
|
await _messageController.initialize(_channel!);
|
||||||
await _messageController.checkUpdate();
|
|
||||||
await _fetchOngoingCall();
|
if (widget.extra != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
log('[ChatInput] Setting initial text and attachments...');
|
||||||
|
if (widget.extra!.initialText != null) {
|
||||||
|
_inputGlobalKey.currentState
|
||||||
|
?.setInitialText(widget.extra!.initialText!);
|
||||||
|
}
|
||||||
|
if (widget.extra!.initialAttachments != null) {
|
||||||
|
_inputGlobalKey.currentState
|
||||||
|
?.setInitialAttachments(widget.extra!.initialAttachments!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
_messageController.checkUpdate(),
|
||||||
|
_fetchOngoingCall(),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
final ws = context.read<WebSocketProvider>();
|
final ws = context.read<WebSocketProvider>();
|
||||||
_wsSubscription = ws.stream.stream.listen((event) {
|
_wsSubscription = ws.pk.stream.listen((event) {
|
||||||
switch (event.method) {
|
switch (event.method) {
|
||||||
case 'calls.new':
|
case 'calls.new':
|
||||||
final payload = SnChatCall.fromJson(event.payload!);
|
final payload = SnChatCall.fromJson(event.payload!);
|
||||||
@@ -216,12 +244,15 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
_channel?.type == 1
|
_channel?.type == 1
|
||||||
? ud.getAccountFromCache(_otherMember?.accountId)?.nick ?? _channel!.name
|
? ud.getAccountFromCache(_otherMember?.accountId)?.nick ??
|
||||||
|
_channel!.name
|
||||||
: _channel?.name ?? 'loading'.tr(),
|
: _channel?.name ?? 'loading'.tr(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
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
|
onPressed: _isCalling
|
||||||
? null
|
? null
|
||||||
: _ongoingCall == null
|
: _ongoingCall == null
|
||||||
@@ -271,9 +302,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
).height(_ongoingCall != null ? 54 : 0, animate: true).animate(
|
||||||
.height(_ongoingCall != null ? 54 : 0, animate: true)
|
const Duration(milliseconds: 300),
|
||||||
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
|
Curves.fastLinearToSlowEaseIn),
|
||||||
if (_messageController.isPending)
|
if (_messageController.isPending)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: const CircularProgressIndicator().center(),
|
child: const CircularProgressIndicator().center(),
|
||||||
@@ -291,6 +322,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
},
|
},
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
final message = _messageController.messages[idx];
|
final message = _messageController.messages[idx];
|
||||||
|
_messageController.readEvent(message.id);
|
||||||
|
|
||||||
bool canMerge = false, canMergePrevious = false;
|
bool canMerge = false, canMergePrevious = false;
|
||||||
if (idx > 0) {
|
if (idx > 0) {
|
||||||
@@ -312,7 +344,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
data: message,
|
data: message,
|
||||||
isMerged: canMerge,
|
isMerged: canMerge,
|
||||||
hasMerged: canMergePrevious,
|
hasMerged: canMergePrevious,
|
||||||
isPending: _messageController.unconfirmedMessages.contains(message.uuid),
|
isPending: _messageController.unconfirmedMessages
|
||||||
|
.contains(message.uuid),
|
||||||
onReply: (value) {
|
onReply: (value) {
|
||||||
_inputGlobalKey.currentState?.setReply(value);
|
_inputGlobalKey.currentState?.setReply(value);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:animations/animations.dart';
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||||
@@ -9,8 +9,10 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/screens/post/post_detail.dart';
|
import 'package:surface/providers/sn_realm.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
|
import 'package:surface/types/realm.dart';
|
||||||
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
@@ -37,61 +39,54 @@ class ExploreScreen extends StatefulWidget {
|
|||||||
State<ExploreScreen> createState() => _ExploreScreenState();
|
State<ExploreScreen> createState() => _ExploreScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ExploreScreenState extends State<ExploreScreen> {
|
// You know what? I'm not going to make this a global variable.
|
||||||
|
// Cuz the global key make the selected category not update to child widget when the category is changed.
|
||||||
|
SnPostCategory? _selectedCategory;
|
||||||
|
|
||||||
|
class _ExploreScreenState extends State<ExploreScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final TabController _tabController =
|
||||||
|
TabController(length: 4, vsync: this);
|
||||||
|
|
||||||
final _fabKey = GlobalKey<ExpandableFabState>();
|
final _fabKey = GlobalKey<ExpandableFabState>();
|
||||||
|
final _listKeys = List.generate(4, (_) => GlobalKey<_PostListWidgetState>());
|
||||||
|
|
||||||
bool _isBusy = true;
|
|
||||||
|
|
||||||
final List<SnPost> _posts = List.empty(growable: true);
|
|
||||||
final List<SnPostCategory> _categories = List.empty(growable: true);
|
final List<SnPostCategory> _categories = List.empty(growable: true);
|
||||||
int? _postCount;
|
|
||||||
|
|
||||||
String? _selectedCategory;
|
|
||||||
|
|
||||||
Future<void> _fetchCategories() async {
|
Future<void> _fetchCategories() async {
|
||||||
_categories.clear();
|
_categories.clear();
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/co/categories?take=100');
|
final resp = await sn.client.get('/cgi/co/categories?take=100');
|
||||||
_categories.addAll(resp.data.map((e) => SnPostCategory.fromJson(e)).cast<SnPostCategory>() ?? []);
|
setState(() {
|
||||||
|
_categories.addAll(resp.data
|
||||||
|
.map((e) => SnPostCategory.fromJson(e))
|
||||||
|
.cast<SnPostCategory>() ??
|
||||||
|
[]);
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (mounted) context.showErrorDialog(err);
|
||||||
context.showErrorDialog(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchPosts() async {
|
void _clearFilter() {
|
||||||
if (_postCount != null && _posts.length >= _postCount!) return;
|
_selectedCategory = null;
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
|
|
||||||
final pt = context.read<SnPostContentProvider>();
|
|
||||||
final result = await pt.listPosts(
|
|
||||||
take: 10,
|
|
||||||
offset: _posts.length,
|
|
||||||
categories: _selectedCategory != null ? [_selectedCategory!] : null,
|
|
||||||
);
|
|
||||||
final out = result.$1;
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
_postCount = result.$2;
|
|
||||||
_posts.addAll(out);
|
|
||||||
|
|
||||||
if (mounted) setState(() => _isBusy = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _refreshPosts() {
|
|
||||||
_postCount = null;
|
|
||||||
_posts.clear();
|
|
||||||
return _fetchPosts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
|
||||||
_fetchPosts();
|
|
||||||
_fetchCategories();
|
_fetchCategories();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshPosts() async {
|
||||||
|
await _listKeys[_tabController.index].currentState?.refreshPosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -104,20 +99,27 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
type: ExpandableFabType.up,
|
type: ExpandableFabType.up,
|
||||||
childrenAnimation: ExpandableFabAnimation.none,
|
childrenAnimation: ExpandableFabAnimation.none,
|
||||||
overlayStyle: ExpandableFabOverlayStyle(
|
overlayStyle: ExpandableFabOverlayStyle(
|
||||||
color: Theme.of(context).colorScheme.surface.withAlpha((255 * 0.5).round()),
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surface
|
||||||
|
.withAlpha((255 * 0.5).round()),
|
||||||
),
|
),
|
||||||
openButtonBuilder: RotateFloatingActionButtonBuilder(
|
openButtonBuilder: RotateFloatingActionButtonBuilder(
|
||||||
child: const Icon(Symbols.add, size: 28),
|
child: const Icon(Symbols.add, size: 28),
|
||||||
fabSize: ExpandableFabSize.regular,
|
fabSize: ExpandableFabSize.regular,
|
||||||
foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
foregroundColor:
|
||||||
backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
),
|
),
|
||||||
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
||||||
child: const Icon(Symbols.close, size: 28),
|
child: const Icon(Symbols.close, size: 28),
|
||||||
fabSize: ExpandableFabSize.regular,
|
fabSize: ExpandableFabSize.regular,
|
||||||
foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
foregroundColor:
|
||||||
backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
@@ -133,7 +135,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
'mode': 'stories',
|
'mode': 'stories',
|
||||||
}).then((value) {
|
}).then((value) {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_refreshPosts();
|
refreshPosts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_fabKey.currentState!.toggle();
|
_fabKey.currentState!.toggle();
|
||||||
@@ -154,7 +156,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
'mode': 'articles',
|
'mode': 'articles',
|
||||||
}).then((value) {
|
}).then((value) {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_refreshPosts();
|
refreshPosts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_fabKey.currentState!.toggle();
|
_fabKey.currentState!.toggle();
|
||||||
@@ -163,95 +165,201 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text('writePostTypeQuestion').tr(),
|
||||||
|
const Gap(20),
|
||||||
|
FloatingActionButton(
|
||||||
|
heroTag: null,
|
||||||
|
tooltip: 'writePostTypeQuestion'.tr(),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||||
|
'mode': 'questions',
|
||||||
|
}).then((value) {
|
||||||
|
if (value == true) {
|
||||||
|
refreshPosts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_fabKey.currentState!.toggle();
|
||||||
|
},
|
||||||
|
child: const Icon(Symbols.question_answer),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text('writePostTypeVideo').tr(),
|
||||||
|
const Gap(20),
|
||||||
|
FloatingActionButton(
|
||||||
|
heroTag: null,
|
||||||
|
tooltip: 'writePostTypeVideo'.tr(),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||||
|
'mode': 'videos',
|
||||||
|
}).then((value) {
|
||||||
|
if (value == true) {
|
||||||
|
refreshPosts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_fabKey.currentState!.toggle();
|
||||||
|
},
|
||||||
|
child: const Icon(Symbols.video_call),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
body: NestedScrollView(
|
||||||
displacement: 40 + MediaQuery.of(context).padding.top,
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
onRefresh: () => _refreshPosts(),
|
return [
|
||||||
child: CustomScrollView(
|
SliverOverlapAbsorber(
|
||||||
slivers: [
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
SliverAppBar(
|
sliver: SliverAppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenExplore').tr(),
|
title: Text('screenExplore').tr(),
|
||||||
floating: true,
|
floating: true,
|
||||||
snap: true,
|
snap: true,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.search),
|
icon: const Icon(Symbols.category),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context).pushNamed('postSearch');
|
showModalBottomSheet(
|
||||||
},
|
context: context,
|
||||||
),
|
builder: (context) => _PostCategoryPickerPopup(
|
||||||
const Gap(8),
|
categories: _categories,
|
||||||
],
|
selected: _selectedCategory,
|
||||||
bottom: PreferredSize(
|
),
|
||||||
preferredSize: const Size.fromHeight(50),
|
).then((value) {
|
||||||
child: SizedBox(
|
if (value != null && context.mounted) {
|
||||||
height: 50,
|
_selectedCategory = value == false ? null : value;
|
||||||
child: SingleChildScrollView(
|
refreshPosts();
|
||||||
scrollDirection: Axis.horizontal,
|
}
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12),
|
});
|
||||||
child: Row(
|
},
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: _categories.map((ele) {
|
|
||||||
return StyledWidget(ChoiceChip(
|
|
||||||
avatar: Icon(kCategoryIcons[ele.alias] ?? Symbols.question_mark),
|
|
||||||
label: Text(
|
|
||||||
'postCategory${ele.alias.capitalize()}'.trExists()
|
|
||||||
? 'postCategory${ele.alias.capitalize()}'.tr()
|
|
||||||
: ele.name,
|
|
||||||
),
|
|
||||||
selected: _selectedCategory == ele.alias,
|
|
||||||
onSelected: (value) {
|
|
||||||
_selectedCategory = value ? ele.alias : null;
|
|
||||||
_refreshPosts();
|
|
||||||
},
|
|
||||||
)).padding(horizontal: 4);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.search),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pushNamed('postSearch');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
bottom: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.globe,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.appBarTheme
|
||||||
|
.foregroundColor),
|
||||||
|
const Gap(8),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'postChannelGlobal',
|
||||||
|
maxLines: 1,
|
||||||
|
).tr().textColor(
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.group,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.appBarTheme
|
||||||
|
.foregroundColor),
|
||||||
|
const Gap(8),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'postChannelFriends',
|
||||||
|
maxLines: 1,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr().textColor(
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.subscriptions,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.appBarTheme
|
||||||
|
.foregroundColor),
|
||||||
|
const Gap(8),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'postChannelFollowing',
|
||||||
|
maxLines: 1,
|
||||||
|
).tr().textColor(
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.workspaces,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.appBarTheme
|
||||||
|
.foregroundColor),
|
||||||
|
const Gap(8),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'postChannelRealm',
|
||||||
|
maxLines: 1,
|
||||||
|
).tr().textColor(
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SliverGap(12),
|
];
|
||||||
SliverInfiniteList(
|
},
|
||||||
itemCount: _posts.length,
|
body: TabBarView(
|
||||||
isLoading: _isBusy,
|
controller: _tabController,
|
||||||
centerLoading: true,
|
children: [
|
||||||
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
_PostListWidget(
|
||||||
onFetchData: _fetchPosts,
|
key: _listKeys[0],
|
||||||
itemBuilder: (context, idx) {
|
onClearFilter: _clearFilter,
|
||||||
return Center(
|
),
|
||||||
child: OpenContainer(
|
_PostListWidget(
|
||||||
closedBuilder: (_, __) => Container(
|
key: _listKeys[1],
|
||||||
constraints: const BoxConstraints(maxWidth: 640),
|
channel: 'friends',
|
||||||
child: PostItem(
|
onClearFilter: _clearFilter,
|
||||||
data: _posts[idx],
|
),
|
||||||
maxWidth: 640,
|
_PostListWidget(
|
||||||
onChanged: (data) {
|
key: _listKeys[2],
|
||||||
setState(() => _posts[idx] = data);
|
channel: 'following',
|
||||||
},
|
onClearFilter: _clearFilter,
|
||||||
onDeleted: () {
|
),
|
||||||
_refreshPosts();
|
_PostListWidget(
|
||||||
},
|
key: _listKeys[3],
|
||||||
),
|
withRealm: true,
|
||||||
),
|
onClearFilter: _clearFilter,
|
||||||
openBuilder: (_, close) => PostDetailScreen(
|
|
||||||
slug: _posts[idx].id.toString(),
|
|
||||||
preload: _posts[idx],
|
|
||||||
onBack: close,
|
|
||||||
),
|
|
||||||
openColor: Colors.transparent,
|
|
||||||
openElevation: 0,
|
|
||||||
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75),
|
|
||||||
transitionType: ContainerTransitionType.fade,
|
|
||||||
closedShape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (_, __) => const Gap(8),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -259,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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/relationship.dart';
|
import 'package:surface/providers/relationship.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/account/account_select.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||||
import '../providers/userinfo.dart';
|
|
||||||
import '../widgets/unauthorized_hint.dart';
|
|
||||||
|
|
||||||
const kFriendStatus = {
|
const kFriendStatus = {
|
||||||
0: 'friendStatusPending',
|
0: 'friendStatusPending',
|
||||||
@@ -168,6 +168,24 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _sendRequest(SnAccount user) async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.post('/cgi/id/users/me/relations', data: {
|
||||||
|
'related': user.name,
|
||||||
|
});
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('friendRequestSent'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -199,11 +217,16 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Icon(Symbols.add),
|
child: const Icon(Symbols.add),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
showModalBottomSheet(
|
final user = await showModalBottomSheet<SnAccount?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _NewFriendWidget(),
|
builder: (context) => AccountSelect(
|
||||||
|
title: 'friendNew'.tr(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (user == null) return;
|
||||||
|
_sendRequest(user);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
@@ -231,8 +254,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: _showBlocks,
|
onTap: _showBlocks,
|
||||||
),
|
),
|
||||||
if (_requests.isNotEmpty || _blocks.isNotEmpty)
|
if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1),
|
||||||
const Divider(height: 1),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: MediaQuery.removePadding(
|
child: MediaQuery.removePadding(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -264,16 +286,12 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: _isUpdating
|
onTap: _isUpdating ? null : () => _changeRelation(relation, 2),
|
||||||
? null
|
|
||||||
: () => _changeRelation(relation, 2),
|
|
||||||
child: Text('friendBlock').tr(),
|
child: Text('friendBlock').tr(),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: _isUpdating
|
onTap: _isUpdating ? null : () => _deleteRelation(relation),
|
||||||
? null
|
|
||||||
: () => _deleteRelation(relation),
|
|
||||||
child: Text('friendDeleteAction').tr(),
|
child: Text('friendDeleteAction').tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -293,83 +311,9 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NewFriendWidget extends StatefulWidget {
|
|
||||||
const _NewFriendWidget();
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_NewFriendWidget> createState() => _NewFriendWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NewFriendWidgetState extends State<_NewFriendWidget> {
|
|
||||||
bool _isBusy = false;
|
|
||||||
|
|
||||||
final TextEditingController _relatedController = TextEditingController();
|
|
||||||
|
|
||||||
Future<void> _sendRequest() async {
|
|
||||||
if (_relatedController.text.isEmpty) return;
|
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client.post('/cgi/id/users/me/relations', data: {
|
|
||||||
'related': _relatedController.text,
|
|
||||||
});
|
|
||||||
if (!mounted) return;
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
context.showSnackbar('friendRequestSent'.tr());
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isBusy = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_relatedController.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return StyledWidget(Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'friendNew',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
).tr(),
|
|
||||||
const Gap(12),
|
|
||||||
TextField(
|
|
||||||
controller: _relatedController,
|
|
||||||
readOnly: _isBusy,
|
|
||||||
autocorrect: false,
|
|
||||||
autofocus: true,
|
|
||||||
textCapitalization: TextCapitalization.none,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'fieldFriendRelatedName'.tr(),
|
|
||||||
suffix: SizedBox(
|
|
||||||
height: 24,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: _isBusy ? null : () => _sendRequest(),
|
|
||||||
icon: Icon(Symbols.send),
|
|
||||||
visualDensity:
|
|
||||||
const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)).padding(all: 24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FriendshipListWidget extends StatefulWidget {
|
class _FriendshipListWidget extends StatefulWidget {
|
||||||
final List<SnRelationship> relations;
|
final List<SnRelationship> relations;
|
||||||
|
|
||||||
const _FriendshipListWidget({required this.relations});
|
const _FriendshipListWidget({required this.relations});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -476,9 +420,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(kFriendStatus[relation.status] ?? 'unknown')
|
Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75),
|
||||||
.tr()
|
|
||||||
.opacity(0.75),
|
|
||||||
if (relation.status == 0)
|
if (relation.status == 0)
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
@@ -499,8 +441,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap:
|
onTap: _isBusy ? null : () => _changeRelation(relation, 1),
|
||||||
_isBusy ? null : () => _changeRelation(relation, 1),
|
|
||||||
child: Text('friendUnblock').tr(),
|
child: Text('friendUnblock').tr(),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class HomeScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
class _HomeScreenState extends State<HomeScreen> {
|
||||||
static const List<HomeScreenDashEntry> kCards = [
|
late final List<HomeScreenDashEntry> kCards = [
|
||||||
HomeScreenDashEntry(
|
HomeScreenDashEntry(
|
||||||
name: 'dashEntryRecommendation',
|
name: 'dashEntryRecommendation',
|
||||||
child: _HomeDashRecommendationPostWidget(),
|
child: _HomeDashRecommendationPostWidget(),
|
||||||
@@ -69,7 +69,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
HomeScreenDashEntry(
|
HomeScreenDashEntry(
|
||||||
name: 'dashEntryTodayNews',
|
name: 'dashEntryTodayNews',
|
||||||
child: _HomeDashTodayNews(),
|
child: _HomeDashTodayNews(),
|
||||||
cols: 2,
|
cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -131,6 +131,7 @@ class _HomeDashUpdateWidget extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Card(
|
child: Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(Symbols.update),
|
leading: Icon(Symbols.update),
|
||||||
title: Text('updateAvailable').tr(),
|
title: Text('updateAvailable').tr(),
|
||||||
@@ -180,6 +181,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
|||||||
return Column(
|
return Column(
|
||||||
children: days.map((ele) {
|
children: days.map((ele) {
|
||||||
return Card(
|
return Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
|
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
|
||||||
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
||||||
@@ -203,6 +205,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
|||||||
final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
|
final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
|
||||||
final diff = nextOne.$2.difference(DateTime.now());
|
final diff = nextOne.$2.difference(DateTime.now());
|
||||||
return Card(
|
return Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
||||||
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
|
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
|
||||||
@@ -270,6 +273,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -288,12 +292,13 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_article!.title,
|
_article!.title,
|
||||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18),
|
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18),
|
||||||
maxLines: 2,
|
maxLines: MediaQuery.of(context).size.width >= 640 ? 2 : 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -302,20 +307,18 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
Builder(
|
Builder(builder: (context) {
|
||||||
builder: (context) {
|
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||||
final date = _article!.publishedAt ?? _article!.createdAt;
|
return Row(
|
||||||
return Row(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
spacing: 2,
|
||||||
spacing: 2,
|
children: [
|
||||||
children: [
|
Text(DateFormat().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(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
],
|
||||||
],
|
).opacity(0.75);
|
||||||
).opacity(0.75);
|
}),
|
||||||
}
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -470,6 +473,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -515,6 +519,11 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
'+${_todayRecord!.resultExperience} EXP',
|
'+${_todayRecord!.resultExperience} EXP',
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
|
if (_todayRecord!.resultCoin >= 0)
|
||||||
|
Text(
|
||||||
|
'+${_todayRecord!.resultCoin} ${'walletCurrencyShort'.tr()}',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -590,6 +599,7 @@ class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -653,36 +663,59 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _currentPage = 0;
|
||||||
|
final PageController _pageController = PageController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_fetchRecommendationPosts();
|
_fetchRecommendationPosts();
|
||||||
|
_pageController.addListener(() {
|
||||||
|
setState(() {
|
||||||
|
_currentPage = _pageController.page?.round() ?? 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isBusy) {
|
if (_isBusy) {
|
||||||
return Card(
|
return Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
child: CircularProgressIndicator().center(),
|
child: CircularProgressIndicator().center(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.star),
|
Row(
|
||||||
const Gap(8),
|
children: [
|
||||||
Text(
|
const Icon(Symbols.star),
|
||||||
'postRecommendation',
|
const Gap(8),
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
Text(
|
||||||
).tr()
|
'postRecommendation',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
).tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text('${_currentPage + 1}/${_posts?.length ?? 0}', style: GoogleFonts.robotoMono())
|
||||||
],
|
],
|
||||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: PageView.builder(
|
child: PageView.builder(
|
||||||
|
controller: _pageController,
|
||||||
scrollBehavior: ScrollConfiguration.of(context).copyWith(dragDevices: {
|
scrollBehavior: ScrollConfiguration.of(context).copyWith(dragDevices: {
|
||||||
PointerDeviceKind.mouse,
|
PointerDeviceKind.mouse,
|
||||||
PointerDeviceKind.touch,
|
PointerDeviceKind.touch,
|
||||||
|
|||||||
@@ -175,54 +175,57 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
|||||||
),
|
),
|
||||||
if (_articleFragment != null && _isReadingFromReader)
|
if (_articleFragment != null && _isReadingFromReader)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: Container(
|
||||||
child: Column(
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: SingleChildScrollView(
|
||||||
spacing: 8,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
|
spacing: 8,
|
||||||
Builder(builder: (context) {
|
children: [
|
||||||
final htmlDescription = parse(_article!.description);
|
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
|
||||||
return Text(
|
Builder(builder: (context) {
|
||||||
htmlDescription.children.map((ele) => ele.text.trim()).join(),
|
final htmlDescription = parse(_article!.description);
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
return Text(
|
||||||
);
|
htmlDescription.children.map((ele) => ele.text.trim()).join(),
|
||||||
}),
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
Builder(builder: (context) {
|
);
|
||||||
final date = _article!.publishedAt ?? _article!.createdAt;
|
}),
|
||||||
return Row(
|
Builder(builder: (context) {
|
||||||
spacing: 2,
|
final date = _article!.publishedAt ?? _article!.createdAt;
|
||||||
children: [
|
return Row(
|
||||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
spacing: 2,
|
||||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
children: [
|
||||||
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(),
|
||||||
).opacity(0.75);
|
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
}),
|
],
|
||||||
Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75),
|
).opacity(0.75);
|
||||||
const Divider(),
|
}),
|
||||||
..._parseHtmlToWidgets(_articleFragment!.children),
|
Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
InkWell(
|
..._parseHtmlToWidgets(_articleFragment!.children),
|
||||||
child: Row(
|
const Divider(),
|
||||||
mainAxisSize: MainAxisSize.min,
|
InkWell(
|
||||||
children: [
|
child: Row(
|
||||||
Text(
|
mainAxisSize: MainAxisSize.min,
|
||||||
'Reference from original website',
|
children: [
|
||||||
style: TextStyle(decoration: TextDecoration.underline),
|
Text(
|
||||||
),
|
'Reference from original website',
|
||||||
const Gap(4),
|
style: TextStyle(decoration: TextDecoration.underline),
|
||||||
Icon(Icons.launch, size: 16),
|
),
|
||||||
],
|
const Gap(4),
|
||||||
).opacity(0.85),
|
Icon(Icons.launch, size: 16),
|
||||||
onTap: () {
|
],
|
||||||
launchUrlString(_article!.url);
|
).opacity(0.85),
|
||||||
},
|
onTap: () {
|
||||||
),
|
launchUrlString(_article!.url);
|
||||||
Gap(MediaQuery.of(context).padding.bottom),
|
},
|
||||||
],
|
),
|
||||||
).padding(horizontal: 12, vertical: 16),
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
),
|
],
|
||||||
|
).padding(horizontal: 12, vertical: 16),
|
||||||
|
),
|
||||||
|
).center(),
|
||||||
)
|
)
|
||||||
else if (_article != null)
|
else if (_article != null)
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -70,11 +70,16 @@ class _NewsScreenState extends State<NewsScreen> {
|
|||||||
sliver: SliverAppBar(
|
sliver: SliverAppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenNews').tr(),
|
title: Text('screenNews').tr(),
|
||||||
|
floating: true,
|
||||||
|
snap: true,
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
isScrollable: true,
|
isScrollable: true,
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(child: Text('newsAllSources'.tr())),
|
Tab(child: Text('newsAllSources'.tr()).textColor(Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
for (final source in _sources!) Tab(child: Text(source.label)),
|
for (final source in _sources!)
|
||||||
|
Tab(
|
||||||
|
child: Text(source.label).textColor(Theme.of(context).appBarTheme.foregroundColor),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -146,80 +151,87 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
|
|||||||
return MediaQuery.removePadding(
|
return MediaQuery.removePadding(
|
||||||
context: context,
|
context: context,
|
||||||
removeTop: true,
|
removeTop: true,
|
||||||
child: RefreshIndicator(
|
child: Center(
|
||||||
onRefresh: _fetchArticles,
|
child: Container(
|
||||||
child: InfiniteList(
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
isLoading: _isBusy,
|
child: RefreshIndicator(
|
||||||
itemCount: _articles.length,
|
onRefresh: _fetchArticles,
|
||||||
hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
|
child: InfiniteList(
|
||||||
onFetchData: () {
|
isLoading: _isBusy,
|
||||||
_fetchArticles();
|
itemCount: _articles.length,
|
||||||
},
|
hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
|
||||||
itemBuilder: (context, index) {
|
onFetchData: () {
|
||||||
final article = _articles[index];
|
_fetchArticles();
|
||||||
|
},
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final article = _articles[index];
|
||||||
|
|
||||||
final baseUri = Uri.parse(article.url);
|
final baseUri = Uri.parse(article.url);
|
||||||
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||||
|
|
||||||
final htmlDescription = parse(article.description);
|
final htmlDescription = parse(article.description);
|
||||||
final date = article.publishedAt ?? article.createdAt;
|
final date = article.publishedAt ?? article.createdAt;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
radius: 8,
|
radius: 8,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'newsDetail',
|
'newsDetail',
|
||||||
pathParameters: {'hash': article.hash},
|
pathParameters: {'hash': article.hash},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
|
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topRight: Radius.circular(8),
|
topRight: Radius.circular(8),
|
||||||
topLeft: Radius.circular(8),
|
topLeft: Radius.circular(8),
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: AutoResizeUniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
article.thumbnail.startsWith('http') ? article.thumbnail : '$baseUrl/${article.thumbnail}',
|
article.thumbnail.startsWith('http')
|
||||||
|
? article.thumbnail
|
||||||
|
: '$baseUrl/${article.thumbnail}',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const Gap(16),
|
||||||
),
|
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
|
||||||
const Gap(16),
|
const Gap(8),
|
||||||
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
|
Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
|
||||||
const Gap(8),
|
.textStyle(Theme.of(context).textTheme.bodyMedium!)
|
||||||
Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
|
.padding(horizontal: 16),
|
||||||
.textStyle(Theme.of(context).textTheme.bodyMedium!)
|
const Gap(8),
|
||||||
.padding(horizontal: 16),
|
Row(
|
||||||
const Gap(8),
|
spacing: 2,
|
||||||
Row(
|
children: [
|
||||||
spacing: 2,
|
Text(widget.allSources.where((x) => x.id == article.source).first.label)
|
||||||
children: [
|
.textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||||
Text(widget.allSources.where((x) => x.id == article.source).first.label)
|
],
|
||||||
.textStyle(Theme.of(context).textTheme.bodySmall!),
|
).opacity(0.75).padding(horizontal: 16),
|
||||||
|
Row(
|
||||||
|
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!),
|
||||||
|
],
|
||||||
|
).opacity(0.75).padding(horizontal: 16),
|
||||||
|
const Gap(16),
|
||||||
],
|
],
|
||||||
).opacity(0.75).padding(horizontal: 16),
|
),
|
||||||
Row(
|
),
|
||||||
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!),
|
|
||||||
],
|
|
||||||
).opacity(0.75).padding(horizontal: 16),
|
|
||||||
const Gap(16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import 'dart:math' as math;
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/notification.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/notification.dart';
|
import 'package:surface/types/notification.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
@@ -21,6 +23,16 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
|||||||
import '../providers/userinfo.dart';
|
import '../providers/userinfo.dart';
|
||||||
import '../widgets/unauthorized_hint.dart';
|
import '../widgets/unauthorized_hint.dart';
|
||||||
|
|
||||||
|
const Map<String, IconData> kNotificationTopicIcons = {
|
||||||
|
'general': Symbols.notifications,
|
||||||
|
'passport.security.alert': Symbols.gpp_maybe,
|
||||||
|
'passport.security.otp': Symbols.password,
|
||||||
|
'interactive.subscription': Symbols.subscriptions,
|
||||||
|
'interactive.feedback': Symbols.add_reaction,
|
||||||
|
'messaging.callStart': Symbols.call_received,
|
||||||
|
'wallet.transaction.new': Symbols.receipt,
|
||||||
|
};
|
||||||
|
|
||||||
class NotificationScreen extends StatefulWidget {
|
class NotificationScreen extends StatefulWidget {
|
||||||
const NotificationScreen({super.key});
|
const NotificationScreen({super.key});
|
||||||
|
|
||||||
@@ -36,13 +48,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
final List<SnNotification> _notifications = List.empty(growable: true);
|
final List<SnNotification> _notifications = List.empty(growable: true);
|
||||||
int? _totalCount;
|
int? _totalCount;
|
||||||
|
|
||||||
static const Map<String, IconData> kNotificationTopicIcons = {
|
|
||||||
'passport.security.alert': Symbols.gpp_maybe,
|
|
||||||
'interactive.subscription': Symbols.subscriptions,
|
|
||||||
'interactive.feedback': Symbols.add_reaction,
|
|
||||||
'messaging.callStart': Symbols.call_received,
|
|
||||||
};
|
|
||||||
|
|
||||||
Future<void> _fetchNotifications() async {
|
Future<void> _fetchNotifications() async {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
if (!ua.isAuthorized) return;
|
if (!ua.isAuthorized) return;
|
||||||
@@ -51,14 +56,13 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final nty = context.read<NotificationProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/notifications?take=10');
|
final resp = await sn.client.get('/cgi/id/notifications?take=10');
|
||||||
_totalCount = resp.data['count'];
|
_totalCount = resp.data['count'];
|
||||||
_notifications.addAll(
|
_notifications.addAll(
|
||||||
resp.data['data']
|
resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? [],
|
||||||
?.map((e) => SnNotification.fromJson(e))
|
|
||||||
.cast<SnNotification>() ??
|
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
|
nty.updateTray();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -85,9 +89,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final nty = context.read<NotificationProvider>();
|
||||||
final resp = await sn.client.put('/cgi/id/notifications/read/all');
|
final resp = await sn.client.put('/cgi/id/notifications/read/all');
|
||||||
_notifications.clear();
|
_notifications.clear();
|
||||||
_fetchNotifications();
|
_fetchNotifications();
|
||||||
|
nty.clear();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar(
|
context.showSnackbar(
|
||||||
@@ -180,8 +186,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
_fetchNotifications();
|
_fetchNotifications();
|
||||||
},
|
},
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
hasReachedMax: _totalCount != null &&
|
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!,
|
||||||
_notifications.length >= _totalCount!,
|
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
final nty = _notifications[idx];
|
final nty = _notifications[idx];
|
||||||
return Row(
|
return Row(
|
||||||
@@ -213,29 +218,36 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
isAutoWarp: true,
|
isAutoWarp: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if ([
|
if (['interactive.reply', 'interactive.feedback', 'interactive.subscription']
|
||||||
'interactive.feedback',
|
.contains(nty.topic) &&
|
||||||
'interactive.subscription'
|
|
||||||
].contains(nty.topic) &&
|
|
||||||
nty.metadata['related_post'] != null)
|
nty.metadata['related_post'] != null)
|
||||||
StyledWidget(Container(
|
GestureDetector(
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
borderRadius: const BorderRadius.all(
|
decoration: BoxDecoration(
|
||||||
Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
width: 1,
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: PostItem(
|
||||||
|
data: SnPost.fromJson(
|
||||||
|
nty.metadata['related_post']!,
|
||||||
|
),
|
||||||
|
showComments: false,
|
||||||
|
showReactions: false,
|
||||||
|
showMenu: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: PostItem(
|
onTap: () {
|
||||||
data: SnPost.fromJson(
|
GoRouter.of(context).pushNamed(
|
||||||
nty.metadata['related_post']!,
|
'postDetail',
|
||||||
),
|
pathParameters: {
|
||||||
showComments: false,
|
'slug': nty.metadata['related_post']!['id'].toString(),
|
||||||
showReactions: false,
|
},
|
||||||
showMenu: false,
|
);
|
||||||
),
|
},
|
||||||
)).padding(top: 8),
|
).padding(top: 8),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -260,10 +272,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.check),
|
icon: const Icon(Symbols.check),
|
||||||
padding: EdgeInsets.all(0),
|
padding: EdgeInsets.all(0),
|
||||||
visualDensity:
|
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||||
const VisualDensity(horizontal: -4, vertical: -4),
|
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty),
|
||||||
onPressed:
|
|
||||||
_isSubmitting ? null : () => _markOneAsRead(nty),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16);
|
).padding(horizontal: 16);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
@@ -17,7 +16,6 @@ import 'package:surface/widgets/navigation/app_background.dart';
|
|||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||||
import 'package:surface/widgets/post/post_item.dart';
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
|
||||||
|
|
||||||
class PostDetailScreen extends StatefulWidget {
|
class PostDetailScreen extends StatefulWidget {
|
||||||
final String slug;
|
final String slug;
|
||||||
@@ -64,7 +62,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
|
||||||
|
final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
|
||||||
|
|
||||||
return AppBackground(
|
return AppBackground(
|
||||||
isRoot: widget.onBack != null,
|
isRoot: widget.onBack != null,
|
||||||
@@ -114,7 +113,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: PostItem(
|
child: PostItem(
|
||||||
data: _data!,
|
data: _data!,
|
||||||
maxWidth: 640,
|
maxWidth: maxWidth,
|
||||||
showComments: false,
|
showComments: false,
|
||||||
showFullPost: true,
|
showFullPost: true,
|
||||||
onChanged: (data) {
|
onChanged: (data) {
|
||||||
@@ -125,11 +124,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SliverToBoxAdapter(child: Divider(height: 1)),
|
if (_data != null && _data!.type != 'video') const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||||
if (_data != null)
|
if (_data != null && _data!.type != 'video')
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 640),
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -142,51 +141,30 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
).padding(horizontal: 20, vertical: 12).center(),
|
).padding(horizontal: 20, vertical: 12).center(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_data != null && ua.isAuthorized)
|
if (_data != null && ua.isAuthorized && _data!.type != 'video')
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Container(
|
child: PostCommentQuickAction(
|
||||||
height: 240,
|
parentPost: _data!,
|
||||||
constraints: const BoxConstraints(maxWidth: 640),
|
maxWidth: maxWidth,
|
||||||
margin:
|
onPosted: () {
|
||||||
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero,
|
setState(() {
|
||||||
decoration: BoxDecoration(
|
_data = _data!.copyWith(
|
||||||
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
metric: _data!.metric.copyWith(
|
||||||
? const BorderRadius.all(Radius.circular(8))
|
replyCount: _data!.metric.replyCount + 1,
|
||||||
: BorderRadius.zero,
|
),
|
||||||
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
);
|
||||||
? Border.all(
|
});
|
||||||
color: Theme.of(context).dividerColor,
|
_childListKey.currentState!.refresh();
|
||||||
width: 1 / devicePixelRatio,
|
},
|
||||||
)
|
),
|
||||||
: Border.symmetric(
|
|
||||||
horizontal: BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: PostMiniEditor(
|
|
||||||
postReplyId: _data!.id,
|
|
||||||
onPost: () {
|
|
||||||
setState(() {
|
|
||||||
_data = _data!.copyWith(
|
|
||||||
metric: _data!.metric.copyWith(
|
|
||||||
replyCount: _data!.metric.replyCount + 1,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
_childListKey.currentState!.refresh();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).center(),
|
|
||||||
),
|
),
|
||||||
if (_data != null)
|
if (_data != null && _data!.type != 'video')
|
||||||
PostCommentSliverList(
|
PostCommentSliverList(
|
||||||
key: _childListKey,
|
key: _childListKey,
|
||||||
parentPostId: _data!.id,
|
parentPost: _data!,
|
||||||
maxWidth: 640,
|
maxWidth: maxWidth,
|
||||||
),
|
),
|
||||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
if (_data != null && _data!.type == 'video') SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@@ -134,7 +133,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
InfiniteList(
|
InfiniteList(
|
||||||
padding: const EdgeInsets.only(top: 100),
|
padding: const EdgeInsets.only(top: 100 + 8),
|
||||||
itemCount: _posts.length,
|
itemCount: _posts.length,
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
||||||
@@ -142,27 +141,18 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
_fetchPosts();
|
_fetchPosts();
|
||||||
},
|
},
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
return GestureDetector(
|
return OpenablePostItem(
|
||||||
child: PostItem(
|
data: _posts[idx],
|
||||||
data: _posts[idx],
|
maxWidth: 640,
|
||||||
maxWidth: 640,
|
onChanged: (data) {
|
||||||
onChanged: (data) {
|
setState(() => _posts[idx] = data);
|
||||||
setState(() => _posts[idx] = data);
|
},
|
||||||
},
|
onDeleted: () {
|
||||||
onDeleted: () {
|
_refreshPosts();
|
||||||
_refreshPosts();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'postDetail',
|
|
||||||
pathParameters: {'slug': _posts[idx].id.toString()},
|
|
||||||
extra: _posts[idx],
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
separatorBuilder: (_, __) => const Gap(8),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 16,
|
top: 16,
|
||||||
|
|||||||
@@ -287,8 +287,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
Theme(
|
Theme(
|
||||||
data: Theme.of(context).copyWith(
|
data: Theme.of(context).copyWith(
|
||||||
appBarTheme: Theme.of(context).appBarTheme.copyWith(
|
appBarTheme: Theme.of(context).appBarTheme.copyWith(
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SliverAppBar(
|
child: SliverAppBar(
|
||||||
expandedHeight: _appBarHeight,
|
expandedHeight: _appBarHeight,
|
||||||
@@ -597,25 +597,16 @@ class _PublisherPostList extends StatelessWidget {
|
|||||||
hasReachedMax: postCount != null && posts.length >= postCount!,
|
hasReachedMax: postCount != null && posts.length >= postCount!,
|
||||||
onFetchData: fetchPosts,
|
onFetchData: fetchPosts,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
return GestureDetector(
|
return OpenablePostItem(
|
||||||
child: PostItem(
|
data: posts[idx],
|
||||||
data: posts[idx],
|
maxWidth: 640,
|
||||||
maxWidth: 640,
|
onChanged: (data) {
|
||||||
onChanged: (data) {
|
onChanged(idx, data);
|
||||||
onChanged(idx, data);
|
|
||||||
},
|
|
||||||
onDeleted: onDeleted,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'postDetail',
|
|
||||||
pathParameters: {'slug': posts[idx].id.toString()},
|
|
||||||
extra: posts[idx],
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
onDeleted: onDeleted,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
separatorBuilder: (_, __) => const Gap(8),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,16 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:surface/widgets/realm/realm_item.dart';
|
||||||
import 'package:surface/widgets/unauthorized_hint.dart';
|
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
|
||||||
|
|
||||||
class RealmScreen extends StatefulWidget {
|
class RealmScreen extends StatefulWidget {
|
||||||
const RealmScreen({super.key});
|
const RealmScreen({super.key});
|
||||||
@@ -75,12 +74,12 @@ class _RealmScreenState extends State<RealmScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_isCompactView = context.read<ConfigProvider>().realmCompactView;
|
||||||
_fetchRealms();
|
_fetchRealms();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
@@ -100,10 +99,17 @@ class _RealmScreenState extends State<RealmScreen> {
|
|||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenRealm').tr(),
|
title: Text('screenRealm').tr(),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.globe),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pushNamed('realmDiscovery');
|
||||||
|
},
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: !_isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
|
icon: !_isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() => _isCompactView = !_isCompactView);
|
setState(() => _isCompactView = !_isCompactView);
|
||||||
|
context.read<ConfigProvider>().realmCompactView = _isCompactView;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@@ -128,121 +134,46 @@ class _RealmScreenState extends State<RealmScreen> {
|
|||||||
itemCount: _realms?.length ?? 0,
|
itemCount: _realms?.length ?? 0,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
final realm = _realms![idx];
|
final realm = _realms![idx];
|
||||||
if (_isCompactView) {
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
leading: AccountImage(
|
|
||||||
content: realm.avatar,
|
|
||||||
fallbackWidget: const Icon(Symbols.group, size: 20),
|
|
||||||
),
|
|
||||||
title: Text(realm.name),
|
|
||||||
subtitle: Text(
|
|
||||||
realm.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
trailing: PopupMenuButton(
|
|
||||||
itemBuilder: (BuildContext context) => [
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.edit),
|
|
||||||
const Gap(16),
|
|
||||||
Text('edit').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'realmManage',
|
|
||||||
queryParameters: {'editing': realm.alias},
|
|
||||||
).then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
_fetchRealms();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.delete),
|
|
||||||
const Gap(16),
|
|
||||||
Text('delete').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_deleteRealm(realm);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'realmDetail',
|
|
||||||
pathParameters: {'alias': realm.alias},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
return RealmItemWidget(
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
showPopularity: false,
|
||||||
child: Card(
|
item: realm,
|
||||||
margin: const EdgeInsets.all(12),
|
isListView: _isCompactView,
|
||||||
child: InkWell(
|
actionListView: [
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
PopupMenuItem(
|
||||||
child: Column(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
AspectRatio(
|
const Icon(Symbols.edit),
|
||||||
aspectRatio: 16 / 7,
|
const Gap(16),
|
||||||
child: Stack(
|
Text('edit').tr(),
|
||||||
clipBehavior: Clip.none,
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: Container(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
child: (realm.banner?.isEmpty ?? true)
|
|
||||||
? const SizedBox.shrink()
|
|
||||||
: AutoResizeUniversalImage(
|
|
||||||
sn.getAttachmentUrl(realm.banner!),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: -30,
|
|
||||||
left: 18,
|
|
||||||
child: AccountImage(
|
|
||||||
content: realm.avatar,
|
|
||||||
radius: 24,
|
|
||||||
fallbackWidget: const Icon(Symbols.group, size: 24),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(20 + 12),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
|
|
||||||
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 24, bottom: 14),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'realmDetail',
|
'realmManage',
|
||||||
pathParameters: {'alias': realm.alias},
|
queryParameters: {'editing': realm.alias},
|
||||||
);
|
).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
_fetchRealms();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
PopupMenuItem(
|
||||||
).center();
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete),
|
||||||
|
const Gap(16),
|
||||||
|
Text('delete').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_deleteRealm(realm);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onUpdate: _fetchRealms,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
|||||||
_aliasController.text = out.alias;
|
_aliasController.text = out.alias;
|
||||||
_nameController.text = out.name;
|
_nameController.text = out.name;
|
||||||
_descriptionController.text = out.description;
|
_descriptionController.text = out.description;
|
||||||
|
_isPublic = out.isPublic;
|
||||||
|
_isCommunity = out.isCommunity;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
if (context.mounted) context.showErrorDialog(err);
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
@@ -67,6 +69,9 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
|||||||
|
|
||||||
final _imagePicker = ImagePicker();
|
final _imagePicker = ImagePicker();
|
||||||
|
|
||||||
|
bool _isPublic = false;
|
||||||
|
bool _isCommunity = false;
|
||||||
|
|
||||||
Future<void> _updateImage(String place) async {
|
Future<void> _updateImage(String place) async {
|
||||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
@@ -138,6 +143,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
|||||||
'description': _descriptionController.text,
|
'description': _descriptionController.text,
|
||||||
'avatar': _avatar,
|
'avatar': _avatar,
|
||||||
'banner': _banner,
|
'banner': _banner,
|
||||||
|
'is_public': _isPublic,
|
||||||
|
'is_community': _isCommunity,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -293,6 +300,23 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
|||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
|
CheckboxListTile(
|
||||||
|
value: _isPublic,
|
||||||
|
title: Text('realmIsPublic'.tr()),
|
||||||
|
subtitle: Text('realmIsPublicDescription'.tr()),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _isPublic = value ?? false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
value: _isCommunity,
|
||||||
|
title: Text('realmIsCommunity'.tr()),
|
||||||
|
subtitle: Text('realmIsCommunityDescription'.tr()),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _isCommunity = value ?? false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -5,14 +5,19 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/types/account.dart';
|
||||||
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/account/account_select.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class RealmDetailScreen extends StatefulWidget {
|
class RealmDetailScreen extends StatefulWidget {
|
||||||
@@ -58,18 +63,36 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<SnChannel>? _channels;
|
||||||
|
|
||||||
|
Future<void> _fetchChannels() async {
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/im/channels/${widget.alias}/public');
|
||||||
|
_channels = List<SnChannel>.from(
|
||||||
|
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (mounted) context.showErrorDialog(err);
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_fetchRealm().then((_) {
|
_fetchRealm().then((_) {
|
||||||
_fetchPublishers();
|
_fetchPublishers();
|
||||||
|
_fetchChannels();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: 3,
|
length: 4,
|
||||||
child: AppScaffold(
|
child: AppScaffold(
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
@@ -81,6 +104,7 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
|
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
|
Tab(icon: Icon(Symbols.explore, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
|
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
|
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
|
||||||
],
|
],
|
||||||
@@ -91,7 +115,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
},
|
},
|
||||||
body: TabBarView(
|
body: TabBarView(
|
||||||
children: [
|
children: [
|
||||||
_RealmDetailHomeWidget(realm: _realm, publishers: _publishers),
|
_RealmDetailHomeWidget(realm: _realm, publishers: _publishers, channels: _channels),
|
||||||
|
_RealmPostListWidget(realm: _realm),
|
||||||
_RealmMemberListWidget(realm: _realm),
|
_RealmMemberListWidget(realm: _realm),
|
||||||
_RealmSettingsWidget(
|
_RealmSettingsWidget(
|
||||||
realm: _realm,
|
realm: _realm,
|
||||||
@@ -110,8 +135,9 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
class _RealmDetailHomeWidget extends StatelessWidget {
|
class _RealmDetailHomeWidget extends StatelessWidget {
|
||||||
final SnRealm? realm;
|
final SnRealm? realm;
|
||||||
final List<SnPublisher>? publishers;
|
final List<SnPublisher>? publishers;
|
||||||
|
final List<SnChannel>? channels;
|
||||||
|
|
||||||
const _RealmDetailHomeWidget({required this.realm, this.publishers});
|
const _RealmDetailHomeWidget({required this.realm, this.publishers, this.channels});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -133,30 +159,76 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
).padding(horizontal: 24),
|
).padding(horizontal: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
const Divider(),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: CustomScrollView(
|
||||||
padding: EdgeInsets.zero,
|
slivers: [
|
||||||
itemCount: publishers?.length ?? 0,
|
if (publishers?.isNotEmpty ?? false)
|
||||||
itemBuilder: (context, idx) {
|
SliverToBoxAdapter(
|
||||||
final ele = publishers![idx];
|
child: Container(
|
||||||
return ListTile(
|
width: double.infinity,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
leading: AccountImage(
|
child: Text('realmCommunityPublishersHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
|
||||||
content: ele.avatar,
|
.padding(horizontal: 24, vertical: 8),
|
||||||
fallbackWidget: const Icon(Symbols.group, size: 24),
|
),
|
||||||
),
|
),
|
||||||
title: Text(ele.nick),
|
SliverList.builder(
|
||||||
subtitle: Text('@${ele.name}'),
|
itemCount: publishers?.length ?? 0,
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
itemBuilder: (context, idx) {
|
||||||
onTap: () {
|
final ele = publishers![idx];
|
||||||
GoRouter.of(context).pushNamed(
|
return ListTile(
|
||||||
'postPublisher',
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
pathParameters: {'name': ele.name},
|
leading: AccountImage(
|
||||||
|
content: ele.avatar,
|
||||||
|
fallbackWidget: const Icon(Symbols.group, size: 24),
|
||||||
|
),
|
||||||
|
title: Text(ele.nick),
|
||||||
|
subtitle: Text('@${ele.name}'),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'postPublisher',
|
||||||
|
pathParameters: {'name': ele.name},
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
if (channels?.isNotEmpty ?? false)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
|
||||||
|
.padding(horizontal: 24, vertical: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverList.builder(
|
||||||
|
itemCount: channels?.length ?? 0,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final ele = channels![idx];
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
leading: AccountImage(
|
||||||
|
content: null,
|
||||||
|
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||||
|
),
|
||||||
|
title: Text(ele.name),
|
||||||
|
subtitle: Text('#${ele.alias}'),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'chatRoom',
|
||||||
|
pathParameters: {
|
||||||
|
'scope': realm?.alias ?? 'global',
|
||||||
|
'alias': ele.alias,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -164,6 +236,72 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _RealmPostListWidget extends StatefulWidget {
|
||||||
|
final SnRealm? realm;
|
||||||
|
|
||||||
|
const _RealmPostListWidget({this.realm});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_RealmPostListWidget> createState() => _RealmPostListWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RealmPostListWidgetState extends State<_RealmPostListWidget> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
int? _totalCount;
|
||||||
|
final List<SnPost> _posts = List.empty(growable: true);
|
||||||
|
|
||||||
|
Future<void> _fetchPosts() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final pt = context.read<SnPostContentProvider>();
|
||||||
|
final out = await pt.listPosts(
|
||||||
|
take: 10,
|
||||||
|
offset: _posts.length,
|
||||||
|
realm: widget.realm?.id.toString(),
|
||||||
|
);
|
||||||
|
_totalCount = out.$2;
|
||||||
|
_posts.addAll(out.$1);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MediaQuery.removePadding(
|
||||||
|
context: context,
|
||||||
|
removeTop: true,
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: _fetchPosts,
|
||||||
|
child: InfiniteList(
|
||||||
|
itemCount: _posts.length,
|
||||||
|
isLoading: _isBusy,
|
||||||
|
hasReachedMax: _totalCount != null && _posts.length >= _totalCount!,
|
||||||
|
onFetchData: _fetchPosts,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final post = _posts[idx];
|
||||||
|
return OpenablePostItem(
|
||||||
|
data: post,
|
||||||
|
maxWidth: 640,
|
||||||
|
onChanged: (data) {
|
||||||
|
setState(() => _posts[idx] = data);
|
||||||
|
},
|
||||||
|
onDeleted: () {
|
||||||
|
setState(() => _posts.removeAt(idx));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) => const Gap(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(top: 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _RealmMemberListWidget extends StatefulWidget {
|
class _RealmMemberListWidget extends StatefulWidget {
|
||||||
final SnRealm? realm;
|
final SnRealm? realm;
|
||||||
|
|
||||||
@@ -187,7 +325,7 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: {
|
final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: {
|
||||||
'take': 10,
|
'take': 10,
|
||||||
'offset': 0,
|
'offset': _members.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
final out = List<SnRealmMember>.from(
|
final out = List<SnRealmMember>.from(
|
||||||
@@ -229,13 +367,35 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showMemberAdd() {
|
Future<void> _addMember(SnAccount related) async {
|
||||||
showModalBottomSheet(
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.post(
|
||||||
|
'/cgi/id/realms/${widget.realm!.alias}/members',
|
||||||
|
data: {'related': related.name},
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('realmMemberAdded'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showMemberAdd() async {
|
||||||
|
final user = await showModalBottomSheet<SnAccount?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _NewRealmMemberWidget(
|
builder: (context) => AccountSelect(
|
||||||
realm: widget.realm!,
|
title: 'realmMemberAdd'.tr(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (user == null) return;
|
||||||
|
_addMember(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -293,85 +453,6 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NewRealmMemberWidget extends StatefulWidget {
|
|
||||||
final SnRealm realm;
|
|
||||||
|
|
||||||
const _NewRealmMemberWidget({required this.realm});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_NewRealmMemberWidget> createState() => _NewRealmMemberWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NewRealmMemberWidgetState extends State<_NewRealmMemberWidget> {
|
|
||||||
bool _isBusy = false;
|
|
||||||
|
|
||||||
final TextEditingController _relatedController = TextEditingController();
|
|
||||||
|
|
||||||
Future<void> _performAction() async {
|
|
||||||
if (_relatedController.text.isEmpty) return;
|
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client.post(
|
|
||||||
'/cgi/id/realms/${widget.realm.alias}/members',
|
|
||||||
data: {
|
|
||||||
'related': _relatedController.text,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!mounted) return;
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
context.showSnackbar('channelMemberAdded'.tr());
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isBusy = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_relatedController.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return StyledWidget(Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'realmMemberAdd',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
).tr(),
|
|
||||||
const Gap(12),
|
|
||||||
TextField(
|
|
||||||
controller: _relatedController,
|
|
||||||
readOnly: _isBusy,
|
|
||||||
autocorrect: false,
|
|
||||||
autofocus: true,
|
|
||||||
textCapitalization: TextCapitalization.none,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'fieldMemberRelatedName'.tr(),
|
|
||||||
suffix: SizedBox(
|
|
||||||
height: 24,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: _isBusy ? null : () => _performAction(),
|
|
||||||
icon: Icon(Symbols.send),
|
|
||||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)).padding(all: 24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RealmSettingsWidget extends StatefulWidget {
|
class _RealmSettingsWidget extends StatefulWidget {
|
||||||
final SnRealm? realm;
|
final SnRealm? realm;
|
||||||
final Function() onUpdate;
|
final Function() onUpdate;
|
||||||
@@ -398,12 +479,31 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
|||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}');
|
await sn.client.delete('/cgi/id/realms/${widget.realm!.id}');
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _leaveRealm() async {
|
||||||
|
final confirm = await context.showConfirmDialog(
|
||||||
|
'realmLeave'.tr(),
|
||||||
|
'realmLeaveDescription'.tr(),
|
||||||
|
);
|
||||||
|
if (!confirm) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}/me');
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
context.showSnackbar('realmDeleted'.tr(args: [
|
|
||||||
'#${widget.realm!.alias}',
|
|
||||||
]));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -422,22 +522,31 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
|||||||
children: [
|
children: [
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.edit),
|
leading: const Icon(Symbols.logout),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
title: Text('realmEdit').tr(),
|
title: Text('realmLeave').tr(),
|
||||||
subtitle: Text('realmEditDescription').tr(),
|
subtitle: Text('realmLeaveDescription').tr(),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
onTap: () {
|
onTap: _isBusy ? null : () => _leaveRealm(),
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'realmManage',
|
|
||||||
queryParameters: {'editing': widget.realm!.alias},
|
|
||||||
).then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
widget.onUpdate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
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)
|
if (isOwned)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.delete),
|
leading: const Icon(Symbols.delete),
|
||||||
|
|||||||
255
lib/screens/realm/realm_discovery.dart
Normal file
255
lib/screens/realm/realm_discovery.dart
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
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/config.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/types/chat.dart';
|
||||||
|
import 'package:surface/types/realm.dart';
|
||||||
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:surface/widgets/realm/realm_item.dart';
|
||||||
|
|
||||||
|
class RealmDiscoveryScreen extends StatefulWidget {
|
||||||
|
const RealmDiscoveryScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RealmDiscoveryScreen> createState() => _RealmDiscoveryScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
|
||||||
|
List<SnRealm>? _realms;
|
||||||
|
bool _isBusy = false;
|
||||||
|
bool _isCompactView = false;
|
||||||
|
|
||||||
|
Future<void> _fetchRealms() async {
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/id/realms');
|
||||||
|
_realms = List<SnRealm>.from(
|
||||||
|
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (mounted) context.showErrorDialog(err);
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_isCompactView = context.read<ConfigProvider>().realmCompactView;
|
||||||
|
_fetchRealms();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
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: [
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: _fetchRealms,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: _realms?.length ?? 0,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final realm = _realms![idx];
|
||||||
|
return RealmItemWidget(
|
||||||
|
item: realm,
|
||||||
|
isListView: _isCompactView,
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _RealmJoinPopup(realm: realm),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RealmJoinPopup extends StatefulWidget {
|
||||||
|
final SnRealm realm;
|
||||||
|
|
||||||
|
const _RealmJoinPopup({required this.realm});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_RealmJoinPopup> createState() => _RealmJoinPopupState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
||||||
|
final List<String> _planJoinChannels = List.empty(growable: true);
|
||||||
|
|
||||||
|
List<SnChannel>? _channels;
|
||||||
|
bool _isBusy = false;
|
||||||
|
bool _isJoining = false;
|
||||||
|
|
||||||
|
Future<void> _fetchPublicChannels() async {
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
|
||||||
|
final out = List<SnChannel>.from(
|
||||||
|
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
||||||
|
);
|
||||||
|
setState(() => _channels = out);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _joinRealm() async {
|
||||||
|
try {
|
||||||
|
setState(() => _isJoining = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
await sn.client.post('/cgi/id/realms/${widget.realm.alias}/members', data: {
|
||||||
|
'related': ua.user?.name,
|
||||||
|
});
|
||||||
|
await _joinSelectedChannels();
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('realmJoined'.tr(args: [widget.realm.name]));
|
||||||
|
Navigator.pop(context);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isJoining = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _joinSelectedChannels() async {
|
||||||
|
if (_planJoinChannels.isEmpty) return;
|
||||||
|
for (final channel in _planJoinChannels) {
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
await sn.client.post('/cgi/im/channels/${widget.realm.alias}/$channel/members', data: {
|
||||||
|
'related': ua.user?.name,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchPublicChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.group_add, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text('realmJoin', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.realm.name,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.realm.description,
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isJoining ? null : () => _joinRealm(),
|
||||||
|
child: Text('join'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24, bottom: 12),
|
||||||
|
const Divider(height: 1),
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
|
||||||
|
.padding(horizontal: 24, vertical: 8),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _channels?.length ?? 0,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final channel = _channels![index];
|
||||||
|
return CheckboxListTile(
|
||||||
|
value: _planJoinChannels.contains(channel.alias),
|
||||||
|
title: Text(channel.name),
|
||||||
|
subtitle: Text(
|
||||||
|
channel.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
secondary: AccountImage(
|
||||||
|
content: null,
|
||||||
|
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
value ??= false;
|
||||||
|
if (value) {
|
||||||
|
setState(() => _planJoinChannels.add(channel.alias));
|
||||||
|
} else {
|
||||||
|
setState(() => _planJoinChannels.remove(channel.alias));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,10 @@ import 'package:dropdown_button2/dropdown_button2.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -14,7 +16,10 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
|
import 'package:surface/providers/database.dart';
|
||||||
|
import 'package:surface/providers/notification.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/sn_sticker.dart';
|
||||||
import 'package:surface/providers/theme.dart';
|
import 'package:surface/providers/theme.dart';
|
||||||
import 'package:surface/theme.dart';
|
import 'package:surface/theme.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
@@ -67,6 +72,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final dt = context.read<DatabaseProvider>();
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -81,7 +87,59 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('settingsAppearance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
Text('settingsAppearance')
|
||||||
|
.bold()
|
||||||
|
.fontSize(17)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
|
ListTile(
|
||||||
|
title: Text('settingsDisplayLanguage').tr(),
|
||||||
|
subtitle: Text('settingsDisplayLanguageDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
leading: const Icon(Symbols.translate),
|
||||||
|
trailing: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<Locale?>(
|
||||||
|
isExpanded: true,
|
||||||
|
items: [
|
||||||
|
...EasyLocalization.of(context)!
|
||||||
|
.supportedLocales
|
||||||
|
.mapIndexed((idx, ele) {
|
||||||
|
return DropdownMenuItem<Locale?>(
|
||||||
|
value: ele,
|
||||||
|
child:
|
||||||
|
Text('${ele.languageCode}-${ele.countryCode}')
|
||||||
|
.fontSize(14),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
DropdownMenuItem<Locale?>(
|
||||||
|
value: null,
|
||||||
|
child: Text('settingsDisplayLanguageSystem')
|
||||||
|
.tr()
|
||||||
|
.fontSize(14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
value: EasyLocalization.of(context)!.currentLocale,
|
||||||
|
onChanged: (Locale? value) {
|
||||||
|
if (value != null) {
|
||||||
|
EasyLocalization.of(context)!.setLocale(value);
|
||||||
|
} else {
|
||||||
|
EasyLocalization.of(context)!.resetLocale();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buttonStyleData: const ButtonStyleData(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 5,
|
||||||
|
),
|
||||||
|
height: 40,
|
||||||
|
width: 160,
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (!kIsWeb)
|
if (!kIsWeb)
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('settingsBackgroundImage').tr(),
|
title: Text('settingsBackgroundImage').tr(),
|
||||||
@@ -90,10 +148,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
leading: const Icon(Symbols.image),
|
leading: const Icon(Symbols.image),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final image = await ImagePicker().pickImage(source: ImageSource.gallery);
|
final image = await ImagePicker()
|
||||||
|
.pickImage(source: ImageSource.gallery);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
|
|
||||||
await File(image.path).copy('$_docBasepath/app_background_image');
|
await File(image.path)
|
||||||
|
.copy('$_docBasepath/app_background_image');
|
||||||
_prefs.setBool(kAppBackgroundStoreKey, true);
|
_prefs.setBool(kAppBackgroundStoreKey, true);
|
||||||
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
@@ -101,7 +161,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
if (!kIsWeb)
|
if (!kIsWeb)
|
||||||
FutureBuilder<bool>(
|
FutureBuilder<bool>(
|
||||||
future: File('$_docBasepath/app_background_image').exists(),
|
future:
|
||||||
|
File('$_docBasepath/app_background_image').exists(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData || !snapshot.data!) {
|
if (!snapshot.hasData || !snapshot.data!) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
@@ -109,12 +170,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text('settingsBackgroundImageClear').tr(),
|
title: Text('settingsBackgroundImageClear').tr(),
|
||||||
subtitle: Text('settingsBackgroundImageClearDescription').tr(),
|
subtitle:
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
Text('settingsBackgroundImageClearDescription')
|
||||||
|
.tr(),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Symbols.texture),
|
leading: const Icon(Symbols.texture),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
File('$_docBasepath/app_background_image').deleteSync();
|
File('$_docBasepath/app_background_image')
|
||||||
|
.deleteSync();
|
||||||
_prefs.remove(kAppBackgroundStoreKey);
|
_prefs.remove(kAppBackgroundStoreKey);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
@@ -144,7 +209,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value);
|
Color pickerColor = Color(
|
||||||
|
_prefs.getInt(kAppColorSchemeStoreKey) ??
|
||||||
|
Colors.indigo.value);
|
||||||
final color = await showDialog<Color?>(
|
final color = await showDialog<Color?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
@@ -205,14 +272,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
],
|
],
|
||||||
value: _prefs.getInt(kAppColorSchemeStoreKey) == null
|
value: _prefs.getInt(kAppColorSchemeStoreKey) == null
|
||||||
? 1
|
? 1
|
||||||
: kColorSchemes.values
|
: kColorSchemes.values.toList().indexWhere((ele) =>
|
||||||
.toList()
|
ele.value ==
|
||||||
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
|
_prefs.getInt(kAppColorSchemeStoreKey)),
|
||||||
onChanged: (int? value) {
|
onChanged: (int? value) {
|
||||||
if (value != null && value != -1) {
|
if (value != null && value != -1) {
|
||||||
_prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values.elementAt(value).value);
|
_prefs.setInt(kAppColorSchemeStoreKey,
|
||||||
|
kColorSchemes.values.elementAt(value).value);
|
||||||
final th = context.read<ThemeProvider>();
|
final th = context.read<ThemeProvider>();
|
||||||
th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value));
|
th.reloadTheme(
|
||||||
|
seedColorOverride:
|
||||||
|
kColorSchemes.values.elementAt(value));
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|
||||||
context.showSnackbar('colorSchemeApplied'.tr());
|
context.showSnackbar('colorSchemeApplied'.tr());
|
||||||
@@ -248,7 +318,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
secondary: const Icon(Symbols.left_panel_close),
|
secondary: const Icon(Symbols.left_panel_close),
|
||||||
title: Text('settingsDrawerPreferCollapse').tr(),
|
title: Text('settingsDrawerPreferCollapse').tr(),
|
||||||
subtitle: Text('settingsDrawerPreferCollapseDescription').tr(),
|
subtitle:
|
||||||
|
Text('settingsDrawerPreferCollapseDescription').tr(),
|
||||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
|
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -263,7 +334,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('settingsFeatures').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
Text('settingsFeatures')
|
||||||
|
.bold()
|
||||||
|
.fontSize(17)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
secondary: const Icon(Symbols.vibration),
|
secondary: const Icon(Symbols.vibration),
|
||||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
@@ -305,7 +380,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('settingsNetwork').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
Text('settingsNetwork')
|
||||||
|
.bold()
|
||||||
|
.fontSize(17)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _serverUrlController,
|
controller: _serverUrlController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -326,7 +405,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
).padding(horizontal: 16, top: 8, bottom: 4),
|
).padding(horizontal: 16, top: 8, bottom: 4),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('settingsNetworkServerPreset').tr(),
|
title: Text('settingsNetworkServerPreset').tr(),
|
||||||
@@ -338,7 +418,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
items: [
|
items: [
|
||||||
...kNetworkServerDirectory,
|
...kNetworkServerDirectory,
|
||||||
if (!kNetworkServerDirectory.map((ele) => ele.$2).contains(_serverUrlController.text))
|
if (!kNetworkServerDirectory
|
||||||
|
.map((ele) => ele.$2)
|
||||||
|
.contains(_serverUrlController.text))
|
||||||
('Custom', _serverUrlController.text),
|
('Custom', _serverUrlController.text),
|
||||||
]
|
]
|
||||||
.map(
|
.map(
|
||||||
@@ -350,7 +432,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(item.$1).fontSize(14),
|
Text(item.$1).fontSize(14),
|
||||||
Text(item.$2, overflow: TextOverflow.ellipsis).fontSize(11)
|
Text(item.$2, overflow: TextOverflow.ellipsis)
|
||||||
|
.fontSize(11)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -396,7 +479,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('settingsPerformance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
Text('settingsPerformance')
|
||||||
|
.bold()
|
||||||
|
.fontSize(17)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('settingsImageQuality').tr(),
|
title: Text('settingsImageQuality').tr(),
|
||||||
subtitle: Text('settingsImageQualityDescription').tr(),
|
subtitle: Text('settingsImageQualityDescription').tr(),
|
||||||
@@ -404,7 +491,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
leading: const Icon(Symbols.image),
|
leading: const Icon(Symbols.image),
|
||||||
trailing: DropdownButtonHideUnderline(
|
trailing: DropdownButtonHideUnderline(
|
||||||
child: DropdownButton2<FilterQuality>(
|
child: DropdownButton2<FilterQuality>(
|
||||||
value: kImageQualityLevel.values.elementAtOrNull(_prefs.getInt('app_image_quality') ?? 3) ??
|
value: kImageQualityLevel.values.elementAtOrNull(
|
||||||
|
_prefs.getInt('app_image_quality') ?? 3) ??
|
||||||
FilterQuality.high,
|
FilterQuality.high,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
items: kImageQualityLevel.entries
|
items: kImageQualityLevel.entries
|
||||||
@@ -417,7 +505,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (FilterQuality? value) {
|
onChanged: (FilterQuality? value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
_prefs.setInt('app_image_quality', kImageQualityLevel.values.toList().indexOf(value));
|
_prefs.setInt('app_image_quality',
|
||||||
|
kImageQualityLevel.values.toList().indexOf(value));
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
buttonStyleData: const ButtonStyleData(
|
buttonStyleData: const ButtonStyleData(
|
||||||
@@ -439,7 +528,82 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('settingsMisc').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
Text('settingsMisc')
|
||||||
|
.bold()
|
||||||
|
.fontSize(17)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.database),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('databaseSize').tr(),
|
||||||
|
subtitle: FutureBuilder(
|
||||||
|
future: dt.getDatabaseSize(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData || kIsWeb) {
|
||||||
|
return Text('unknown').tr();
|
||||||
|
}
|
||||||
|
return Text(
|
||||||
|
snapshot.data!.formatBytes(),
|
||||||
|
style: GoogleFonts.robotoMono(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.database_off),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('databaseDelete').tr(),
|
||||||
|
subtitle: Text('databaseDeleteDescription').tr(),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
await dt.removeDatabase();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
context.showSnackbar('databaseDeleted'.tr());
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
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(
|
ListTile(
|
||||||
title: Text('settingsMiscAbout').tr(),
|
title: Text('settingsMiscAbout').tr(),
|
||||||
subtitle: Text('settingsMiscAboutDescription').tr(),
|
subtitle: Text('settingsMiscAboutDescription').tr(),
|
||||||
|
|||||||
@@ -8,9 +8,20 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
|
import 'package:surface/providers/channel.dart';
|
||||||
|
import 'package:surface/providers/user_directory.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/screens/chat/room.dart';
|
||||||
import 'package:surface/screens/post/post_editor.dart';
|
import 'package:surface/screens/post/post_editor.dart';
|
||||||
|
import 'package:surface/types/chat.dart';
|
||||||
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
class AppSharingListener extends StatefulWidget {
|
class AppSharingListener extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@@ -51,20 +62,39 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
pathParameters: {
|
pathParameters: {
|
||||||
'mode': 'stories',
|
'mode': 'stories',
|
||||||
},
|
},
|
||||||
extra: PostEditorExtraProps(
|
extra: PostEditorExtra(
|
||||||
text: value
|
text: value
|
||||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||||
.map((e) => e.path).join('\n'),
|
.map((e) => e.path)
|
||||||
|
.join('\n'),
|
||||||
attachments: value
|
attachments: value
|
||||||
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
|
||||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(),
|
.contains(e.type))
|
||||||
|
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
leading: Icon(Icons.chat_outlined),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
title: Text('shareIntentSendChannel').tr(),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _ShareIntentChannelSelect(value: value),
|
||||||
|
).then((val) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
if (val == true) Navigator.pop(context);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
).width(280),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -103,7 +133,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||||
_initialize();
|
_initialize();
|
||||||
_initialHandle();
|
_initialHandle();
|
||||||
}
|
}
|
||||||
@@ -120,3 +150,193 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
return widget.child;
|
return widget.child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ShareIntentChannelSelect extends StatefulWidget {
|
||||||
|
final Iterable<SharedMediaFile> value;
|
||||||
|
|
||||||
|
const _ShareIntentChannelSelect({required this.value});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||||
|
bool _isBusy = true;
|
||||||
|
|
||||||
|
List<SnChannel>? _channels;
|
||||||
|
Map<int, SnChatMessage>? _lastMessages;
|
||||||
|
|
||||||
|
void _refreshChannels() {
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
if (!ua.isAuthorized) {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final chan = context.read<ChatChannelProvider>();
|
||||||
|
chan.fetchChannels().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)) return -1;
|
||||||
|
if (_lastMessages!.containsKey(b.id)) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
for (final channel in channels) {
|
||||||
|
if (channel.type == 1) {
|
||||||
|
await ud.listAccount(
|
||||||
|
channel.members
|
||||||
|
?.cast<SnChannelMember?>()
|
||||||
|
.map((ele) => ele?.accountId)
|
||||||
|
.where((ele) => ele != null)
|
||||||
|
.toSet() ??
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) setState(() => _channels = channels);
|
||||||
|
})
|
||||||
|
..onError((err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
})
|
||||||
|
..onDone(() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_refreshChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.chat, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
Expanded(
|
||||||
|
child: MediaQuery.removePadding(
|
||||||
|
context: context,
|
||||||
|
removeTop: true,
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _channels?.length ?? 0,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final channel = _channels![idx];
|
||||||
|
final lastMessage = _lastMessages?[channel.id];
|
||||||
|
|
||||||
|
if (channel.type == 1) {
|
||||||
|
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),
|
||||||
|
subtitle: lastMessage != null
|
||||||
|
? Text(
|
||||||
|
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'channelDirectMessageDescription'.tr(args: [
|
||||||
|
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
||||||
|
]),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
leading: AccountImage(
|
||||||
|
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'chatRoom',
|
||||||
|
pathParameters: {
|
||||||
|
'scope': channel.realm?.alias ?? 'global',
|
||||||
|
'alias': channel.alias,
|
||||||
|
},
|
||||||
|
).then((value) {
|
||||||
|
if (mounted) _refreshChannels();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(channel.name),
|
||||||
|
subtitle: lastMessage != null
|
||||||
|
? Text(
|
||||||
|
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
channel.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
leading: AccountImage(
|
||||||
|
content: null,
|
||||||
|
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
GoRouter.of(context)
|
||||||
|
.pushNamed(
|
||||||
|
'chatRoom',
|
||||||
|
pathParameters: {
|
||||||
|
'scope': channel.realm?.alias ?? 'global',
|
||||||
|
'alias': channel.alias,
|
||||||
|
},
|
||||||
|
extra: ChatRoomScreenExtra(
|
||||||
|
initialText: widget.value
|
||||||
|
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
||||||
|
.map((e) => e.path)
|
||||||
|
.join('\n'),
|
||||||
|
initialAttachments: widget.value
|
||||||
|
.where((e) =>
|
||||||
|
[SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
||||||
|
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then((value) {
|
||||||
|
if (value == true) _refreshChannels();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
464
lib/screens/stickers.dart
Normal file
464
lib/screens/stickers.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
266
lib/screens/stickers/pack_detail.dart
Normal file
266
lib/screens/stickers/pack_detail.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
279
lib/screens/wallet.dart
Normal file
279
lib/screens/wallet.dart
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/wallet.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 WalletScreen extends StatefulWidget {
|
||||||
|
const WalletScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WalletScreen> createState() => _WalletScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WalletScreenState extends State<WalletScreen> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
SnWallet? _wallet;
|
||||||
|
|
||||||
|
Future<void> _fetchWallet() async {
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/wa/wallets/me');
|
||||||
|
_wallet = SnWallet.fromJson(resp.data);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchWallet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: PageBackButton(),
|
||||||
|
title: Text('screenAccountWallet').tr(),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
if (_wallet == null)
|
||||||
|
Expanded(
|
||||||
|
child: _CreateWalletWidget(
|
||||||
|
onCreate: () {
|
||||||
|
_fetchWallet();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Card(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 28,
|
||||||
|
child: Icon(Symbols.wallet, size: 28),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
SizedBox(width: double.infinity),
|
||||||
|
Text(
|
||||||
|
NumberFormat.compactCurrency(
|
||||||
|
locale: EasyLocalization.of(context)!.currentLocale.toString(),
|
||||||
|
symbol: '${'walletCurrencyShort'.tr()} ',
|
||||||
|
decimalDigits: 2,
|
||||||
|
).format(double.parse(_wallet!.balance)),
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
Text('walletCurrency'.plural(double.parse(_wallet!.balance))),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, vertical: 24),
|
||||||
|
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||||
|
if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WalletTransactionList extends StatefulWidget {
|
||||||
|
final SnWallet myself;
|
||||||
|
|
||||||
|
const _WalletTransactionList({required this.myself});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_WalletTransactionList> createState() => _WalletTransactionListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WalletTransactionListState extends State<_WalletTransactionList> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
int? _totalCount;
|
||||||
|
final List<SnTransaction> _transactions = List.empty(growable: true);
|
||||||
|
|
||||||
|
Future<void> _fetchTransactions() async {
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/wa/transactions/me', queryParameters: {
|
||||||
|
'take': 10,
|
||||||
|
'offset': _transactions.length,
|
||||||
|
});
|
||||||
|
_totalCount = resp.data['count'];
|
||||||
|
_transactions.addAll(
|
||||||
|
resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? [],
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchTransactions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MediaQuery.removePadding(
|
||||||
|
context: context,
|
||||||
|
removeTop: true,
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: _fetchTransactions,
|
||||||
|
child: InfiniteList(
|
||||||
|
itemCount: _transactions.length,
|
||||||
|
isLoading: _isBusy,
|
||||||
|
hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!,
|
||||||
|
onFetchData: () {
|
||||||
|
_fetchTransactions();
|
||||||
|
},
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final ele = _transactions[idx];
|
||||||
|
final isIncoming = ele.payeeId == widget.myself.id;
|
||||||
|
return ListTile(
|
||||||
|
leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made),
|
||||||
|
title: Text(
|
||||||
|
'${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
|
||||||
|
style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(ele.remark),
|
||||||
|
const Gap(2),
|
||||||
|
Text(
|
||||||
|
DateFormat(
|
||||||
|
null,
|
||||||
|
EasyLocalization.of(context)!.currentLocale.toString(),
|
||||||
|
).format(ele.createdAt),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateWalletWidget extends StatefulWidget {
|
||||||
|
final Function()? onCreate;
|
||||||
|
|
||||||
|
const _CreateWalletWidget({required this.onCreate});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CreateWalletWidget> createState() => _CreateWalletWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Future<void> _createWallet() async {
|
||||||
|
final TextEditingController passwordController = TextEditingController();
|
||||||
|
final password = await showDialog<String?>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text('walletCreate').tr(),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('walletCreatePassword').tr(),
|
||||||
|
const Gap(8),
|
||||||
|
TextField(
|
||||||
|
autofocus: true,
|
||||||
|
obscureText: true,
|
||||||
|
controller: passwordController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'fieldPassword'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(ctx).pop(),
|
||||||
|
child: Text('cancel').tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(ctx).pop(passwordController.text);
|
||||||
|
},
|
||||||
|
child: Text('next').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
passwordController.dispose();
|
||||||
|
});
|
||||||
|
if (password == null || password.isEmpty) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.post('/cgi/wa/wallets/me', data: {
|
||||||
|
'password': password,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 380),
|
||||||
|
child: Card(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 28,
|
||||||
|
child: Icon(Symbols.add, size: 28),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||||
|
Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(),
|
||||||
|
const Gap(8),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _createWallet(),
|
||||||
|
child: Text('next').tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, vertical: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ Future<ThemeData> createAppTheme(
|
|||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
);
|
);
|
||||||
|
|
||||||
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||||
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
@@ -51,13 +51,13 @@ Future<ThemeData> createAppTheme(
|
|||||||
),
|
),
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
elevation: hasAppBarBlurry ? 0 : null,
|
elevation: hasAppBarTransparent ? 0 : null,
|
||||||
backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
|
backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
|
||||||
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
|
foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
pageTransitionsTheme: PageTransitionsTheme(
|
pageTransitionsTheme: PageTransitionsTheme(
|
||||||
builders: {
|
builders: {
|
||||||
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
|
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
||||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||||
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
|
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
|
||||||
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
|
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
|
|
||||||
part 'account.freezed.dart';
|
part 'account.freezed.dart';
|
||||||
part 'account.g.dart';
|
part 'account.g.dart';
|
||||||
@@ -9,18 +8,19 @@ class SnAccount with _$SnAccount {
|
|||||||
const SnAccount._();
|
const SnAccount._();
|
||||||
|
|
||||||
const factory SnAccount({
|
const factory SnAccount({
|
||||||
@HiveField(0) required int id,
|
required int id,
|
||||||
required DateTime createdAt,
|
required DateTime createdAt,
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
required DateTime? deletedAt,
|
required DateTime? deletedAt,
|
||||||
required DateTime? confirmedAt,
|
required DateTime? confirmedAt,
|
||||||
required List<SnAccountContact>? contacts,
|
required List<SnAccountContact>? contacts,
|
||||||
required String avatar,
|
@Default("") String avatar,
|
||||||
required String banner,
|
@Default("") String banner,
|
||||||
required String description,
|
required String description,
|
||||||
required String name,
|
required String name,
|
||||||
required String nick,
|
required String nick,
|
||||||
required Map<String, dynamic> permNodes,
|
@Default({}) Map<String, dynamic> permNodes,
|
||||||
|
required String language,
|
||||||
required SnAccountProfile? profile,
|
required SnAccountProfile? profile,
|
||||||
@Default([]) List<SnAccountBadge> badges,
|
@Default([]) List<SnAccountBadge> badges,
|
||||||
required DateTime? suspendedAt,
|
required DateTime? suspendedAt,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ SnAccount _$SnAccountFromJson(Map<String, dynamic> json) {
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnAccount {
|
mixin _$SnAccount {
|
||||||
@HiveField(0)
|
|
||||||
int get id => throw _privateConstructorUsedError;
|
int get id => throw _privateConstructorUsedError;
|
||||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||||
@@ -33,6 +32,7 @@ mixin _$SnAccount {
|
|||||||
String get name => throw _privateConstructorUsedError;
|
String get name => throw _privateConstructorUsedError;
|
||||||
String get nick => throw _privateConstructorUsedError;
|
String get nick => throw _privateConstructorUsedError;
|
||||||
Map<String, dynamic> get permNodes => throw _privateConstructorUsedError;
|
Map<String, dynamic> get permNodes => throw _privateConstructorUsedError;
|
||||||
|
String get language => throw _privateConstructorUsedError;
|
||||||
SnAccountProfile? get profile => throw _privateConstructorUsedError;
|
SnAccountProfile? get profile => throw _privateConstructorUsedError;
|
||||||
List<SnAccountBadge> get badges => throw _privateConstructorUsedError;
|
List<SnAccountBadge> get badges => throw _privateConstructorUsedError;
|
||||||
DateTime? get suspendedAt => throw _privateConstructorUsedError;
|
DateTime? get suspendedAt => throw _privateConstructorUsedError;
|
||||||
@@ -57,7 +57,7 @@ abstract class $SnAccountCopyWith<$Res> {
|
|||||||
_$SnAccountCopyWithImpl<$Res, SnAccount>;
|
_$SnAccountCopyWithImpl<$Res, SnAccount>;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@HiveField(0) int id,
|
{int id,
|
||||||
DateTime createdAt,
|
DateTime createdAt,
|
||||||
DateTime updatedAt,
|
DateTime updatedAt,
|
||||||
DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
@@ -69,6 +69,7 @@ abstract class $SnAccountCopyWith<$Res> {
|
|||||||
String name,
|
String name,
|
||||||
String nick,
|
String nick,
|
||||||
Map<String, dynamic> permNodes,
|
Map<String, dynamic> permNodes,
|
||||||
|
String language,
|
||||||
SnAccountProfile? profile,
|
SnAccountProfile? profile,
|
||||||
List<SnAccountBadge> badges,
|
List<SnAccountBadge> badges,
|
||||||
DateTime? suspendedAt,
|
DateTime? suspendedAt,
|
||||||
@@ -107,6 +108,7 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
|
|||||||
Object? name = null,
|
Object? name = null,
|
||||||
Object? nick = null,
|
Object? nick = null,
|
||||||
Object? permNodes = null,
|
Object? permNodes = null,
|
||||||
|
Object? language = null,
|
||||||
Object? profile = freezed,
|
Object? profile = freezed,
|
||||||
Object? badges = null,
|
Object? badges = null,
|
||||||
Object? suspendedAt = freezed,
|
Object? suspendedAt = freezed,
|
||||||
@@ -164,6 +166,10 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
|
|||||||
? _value.permNodes
|
? _value.permNodes
|
||||||
: permNodes // ignore: cast_nullable_to_non_nullable
|
: permNodes // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>,
|
as Map<String, dynamic>,
|
||||||
|
language: null == language
|
||||||
|
? _value.language
|
||||||
|
: language // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
profile: freezed == profile
|
profile: freezed == profile
|
||||||
? _value.profile
|
? _value.profile
|
||||||
: profile // ignore: cast_nullable_to_non_nullable
|
: profile // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -219,7 +225,7 @@ abstract class _$$SnAccountImplCopyWith<$Res>
|
|||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@HiveField(0) int id,
|
{int id,
|
||||||
DateTime createdAt,
|
DateTime createdAt,
|
||||||
DateTime updatedAt,
|
DateTime updatedAt,
|
||||||
DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
@@ -231,6 +237,7 @@ abstract class _$$SnAccountImplCopyWith<$Res>
|
|||||||
String name,
|
String name,
|
||||||
String nick,
|
String nick,
|
||||||
Map<String, dynamic> permNodes,
|
Map<String, dynamic> permNodes,
|
||||||
|
String language,
|
||||||
SnAccountProfile? profile,
|
SnAccountProfile? profile,
|
||||||
List<SnAccountBadge> badges,
|
List<SnAccountBadge> badges,
|
||||||
DateTime? suspendedAt,
|
DateTime? suspendedAt,
|
||||||
@@ -268,6 +275,7 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
|||||||
Object? name = null,
|
Object? name = null,
|
||||||
Object? nick = null,
|
Object? nick = null,
|
||||||
Object? permNodes = null,
|
Object? permNodes = null,
|
||||||
|
Object? language = null,
|
||||||
Object? profile = freezed,
|
Object? profile = freezed,
|
||||||
Object? badges = null,
|
Object? badges = null,
|
||||||
Object? suspendedAt = freezed,
|
Object? suspendedAt = freezed,
|
||||||
@@ -325,6 +333,10 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
|||||||
? _value._permNodes
|
? _value._permNodes
|
||||||
: permNodes // ignore: cast_nullable_to_non_nullable
|
: permNodes // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>,
|
as Map<String, dynamic>,
|
||||||
|
language: null == language
|
||||||
|
? _value.language
|
||||||
|
: language // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
profile: freezed == profile
|
profile: freezed == profile
|
||||||
? _value.profile
|
? _value.profile
|
||||||
: profile // ignore: cast_nullable_to_non_nullable
|
: profile // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -361,18 +373,19 @@ class __$$SnAccountImplCopyWithImpl<$Res>
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$SnAccountImpl extends _SnAccount {
|
class _$SnAccountImpl extends _SnAccount {
|
||||||
const _$SnAccountImpl(
|
const _$SnAccountImpl(
|
||||||
{@HiveField(0) required this.id,
|
{required this.id,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
required this.deletedAt,
|
required this.deletedAt,
|
||||||
required this.confirmedAt,
|
required this.confirmedAt,
|
||||||
required final List<SnAccountContact>? contacts,
|
required final List<SnAccountContact>? contacts,
|
||||||
required this.avatar,
|
this.avatar = "",
|
||||||
required this.banner,
|
this.banner = "",
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.nick,
|
required this.nick,
|
||||||
required final Map<String, dynamic> permNodes,
|
final Map<String, dynamic> permNodes = const {},
|
||||||
|
required this.language,
|
||||||
required this.profile,
|
required this.profile,
|
||||||
final List<SnAccountBadge> badges = const [],
|
final List<SnAccountBadge> badges = const [],
|
||||||
required this.suspendedAt,
|
required this.suspendedAt,
|
||||||
@@ -389,7 +402,6 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
_$$SnAccountImplFromJson(json);
|
_$$SnAccountImplFromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@HiveField(0)
|
|
||||||
final int id;
|
final int id;
|
||||||
@override
|
@override
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
@@ -410,8 +422,10 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@JsonKey()
|
||||||
final String avatar;
|
final String avatar;
|
||||||
@override
|
@override
|
||||||
|
@JsonKey()
|
||||||
final String banner;
|
final String banner;
|
||||||
@override
|
@override
|
||||||
final String description;
|
final String description;
|
||||||
@@ -421,12 +435,15 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
final String nick;
|
final String nick;
|
||||||
final Map<String, dynamic> _permNodes;
|
final Map<String, dynamic> _permNodes;
|
||||||
@override
|
@override
|
||||||
|
@JsonKey()
|
||||||
Map<String, dynamic> get permNodes {
|
Map<String, dynamic> get permNodes {
|
||||||
if (_permNodes is EqualUnmodifiableMapView) return _permNodes;
|
if (_permNodes is EqualUnmodifiableMapView) return _permNodes;
|
||||||
// ignore: implicit_dynamic_type
|
// ignore: implicit_dynamic_type
|
||||||
return EqualUnmodifiableMapView(_permNodes);
|
return EqualUnmodifiableMapView(_permNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String language;
|
||||||
@override
|
@override
|
||||||
final SnAccountProfile? profile;
|
final SnAccountProfile? profile;
|
||||||
final List<SnAccountBadge> _badges;
|
final List<SnAccountBadge> _badges;
|
||||||
@@ -451,7 +468,7 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -477,6 +494,8 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
(identical(other.nick, nick) || other.nick == nick) &&
|
(identical(other.nick, nick) || other.nick == nick) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other._permNodes, _permNodes) &&
|
.equals(other._permNodes, _permNodes) &&
|
||||||
|
(identical(other.language, language) ||
|
||||||
|
other.language == language) &&
|
||||||
(identical(other.profile, profile) || other.profile == profile) &&
|
(identical(other.profile, profile) || other.profile == profile) &&
|
||||||
const DeepCollectionEquality().equals(other._badges, _badges) &&
|
const DeepCollectionEquality().equals(other._badges, _badges) &&
|
||||||
(identical(other.suspendedAt, suspendedAt) ||
|
(identical(other.suspendedAt, suspendedAt) ||
|
||||||
@@ -507,6 +526,7 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
name,
|
name,
|
||||||
nick,
|
nick,
|
||||||
const DeepCollectionEquality().hash(_permNodes),
|
const DeepCollectionEquality().hash(_permNodes),
|
||||||
|
language,
|
||||||
profile,
|
profile,
|
||||||
const DeepCollectionEquality().hash(_badges),
|
const DeepCollectionEquality().hash(_badges),
|
||||||
suspendedAt,
|
suspendedAt,
|
||||||
@@ -534,18 +554,19 @@ class _$SnAccountImpl extends _SnAccount {
|
|||||||
|
|
||||||
abstract class _SnAccount extends SnAccount {
|
abstract class _SnAccount extends SnAccount {
|
||||||
const factory _SnAccount(
|
const factory _SnAccount(
|
||||||
{@HiveField(0) required final int id,
|
{required final int id,
|
||||||
required final DateTime createdAt,
|
required final DateTime createdAt,
|
||||||
required final DateTime updatedAt,
|
required final DateTime updatedAt,
|
||||||
required final DateTime? deletedAt,
|
required final DateTime? deletedAt,
|
||||||
required final DateTime? confirmedAt,
|
required final DateTime? confirmedAt,
|
||||||
required final List<SnAccountContact>? contacts,
|
required final List<SnAccountContact>? contacts,
|
||||||
required final String avatar,
|
final String avatar,
|
||||||
required final String banner,
|
final String banner,
|
||||||
required final String description,
|
required final String description,
|
||||||
required final String name,
|
required final String name,
|
||||||
required final String nick,
|
required final String nick,
|
||||||
required final Map<String, dynamic> permNodes,
|
final Map<String, dynamic> permNodes,
|
||||||
|
required final String language,
|
||||||
required final SnAccountProfile? profile,
|
required final SnAccountProfile? profile,
|
||||||
final List<SnAccountBadge> badges,
|
final List<SnAccountBadge> badges,
|
||||||
required final DateTime? suspendedAt,
|
required final DateTime? suspendedAt,
|
||||||
@@ -559,7 +580,6 @@ abstract class _SnAccount extends SnAccount {
|
|||||||
_$SnAccountImpl.fromJson;
|
_$SnAccountImpl.fromJson;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@HiveField(0)
|
|
||||||
int get id;
|
int get id;
|
||||||
@override
|
@override
|
||||||
DateTime get createdAt;
|
DateTime get createdAt;
|
||||||
@@ -584,6 +604,8 @@ abstract class _SnAccount extends SnAccount {
|
|||||||
@override
|
@override
|
||||||
Map<String, dynamic> get permNodes;
|
Map<String, dynamic> get permNodes;
|
||||||
@override
|
@override
|
||||||
|
String get language;
|
||||||
|
@override
|
||||||
SnAccountProfile? get profile;
|
SnAccountProfile? get profile;
|
||||||
@override
|
@override
|
||||||
List<SnAccountBadge> get badges;
|
List<SnAccountBadge> get badges;
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
|
|||||||
contacts: (json['contacts'] as List<dynamic>?)
|
contacts: (json['contacts'] as List<dynamic>?)
|
||||||
?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
|
?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
avatar: json['avatar'] as String,
|
avatar: json['avatar'] as String? ?? "",
|
||||||
banner: json['banner'] as String,
|
banner: json['banner'] as String? ?? "",
|
||||||
description: json['description'] as String,
|
description: json['description'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
nick: json['nick'] as String,
|
nick: json['nick'] as String,
|
||||||
permNodes: json['perm_nodes'] as Map<String, dynamic>,
|
permNodes: json['perm_nodes'] as Map<String, dynamic>? ?? const {},
|
||||||
|
language: json['language'] as String,
|
||||||
profile: json['profile'] == null
|
profile: json['profile'] == null
|
||||||
? null
|
? null
|
||||||
: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
|
: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
|
||||||
@@ -56,6 +57,7 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
|
|||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'nick': instance.nick,
|
'nick': instance.nick,
|
||||||
'perm_nodes': instance.permNodes,
|
'perm_nodes': instance.permNodes,
|
||||||
|
'language': instance.language,
|
||||||
'profile': instance.profile?.toJson(),
|
'profile': instance.profile?.toJson(),
|
||||||
'badges': instance.badges.map((e) => e.toJson()).toList(),
|
'badges': instance.badges.map((e) => e.toJson()).toList(),
|
||||||
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
||||||
|
|||||||
@@ -177,3 +177,14 @@ class SnStickerPack with _$SnStickerPack {
|
|||||||
|
|
||||||
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json);
|
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnAttachmentBilling with _$SnAttachmentBilling {
|
||||||
|
const factory SnAttachmentBilling({
|
||||||
|
required int currentBytes,
|
||||||
|
required int discountFileSize,
|
||||||
|
required double includedRatio,
|
||||||
|
}) = _SnAttachmentBilling;
|
||||||
|
|
||||||
|
factory SnAttachmentBilling.fromJson(Map<String, Object?> json) => _$SnAttachmentBillingFromJson(json);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3007,3 +3007,195 @@ abstract class _SnStickerPack implements SnStickerPack {
|
|||||||
_$$SnStickerPackImplCopyWith<_$SnStickerPackImpl> get copyWith =>
|
_$$SnStickerPackImplCopyWith<_$SnStickerPackImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SnAttachmentBilling _$SnAttachmentBillingFromJson(Map<String, dynamic> json) {
|
||||||
|
return _SnAttachmentBilling.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnAttachmentBilling {
|
||||||
|
int get currentBytes => throw _privateConstructorUsedError;
|
||||||
|
int get discountFileSize => throw _privateConstructorUsedError;
|
||||||
|
double get includedRatio => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this SnAttachmentBilling to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of SnAttachmentBilling
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$SnAttachmentBillingCopyWith<SnAttachmentBilling> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $SnAttachmentBillingCopyWith<$Res> {
|
||||||
|
factory $SnAttachmentBillingCopyWith(
|
||||||
|
SnAttachmentBilling value, $Res Function(SnAttachmentBilling) then) =
|
||||||
|
_$SnAttachmentBillingCopyWithImpl<$Res, SnAttachmentBilling>;
|
||||||
|
@useResult
|
||||||
|
$Res call({int currentBytes, int discountFileSize, double includedRatio});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnAttachmentBillingCopyWithImpl<$Res, $Val extends SnAttachmentBilling>
|
||||||
|
implements $SnAttachmentBillingCopyWith<$Res> {
|
||||||
|
_$SnAttachmentBillingCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnAttachmentBilling
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? currentBytes = null,
|
||||||
|
Object? discountFileSize = null,
|
||||||
|
Object? includedRatio = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
currentBytes: null == currentBytes
|
||||||
|
? _value.currentBytes
|
||||||
|
: currentBytes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
discountFileSize: null == discountFileSize
|
||||||
|
? _value.discountFileSize
|
||||||
|
: discountFileSize // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
includedRatio: null == includedRatio
|
||||||
|
? _value.includedRatio
|
||||||
|
: includedRatio // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$SnAttachmentBillingImplCopyWith<$Res>
|
||||||
|
implements $SnAttachmentBillingCopyWith<$Res> {
|
||||||
|
factory _$$SnAttachmentBillingImplCopyWith(_$SnAttachmentBillingImpl value,
|
||||||
|
$Res Function(_$SnAttachmentBillingImpl) then) =
|
||||||
|
__$$SnAttachmentBillingImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({int currentBytes, int discountFileSize, double includedRatio});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$SnAttachmentBillingImplCopyWithImpl<$Res>
|
||||||
|
extends _$SnAttachmentBillingCopyWithImpl<$Res, _$SnAttachmentBillingImpl>
|
||||||
|
implements _$$SnAttachmentBillingImplCopyWith<$Res> {
|
||||||
|
__$$SnAttachmentBillingImplCopyWithImpl(_$SnAttachmentBillingImpl _value,
|
||||||
|
$Res Function(_$SnAttachmentBillingImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of SnAttachmentBilling
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? currentBytes = null,
|
||||||
|
Object? discountFileSize = null,
|
||||||
|
Object? includedRatio = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$SnAttachmentBillingImpl(
|
||||||
|
currentBytes: null == currentBytes
|
||||||
|
? _value.currentBytes
|
||||||
|
: currentBytes // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
discountFileSize: null == discountFileSize
|
||||||
|
? _value.discountFileSize
|
||||||
|
: discountFileSize // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
includedRatio: null == includedRatio
|
||||||
|
? _value.includedRatio
|
||||||
|
: includedRatio // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$SnAttachmentBillingImpl implements _SnAttachmentBilling {
|
||||||
|
const _$SnAttachmentBillingImpl(
|
||||||
|
{required this.currentBytes,
|
||||||
|
required this.discountFileSize,
|
||||||
|
required this.includedRatio});
|
||||||
|
|
||||||
|
factory _$SnAttachmentBillingImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$SnAttachmentBillingImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final int currentBytes;
|
||||||
|
@override
|
||||||
|
final int discountFileSize;
|
||||||
|
@override
|
||||||
|
final double includedRatio;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnAttachmentBilling(currentBytes: $currentBytes, discountFileSize: $discountFileSize, includedRatio: $includedRatio)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$SnAttachmentBillingImpl &&
|
||||||
|
(identical(other.currentBytes, currentBytes) ||
|
||||||
|
other.currentBytes == currentBytes) &&
|
||||||
|
(identical(other.discountFileSize, discountFileSize) ||
|
||||||
|
other.discountFileSize == discountFileSize) &&
|
||||||
|
(identical(other.includedRatio, includedRatio) ||
|
||||||
|
other.includedRatio == includedRatio));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(runtimeType, currentBytes, discountFileSize, includedRatio);
|
||||||
|
|
||||||
|
/// Create a copy of SnAttachmentBilling
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$SnAttachmentBillingImplCopyWith<_$SnAttachmentBillingImpl> get copyWith =>
|
||||||
|
__$$SnAttachmentBillingImplCopyWithImpl<_$SnAttachmentBillingImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$SnAttachmentBillingImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _SnAttachmentBilling implements SnAttachmentBilling {
|
||||||
|
const factory _SnAttachmentBilling(
|
||||||
|
{required final int currentBytes,
|
||||||
|
required final int discountFileSize,
|
||||||
|
required final double includedRatio}) = _$SnAttachmentBillingImpl;
|
||||||
|
|
||||||
|
factory _SnAttachmentBilling.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$SnAttachmentBillingImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get currentBytes;
|
||||||
|
@override
|
||||||
|
int get discountFileSize;
|
||||||
|
@override
|
||||||
|
double get includedRatio;
|
||||||
|
|
||||||
|
/// Create a copy of SnAttachmentBilling
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$SnAttachmentBillingImplCopyWith<_$SnAttachmentBillingImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|||||||
@@ -281,3 +281,19 @@ Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) =>
|
|||||||
'stickers': instance.stickers?.map((e) => e.toJson()).toList(),
|
'stickers': instance.stickers?.map((e) => e.toJson()).toList(),
|
||||||
'account_id': instance.accountId,
|
'account_id': instance.accountId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_$SnAttachmentBillingImpl _$$SnAttachmentBillingImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SnAttachmentBillingImpl(
|
||||||
|
currentBytes: (json['current_bytes'] as num).toInt(),
|
||||||
|
discountFileSize: (json['discount_file_size'] as num).toInt(),
|
||||||
|
includedRatio: (json['included_ratio'] as num).toDouble(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnAttachmentBillingImplToJson(
|
||||||
|
_$SnAttachmentBillingImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'current_bytes': instance.currentBytes,
|
||||||
|
'discount_file_size': instance.discountFileSize,
|
||||||
|
'included_ratio': instance.includedRatio,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
@@ -12,23 +11,22 @@ part 'chat.g.dart';
|
|||||||
class SnChannel with _$SnChannel {
|
class SnChannel with _$SnChannel {
|
||||||
const SnChannel._();
|
const SnChannel._();
|
||||||
|
|
||||||
@HiveType(typeId: 2)
|
|
||||||
const factory SnChannel({
|
const factory SnChannel({
|
||||||
@HiveField(0) required int id,
|
required int id,
|
||||||
@HiveField(1) required DateTime createdAt,
|
required DateTime createdAt,
|
||||||
@HiveField(2) required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
@HiveField(3) required dynamic deletedAt,
|
required dynamic deletedAt,
|
||||||
@HiveField(4) required String alias,
|
required String alias,
|
||||||
@HiveField(5) required String name,
|
required String name,
|
||||||
@HiveField(6) required String description,
|
required String description,
|
||||||
@HiveField(7) required List<SnChannelMember>? members,
|
required List<SnChannelMember>? members,
|
||||||
List<SnChatMessage>? messages,
|
List<SnChatMessage>? messages,
|
||||||
@HiveField(8) required int type,
|
required int type,
|
||||||
@HiveField(9) required int accountId,
|
required int accountId,
|
||||||
@HiveField(10) required SnRealm? realm,
|
required SnRealm? realm,
|
||||||
@HiveField(11) required int? realmId,
|
required int? realmId,
|
||||||
@HiveField(12) required bool isPublic,
|
required bool isPublic,
|
||||||
@HiveField(13) required bool isCommunity,
|
required bool isCommunity,
|
||||||
}) = _SnChannel;
|
}) = _SnChannel;
|
||||||
|
|
||||||
factory SnChannel.fromJson(Map<String, dynamic> json) =>
|
factory SnChannel.fromJson(Map<String, dynamic> json) =>
|
||||||
@@ -42,19 +40,18 @@ class SnChannel with _$SnChannel {
|
|||||||
class SnChannelMember with _$SnChannelMember {
|
class SnChannelMember with _$SnChannelMember {
|
||||||
const SnChannelMember._();
|
const SnChannelMember._();
|
||||||
|
|
||||||
@HiveType(typeId: 3)
|
|
||||||
const factory SnChannelMember({
|
const factory SnChannelMember({
|
||||||
@HiveField(0) required int id,
|
required int id,
|
||||||
@HiveField(1) required DateTime createdAt,
|
required DateTime createdAt,
|
||||||
@HiveField(2) required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
@HiveField(3) required DateTime? deletedAt,
|
required DateTime? deletedAt,
|
||||||
@HiveField(4) required int channelId,
|
required int channelId,
|
||||||
@HiveField(5) required int accountId,
|
required int accountId,
|
||||||
@HiveField(6) required String? nick,
|
required String? nick,
|
||||||
@HiveField(7) required SnChannel? channel,
|
required SnChannel? channel,
|
||||||
@HiveField(8) required SnAccount? account,
|
required SnAccount? account,
|
||||||
@Default(0) int notify,
|
@Default(0) int notify,
|
||||||
@HiveField(9) required int powerLevel,
|
required int powerLevel,
|
||||||
dynamic calls,
|
dynamic calls,
|
||||||
dynamic events,
|
dynamic events,
|
||||||
}) = _SnChannelMember;
|
}) = _SnChannelMember;
|
||||||
@@ -67,21 +64,20 @@ class SnChannelMember with _$SnChannelMember {
|
|||||||
class SnChatMessage with _$SnChatMessage {
|
class SnChatMessage with _$SnChatMessage {
|
||||||
const SnChatMessage._();
|
const SnChatMessage._();
|
||||||
|
|
||||||
@HiveType(typeId: 4)
|
|
||||||
const factory SnChatMessage({
|
const factory SnChatMessage({
|
||||||
@HiveField(0) required int id,
|
required int id,
|
||||||
@HiveField(1) required DateTime createdAt,
|
required DateTime createdAt,
|
||||||
@HiveField(2) required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
@HiveField(3) required DateTime? deletedAt,
|
required DateTime? deletedAt,
|
||||||
@HiveField(4) required String uuid,
|
required String uuid,
|
||||||
@HiveField(5) @Default({}) Map<String, dynamic> body,
|
@Default({}) Map<String, dynamic> body,
|
||||||
@HiveField(6) required String type,
|
required String type,
|
||||||
@HiveField(7) required SnChannel channel,
|
required SnChannel channel,
|
||||||
@HiveField(8) required SnChannelMember sender,
|
required SnChannelMember sender,
|
||||||
@HiveField(9) required int channelId,
|
required int channelId,
|
||||||
@HiveField(10) required int senderId,
|
required int senderId,
|
||||||
@HiveField(11) required int? quoteEventId,
|
required int? quoteEventId,
|
||||||
@HiveField(12) required int? relatedEventId,
|
required int? relatedEventId,
|
||||||
SnChatMessagePreload? preload,
|
SnChatMessagePreload? preload,
|
||||||
}) = _SnChatMessage;
|
}) = _SnChatMessage;
|
||||||
|
|
||||||
|
|||||||
@@ -20,34 +20,20 @@ SnChannel _$SnChannelFromJson(Map<String, dynamic> json) {
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnChannel {
|
mixin _$SnChannel {
|
||||||
@HiveField(0)
|
|
||||||
int get id => throw _privateConstructorUsedError;
|
int get id => throw _privateConstructorUsedError;
|
||||||
@HiveField(1)
|
|
||||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||||
@HiveField(2)
|
|
||||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||||
@HiveField(3)
|
|
||||||
dynamic get deletedAt => throw _privateConstructorUsedError;
|
dynamic get deletedAt => throw _privateConstructorUsedError;
|
||||||
@HiveField(4)
|
|
||||||
String get alias => throw _privateConstructorUsedError;
|
String get alias => throw _privateConstructorUsedError;
|
||||||
@HiveField(5)
|
|
||||||
String get name => throw _privateConstructorUsedError;
|
String get name => throw _privateConstructorUsedError;
|
||||||
@HiveField(6)
|
|
||||||
String get description => throw _privateConstructorUsedError;
|
String get description => throw _privateConstructorUsedError;
|
||||||
@HiveField(7)
|
|
||||||
List<SnChannelMember>? get members => throw _privateConstructorUsedError;
|
List<SnChannelMember>? get members => throw _privateConstructorUsedError;
|
||||||
List<SnChatMessage>? get messages => throw _privateConstructorUsedError;
|
List<SnChatMessage>? get messages => throw _privateConstructorUsedError;
|
||||||
@HiveField(8)
|
|
||||||
int get type => throw _privateConstructorUsedError;
|
int get type => throw _privateConstructorUsedError;
|
||||||
@HiveField(9)
|
|
||||||
int get accountId => throw _privateConstructorUsedError;
|
int get accountId => throw _privateConstructorUsedError;
|
||||||
@HiveField(10)
|
|
||||||
SnRealm? get realm => throw _privateConstructorUsedError;
|
SnRealm? get realm => throw _privateConstructorUsedError;
|
||||||
@HiveField(11)
|
|
||||||
int? get realmId => throw _privateConstructorUsedError;
|
int? get realmId => throw _privateConstructorUsedError;
|
||||||
@HiveField(12)
|
|
||||||
bool get isPublic => throw _privateConstructorUsedError;
|
bool get isPublic => throw _privateConstructorUsedError;
|
||||||
@HiveField(13)
|
|
||||||
bool get isCommunity => throw _privateConstructorUsedError;
|
bool get isCommunity => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this SnChannel to a JSON map.
|
/// Serializes this SnChannel to a JSON map.
|
||||||
@@ -66,21 +52,21 @@ abstract class $SnChannelCopyWith<$Res> {
|
|||||||
_$SnChannelCopyWithImpl<$Res, SnChannel>;
|
_$SnChannelCopyWithImpl<$Res, SnChannel>;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@HiveField(0) int id,
|
{int id,
|
||||||
@HiveField(1) DateTime createdAt,
|
DateTime createdAt,
|
||||||
@HiveField(2) DateTime updatedAt,
|
DateTime updatedAt,
|
||||||
@HiveField(3) dynamic deletedAt,
|
dynamic deletedAt,
|
||||||
@HiveField(4) String alias,
|
String alias,
|
||||||
@HiveField(5) String name,
|
String name,
|
||||||
@HiveField(6) String description,
|
String description,
|
||||||
@HiveField(7) List<SnChannelMember>? members,
|
List<SnChannelMember>? members,
|
||||||
List<SnChatMessage>? messages,
|
List<SnChatMessage>? messages,
|
||||||
@HiveField(8) int type,
|
int type,
|
||||||
@HiveField(9) int accountId,
|
int accountId,
|
||||||
@HiveField(10) SnRealm? realm,
|
SnRealm? realm,
|
||||||
@HiveField(11) int? realmId,
|
int? realmId,
|
||||||
@HiveField(12) bool isPublic,
|
bool isPublic,
|
||||||
@HiveField(13) bool isCommunity});
|
bool isCommunity});
|
||||||
|
|
||||||
$SnRealmCopyWith<$Res>? get realm;
|
$SnRealmCopyWith<$Res>? get realm;
|
||||||
}
|
}
|
||||||
@@ -204,21 +190,21 @@ abstract class _$$SnChannelImplCopyWith<$Res>
|
|||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@HiveField(0) int id,
|
{int id,
|
||||||
@HiveField(1) DateTime createdAt,
|
DateTime createdAt,
|
||||||
@HiveField(2) DateTime updatedAt,
|
DateTime updatedAt,
|
||||||
@HiveField(3) dynamic deletedAt,
|
dynamic deletedAt,
|
||||||
@HiveField(4) String alias,
|
String alias,
|
||||||
@HiveField(5) String name,
|
String name,
|
||||||
@HiveField(6) String description,
|
String description,
|
||||||
@HiveField(7) List<SnChannelMember>? members,
|
List<SnChannelMember>? members,
|
||||||
List<SnChatMessage>? messages,
|
List<SnChatMessage>? messages,
|
||||||
@HiveField(8) int type,
|
int type,
|
||||||
@HiveField(9) int accountId,
|
int accountId,
|
||||||
@HiveField(10) SnRealm? realm,
|
SnRealm? realm,
|
||||||
@HiveField(11) int? realmId,
|
int? realmId,
|
||||||
@HiveField(12) bool isPublic,
|
bool isPublic,
|
||||||
@HiveField(13) bool isCommunity});
|
bool isCommunity});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
$SnRealmCopyWith<$Res>? get realm;
|
$SnRealmCopyWith<$Res>? get realm;
|
||||||
@@ -320,24 +306,23 @@ class __$$SnChannelImplCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@HiveType(typeId: 2)
|
|
||||||
class _$SnChannelImpl extends _SnChannel {
|
class _$SnChannelImpl extends _SnChannel {
|
||||||
const _$SnChannelImpl(
|
const _$SnChannelImpl(
|
||||||
{@HiveField(0) required this.id,
|
{required this.id,
|
||||||
@HiveField(1) required this.createdAt,
|
required this.createdAt,
|
||||||
@HiveField(2) required this.updatedAt,
|
required this.updatedAt,
|
||||||
@HiveField(3) required this.deletedAt,
|
required this.deletedAt,
|
||||||
@HiveField(4) required this.alias,
|
required this.alias,
|
||||||
@HiveField(5) required this.name,
|
required this.name,
|
||||||
@HiveField(6) required this.description,
|
required this.description,
|
||||||
@HiveField(7) required final List<SnChannelMember>? members,
|
required final List<SnChannelMember>? members,
|
||||||
final List<SnChatMessage>? messages,
|
final List<SnChatMessage>? messages,
|
||||||
@HiveField(8) required this.type,
|
required this.type,
|
||||||
@HiveField(9) required this.accountId,
|
required this.accountId,
|
||||||
@HiveField(10) required this.realm,
|
required this.realm,
|
||||||
@HiveField(11) required this.realmId,
|
required this.realmId,
|
||||||
@HiveField(12) required this.isPublic,
|
required this.isPublic,
|
||||||
@HiveField(13) required this.isCommunity})
|
required this.isCommunity})
|
||||||
: _members = members,
|
: _members = members,
|
||||||
_messages = messages,
|
_messages = messages,
|
||||||
super._();
|
super._();
|
||||||
@@ -346,29 +331,21 @@ class _$SnChannelImpl extends _SnChannel {
|
|||||||
_$$SnChannelImplFromJson(json);
|
_$$SnChannelImplFromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@HiveField(0)
|
|
||||||
final int id;
|
final int id;
|
||||||
@override
|
@override
|
||||||
@HiveField(1)
|
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(2)
|
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(3)
|
|
||||||
final dynamic deletedAt;
|
final dynamic deletedAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(4)
|
|
||||||
final String alias;
|
final String alias;
|
||||||
@override
|
@override
|
||||||
@HiveField(5)
|
|
||||||
final String name;
|
final String name;
|
||||||
@override
|
@override
|
||||||
@HiveField(6)
|
|
||||||
final String description;
|
final String description;
|
||||||
final List<SnChannelMember>? _members;
|
final List<SnChannelMember>? _members;
|
||||||
@override
|
@override
|
||||||
@HiveField(7)
|
|
||||||
List<SnChannelMember>? get members {
|
List<SnChannelMember>? get members {
|
||||||
final value = _members;
|
final value = _members;
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
@@ -388,22 +365,16 @@ class _$SnChannelImpl extends _SnChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@HiveField(8)
|
|
||||||
final int type;
|
final int type;
|
||||||
@override
|
@override
|
||||||
@HiveField(9)
|
|
||||||
final int accountId;
|
final int accountId;
|
||||||
@override
|
@override
|
||||||
@HiveField(10)
|
|
||||||
final SnRealm? realm;
|
final SnRealm? realm;
|
||||||
@override
|
@override
|
||||||
@HiveField(11)
|
|
||||||
final int? realmId;
|
final int? realmId;
|
||||||
@override
|
@override
|
||||||
@HiveField(12)
|
|
||||||
final bool isPublic;
|
final bool isPublic;
|
||||||
@override
|
@override
|
||||||
@HiveField(13)
|
|
||||||
final bool isCommunity;
|
final bool isCommunity;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -477,69 +448,55 @@ class _$SnChannelImpl extends _SnChannel {
|
|||||||
|
|
||||||
abstract class _SnChannel extends SnChannel {
|
abstract class _SnChannel extends SnChannel {
|
||||||
const factory _SnChannel(
|
const factory _SnChannel(
|
||||||
{@HiveField(0) required final int id,
|
{required final int id,
|
||||||
@HiveField(1) required final DateTime createdAt,
|
required final DateTime createdAt,
|
||||||
@HiveField(2) required final DateTime updatedAt,
|
required final DateTime updatedAt,
|
||||||
@HiveField(3) required final dynamic deletedAt,
|
required final dynamic deletedAt,
|
||||||
@HiveField(4) required final String alias,
|
required final String alias,
|
||||||
@HiveField(5) required final String name,
|
required final String name,
|
||||||
@HiveField(6) required final String description,
|
required final String description,
|
||||||
@HiveField(7) required final List<SnChannelMember>? members,
|
required final List<SnChannelMember>? members,
|
||||||
final List<SnChatMessage>? messages,
|
final List<SnChatMessage>? messages,
|
||||||
@HiveField(8) required final int type,
|
required final int type,
|
||||||
@HiveField(9) required final int accountId,
|
required final int accountId,
|
||||||
@HiveField(10) required final SnRealm? realm,
|
required final SnRealm? realm,
|
||||||
@HiveField(11) required final int? realmId,
|
required final int? realmId,
|
||||||
@HiveField(12) required final bool isPublic,
|
required final bool isPublic,
|
||||||
@HiveField(13) required final bool isCommunity}) = _$SnChannelImpl;
|
required final bool isCommunity}) = _$SnChannelImpl;
|
||||||
const _SnChannel._() : super._();
|
const _SnChannel._() : super._();
|
||||||
|
|
||||||
factory _SnChannel.fromJson(Map<String, dynamic> json) =
|
factory _SnChannel.fromJson(Map<String, dynamic> json) =
|
||||||
_$SnChannelImpl.fromJson;
|
_$SnChannelImpl.fromJson;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@HiveField(0)
|
|
||||||
int get id;
|
int get id;
|
||||||
@override
|
@override
|
||||||
@HiveField(1)
|
|
||||||
DateTime get createdAt;
|
DateTime get createdAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(2)
|
|
||||||
DateTime get updatedAt;
|
DateTime get updatedAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(3)
|
|
||||||
dynamic get deletedAt;
|
dynamic get deletedAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(4)
|
|
||||||
String get alias;
|
String get alias;
|
||||||
@override
|
@override
|
||||||
@HiveField(5)
|
|
||||||
String get name;
|
String get name;
|
||||||
@override
|
@override
|
||||||
@HiveField(6)
|
|
||||||
String get description;
|
String get description;
|
||||||
@override
|
@override
|
||||||
@HiveField(7)
|
|
||||||
List<SnChannelMember>? get members;
|
List<SnChannelMember>? get members;
|
||||||
@override
|
@override
|
||||||
List<SnChatMessage>? get messages;
|
List<SnChatMessage>? get messages;
|
||||||
@override
|
@override
|
||||||
@HiveField(8)
|
|
||||||
int get type;
|
int get type;
|
||||||
@override
|
@override
|
||||||
@HiveField(9)
|
|
||||||
int get accountId;
|
int get accountId;
|
||||||
@override
|
@override
|
||||||
@HiveField(10)
|
|
||||||
SnRealm? get realm;
|
SnRealm? get realm;
|
||||||
@override
|
@override
|
||||||
@HiveField(11)
|
|
||||||
int? get realmId;
|
int? get realmId;
|
||||||
@override
|
@override
|
||||||
@HiveField(12)
|
|
||||||
bool get isPublic;
|
bool get isPublic;
|
||||||
@override
|
@override
|
||||||
@HiveField(13)
|
|
||||||
bool get isCommunity;
|
bool get isCommunity;
|
||||||
|
|
||||||
/// Create a copy of SnChannel
|
/// Create a copy of SnChannel
|
||||||
@@ -556,26 +513,16 @@ SnChannelMember _$SnChannelMemberFromJson(Map<String, dynamic> json) {
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnChannelMember {
|
mixin _$SnChannelMember {
|
||||||
@HiveField(0)
|
|
||||||
int get id => throw _privateConstructorUsedError;
|
int get id => throw _privateConstructorUsedError;
|
||||||
@HiveField(1)
|
|
||||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||||
@HiveField(2)
|
|
||||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||||
@HiveField(3)
|
|
||||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||||
@HiveField(4)
|
|
||||||
int get channelId => throw _privateConstructorUsedError;
|
int get channelId => throw _privateConstructorUsedError;
|
||||||
@HiveField(5)
|
|
||||||
int get accountId => throw _privateConstructorUsedError;
|
int get accountId => throw _privateConstructorUsedError;
|
||||||
@HiveField(6)
|
|
||||||
String? get nick => throw _privateConstructorUsedError;
|
String? get nick => throw _privateConstructorUsedError;
|
||||||
@HiveField(7)
|
|
||||||
SnChannel? get channel => throw _privateConstructorUsedError;
|
SnChannel? get channel => throw _privateConstructorUsedError;
|
||||||
@HiveField(8)
|
|
||||||
SnAccount? get account => throw _privateConstructorUsedError;
|
SnAccount? get account => throw _privateConstructorUsedError;
|
||||||
int get notify => throw _privateConstructorUsedError;
|
int get notify => throw _privateConstructorUsedError;
|
||||||
@HiveField(9)
|
|
||||||
int get powerLevel => throw _privateConstructorUsedError;
|
int get powerLevel => throw _privateConstructorUsedError;
|
||||||
dynamic get calls => throw _privateConstructorUsedError;
|
dynamic get calls => throw _privateConstructorUsedError;
|
||||||
dynamic get events => throw _privateConstructorUsedError;
|
dynamic get events => throw _privateConstructorUsedError;
|
||||||
@@ -597,17 +544,17 @@ abstract class $SnChannelMemberCopyWith<$Res> {
|
|||||||
_$SnChannelMemberCopyWithImpl<$Res, SnChannelMember>;
|
_$SnChannelMemberCopyWithImpl<$Res, SnChannelMember>;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@HiveField(0) int id,
|
{int id,
|
||||||
@HiveField(1) DateTime createdAt,
|
DateTime createdAt,
|
||||||
@HiveField(2) DateTime updatedAt,
|
DateTime updatedAt,
|
||||||
@HiveField(3) DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
@HiveField(4) int channelId,
|
int channelId,
|
||||||
@HiveField(5) int accountId,
|
int accountId,
|
||||||
@HiveField(6) String? nick,
|
String? nick,
|
||||||
@HiveField(7) SnChannel? channel,
|
SnChannel? channel,
|
||||||
@HiveField(8) SnAccount? account,
|
SnAccount? account,
|
||||||
int notify,
|
int notify,
|
||||||
@HiveField(9) int powerLevel,
|
int powerLevel,
|
||||||
dynamic calls,
|
dynamic calls,
|
||||||
dynamic events});
|
dynamic events});
|
||||||
|
|
||||||
@@ -738,17 +685,17 @@ abstract class _$$SnChannelMemberImplCopyWith<$Res>
|
|||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@HiveField(0) int id,
|
{int id,
|
||||||
@HiveField(1) DateTime createdAt,
|
DateTime createdAt,
|
||||||
@HiveField(2) DateTime updatedAt,
|
DateTime updatedAt,
|
||||||
@HiveField(3) DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
@HiveField(4) int channelId,
|
int channelId,
|
||||||
@HiveField(5) int accountId,
|
int accountId,
|
||||||
@HiveField(6) String? nick,
|
String? nick,
|
||||||
@HiveField(7) SnChannel? channel,
|
SnChannel? channel,
|
||||||
@HiveField(8) SnAccount? account,
|
SnAccount? account,
|
||||||
int notify,
|
int notify,
|
||||||
@HiveField(9) int powerLevel,
|
int powerLevel,
|
||||||
dynamic calls,
|
dynamic calls,
|
||||||
dynamic events});
|
dynamic events});
|
||||||
|
|
||||||
@@ -844,20 +791,19 @@ class __$$SnChannelMemberImplCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@HiveType(typeId: 3)
|
|
||||||
class _$SnChannelMemberImpl extends _SnChannelMember {
|
class _$SnChannelMemberImpl extends _SnChannelMember {
|
||||||
const _$SnChannelMemberImpl(
|
const _$SnChannelMemberImpl(
|
||||||
{@HiveField(0) required this.id,
|
{required this.id,
|
||||||
@HiveField(1) required this.createdAt,
|
required this.createdAt,
|
||||||
@HiveField(2) required this.updatedAt,
|
required this.updatedAt,
|
||||||
@HiveField(3) required this.deletedAt,
|
required this.deletedAt,
|
||||||
@HiveField(4) required this.channelId,
|
required this.channelId,
|
||||||
@HiveField(5) required this.accountId,
|
required this.accountId,
|
||||||
@HiveField(6) required this.nick,
|
required this.nick,
|
||||||
@HiveField(7) required this.channel,
|
required this.channel,
|
||||||
@HiveField(8) required this.account,
|
required this.account,
|
||||||
this.notify = 0,
|
this.notify = 0,
|
||||||
@HiveField(9) required this.powerLevel,
|
required this.powerLevel,
|
||||||
this.calls,
|
this.calls,
|
||||||
this.events})
|
this.events})
|
||||||
: super._();
|
: super._();
|
||||||
@@ -866,37 +812,27 @@ class _$SnChannelMemberImpl extends _SnChannelMember {
|
|||||||
_$$SnChannelMemberImplFromJson(json);
|
_$$SnChannelMemberImplFromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@HiveField(0)
|
|
||||||
final int id;
|
final int id;
|
||||||
@override
|
@override
|
||||||
@HiveField(1)
|
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(2)
|
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(3)
|
|
||||||
final DateTime? deletedAt;
|
final DateTime? deletedAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(4)
|
|
||||||
final int channelId;
|
final int channelId;
|
||||||
@override
|
@override
|
||||||
@HiveField(5)
|
|
||||||
final int accountId;
|
final int accountId;
|
||||||
@override
|
@override
|
||||||
@HiveField(6)
|
|
||||||
final String? nick;
|
final String? nick;
|
||||||
@override
|
@override
|
||||||
@HiveField(7)
|
|
||||||
final SnChannel? channel;
|
final SnChannel? channel;
|
||||||
@override
|
@override
|
||||||
@HiveField(8)
|
|
||||||
final SnAccount? account;
|
final SnAccount? account;
|
||||||
@override
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final int notify;
|
final int notify;
|
||||||
@override
|
@override
|
||||||
@HiveField(9)
|
|
||||||
final int powerLevel;
|
final int powerLevel;
|
||||||
@override
|
@override
|
||||||
final dynamic calls;
|
final dynamic calls;
|
||||||
@@ -971,17 +907,17 @@ class _$SnChannelMemberImpl extends _SnChannelMember {
|
|||||||
|
|
||||||
abstract class _SnChannelMember extends SnChannelMember {
|
abstract class _SnChannelMember extends SnChannelMember {
|
||||||
const factory _SnChannelMember(
|
const factory _SnChannelMember(
|
||||||
{@HiveField(0) required final int id,
|
{required final int id,
|
||||||
@HiveField(1) required final DateTime createdAt,
|
required final DateTime createdAt,
|
||||||
@HiveField(2) required final DateTime updatedAt,
|
required final DateTime updatedAt,
|
||||||
@HiveField(3) required final DateTime? deletedAt,
|
required final DateTime? deletedAt,
|
||||||
@HiveField(4) required final int channelId,
|
required final int channelId,
|
||||||
@HiveField(5) required final int accountId,
|
required final int accountId,
|
||||||
@HiveField(6) required final String? nick,
|
required final String? nick,
|
||||||
@HiveField(7) required final SnChannel? channel,
|
required final SnChannel? channel,
|
||||||
@HiveField(8) required final SnAccount? account,
|
required final SnAccount? account,
|
||||||
final int notify,
|
final int notify,
|
||||||
@HiveField(9) required final int powerLevel,
|
required final int powerLevel,
|
||||||
final dynamic calls,
|
final dynamic calls,
|
||||||
final dynamic events}) = _$SnChannelMemberImpl;
|
final dynamic events}) = _$SnChannelMemberImpl;
|
||||||
const _SnChannelMember._() : super._();
|
const _SnChannelMember._() : super._();
|
||||||
@@ -990,36 +926,26 @@ abstract class _SnChannelMember extends SnChannelMember {
|
|||||||
_$SnChannelMemberImpl.fromJson;
|
_$SnChannelMemberImpl.fromJson;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@HiveField(0)
|
|
||||||
int get id;
|
int get id;
|
||||||
@override
|
@override
|
||||||
@HiveField(1)
|
|
||||||
DateTime get createdAt;
|
DateTime get createdAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(2)
|
|
||||||
DateTime get updatedAt;
|
DateTime get updatedAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(3)
|
|
||||||
DateTime? get deletedAt;
|
DateTime? get deletedAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(4)
|
|
||||||
int get channelId;
|
int get channelId;
|
||||||
@override
|
@override
|
||||||
@HiveField(5)
|
|
||||||
int get accountId;
|
int get accountId;
|
||||||
@override
|
@override
|
||||||
@HiveField(6)
|
|
||||||
String? get nick;
|
String? get nick;
|
||||||
@override
|
@override
|
||||||
@HiveField(7)
|
|
||||||
SnChannel? get channel;
|
SnChannel? get channel;
|
||||||
@override
|
@override
|
||||||
@HiveField(8)
|
|
||||||
SnAccount? get account;
|
SnAccount? get account;
|
||||||
@override
|
@override
|
||||||
int get notify;
|
int get notify;
|
||||||
@override
|
@override
|
||||||
@HiveField(9)
|
|
||||||
int get powerLevel;
|
int get powerLevel;
|
||||||
@override
|
@override
|
||||||
dynamic get calls;
|
dynamic get calls;
|
||||||
@@ -1040,31 +966,18 @@ SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) {
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnChatMessage {
|
mixin _$SnChatMessage {
|
||||||
@HiveField(0)
|
|
||||||
int get id => throw _privateConstructorUsedError;
|
int get id => throw _privateConstructorUsedError;
|
||||||
@HiveField(1)
|
|
||||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||||
@HiveField(2)
|
|
||||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||||
@HiveField(3)
|
|
||||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||||
@HiveField(4)
|
|
||||||
String get uuid => throw _privateConstructorUsedError;
|
String get uuid => throw _privateConstructorUsedError;
|
||||||
@HiveField(5)
|
|
||||||
Map<String, dynamic> get body => throw _privateConstructorUsedError;
|
Map<String, dynamic> get body => throw _privateConstructorUsedError;
|
||||||
@HiveField(6)
|
|
||||||
String get type => throw _privateConstructorUsedError;
|
String get type => throw _privateConstructorUsedError;
|
||||||
@HiveField(7)
|
|
||||||
SnChannel get channel => throw _privateConstructorUsedError;
|
SnChannel get channel => throw _privateConstructorUsedError;
|
||||||
@HiveField(8)
|
|
||||||
SnChannelMember get sender => throw _privateConstructorUsedError;
|
SnChannelMember get sender => throw _privateConstructorUsedError;
|
||||||
@HiveField(9)
|
|
||||||
int get channelId => throw _privateConstructorUsedError;
|
int get channelId => throw _privateConstructorUsedError;
|
||||||
@HiveField(10)
|
|
||||||
int get senderId => throw _privateConstructorUsedError;
|
int get senderId => throw _privateConstructorUsedError;
|
||||||
@HiveField(11)
|
|
||||||
int? get quoteEventId => throw _privateConstructorUsedError;
|
int? get quoteEventId => throw _privateConstructorUsedError;
|
||||||
@HiveField(12)
|
|
||||||
int? get relatedEventId => throw _privateConstructorUsedError;
|
int? get relatedEventId => throw _privateConstructorUsedError;
|
||||||
SnChatMessagePreload? get preload => throw _privateConstructorUsedError;
|
SnChatMessagePreload? get preload => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
@@ -1085,19 +998,19 @@ abstract class $SnChatMessageCopyWith<$Res> {
|
|||||||
_$SnChatMessageCopyWithImpl<$Res, SnChatMessage>;
|
_$SnChatMessageCopyWithImpl<$Res, SnChatMessage>;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@HiveField(0) int id,
|
{int id,
|
||||||
@HiveField(1) DateTime createdAt,
|
DateTime createdAt,
|
||||||
@HiveField(2) DateTime updatedAt,
|
DateTime updatedAt,
|
||||||
@HiveField(3) DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
@HiveField(4) String uuid,
|
String uuid,
|
||||||
@HiveField(5) Map<String, dynamic> body,
|
Map<String, dynamic> body,
|
||||||
@HiveField(6) String type,
|
String type,
|
||||||
@HiveField(7) SnChannel channel,
|
SnChannel channel,
|
||||||
@HiveField(8) SnChannelMember sender,
|
SnChannelMember sender,
|
||||||
@HiveField(9) int channelId,
|
int channelId,
|
||||||
@HiveField(10) int senderId,
|
int senderId,
|
||||||
@HiveField(11) int? quoteEventId,
|
int? quoteEventId,
|
||||||
@HiveField(12) int? relatedEventId,
|
int? relatedEventId,
|
||||||
SnChatMessagePreload? preload});
|
SnChatMessagePreload? preload});
|
||||||
|
|
||||||
$SnChannelCopyWith<$Res> get channel;
|
$SnChannelCopyWith<$Res> get channel;
|
||||||
@@ -1239,19 +1152,19 @@ abstract class _$$SnChatMessageImplCopyWith<$Res>
|
|||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@HiveField(0) int id,
|
{int id,
|
||||||
@HiveField(1) DateTime createdAt,
|
DateTime createdAt,
|
||||||
@HiveField(2) DateTime updatedAt,
|
DateTime updatedAt,
|
||||||
@HiveField(3) DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
@HiveField(4) String uuid,
|
String uuid,
|
||||||
@HiveField(5) Map<String, dynamic> body,
|
Map<String, dynamic> body,
|
||||||
@HiveField(6) String type,
|
String type,
|
||||||
@HiveField(7) SnChannel channel,
|
SnChannel channel,
|
||||||
@HiveField(8) SnChannelMember sender,
|
SnChannelMember sender,
|
||||||
@HiveField(9) int channelId,
|
int channelId,
|
||||||
@HiveField(10) int senderId,
|
int senderId,
|
||||||
@HiveField(11) int? quoteEventId,
|
int? quoteEventId,
|
||||||
@HiveField(12) int? relatedEventId,
|
int? relatedEventId,
|
||||||
SnChatMessagePreload? preload});
|
SnChatMessagePreload? preload});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1353,22 +1266,21 @@ class __$$SnChatMessageImplCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@HiveType(typeId: 4)
|
|
||||||
class _$SnChatMessageImpl extends _SnChatMessage {
|
class _$SnChatMessageImpl extends _SnChatMessage {
|
||||||
const _$SnChatMessageImpl(
|
const _$SnChatMessageImpl(
|
||||||
{@HiveField(0) required this.id,
|
{required this.id,
|
||||||
@HiveField(1) required this.createdAt,
|
required this.createdAt,
|
||||||
@HiveField(2) required this.updatedAt,
|
required this.updatedAt,
|
||||||
@HiveField(3) required this.deletedAt,
|
required this.deletedAt,
|
||||||
@HiveField(4) required this.uuid,
|
required this.uuid,
|
||||||
@HiveField(5) final Map<String, dynamic> body = const {},
|
final Map<String, dynamic> body = const {},
|
||||||
@HiveField(6) required this.type,
|
required this.type,
|
||||||
@HiveField(7) required this.channel,
|
required this.channel,
|
||||||
@HiveField(8) required this.sender,
|
required this.sender,
|
||||||
@HiveField(9) required this.channelId,
|
required this.channelId,
|
||||||
@HiveField(10) required this.senderId,
|
required this.senderId,
|
||||||
@HiveField(11) required this.quoteEventId,
|
required this.quoteEventId,
|
||||||
@HiveField(12) required this.relatedEventId,
|
required this.relatedEventId,
|
||||||
this.preload})
|
this.preload})
|
||||||
: _body = body,
|
: _body = body,
|
||||||
super._();
|
super._();
|
||||||
@@ -1377,24 +1289,18 @@ class _$SnChatMessageImpl extends _SnChatMessage {
|
|||||||
_$$SnChatMessageImplFromJson(json);
|
_$$SnChatMessageImplFromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@HiveField(0)
|
|
||||||
final int id;
|
final int id;
|
||||||
@override
|
@override
|
||||||
@HiveField(1)
|
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(2)
|
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(3)
|
|
||||||
final DateTime? deletedAt;
|
final DateTime? deletedAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(4)
|
|
||||||
final String uuid;
|
final String uuid;
|
||||||
final Map<String, dynamic> _body;
|
final Map<String, dynamic> _body;
|
||||||
@override
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
@HiveField(5)
|
|
||||||
Map<String, dynamic> get body {
|
Map<String, dynamic> get body {
|
||||||
if (_body is EqualUnmodifiableMapView) return _body;
|
if (_body is EqualUnmodifiableMapView) return _body;
|
||||||
// ignore: implicit_dynamic_type
|
// ignore: implicit_dynamic_type
|
||||||
@@ -1402,25 +1308,18 @@ class _$SnChatMessageImpl extends _SnChatMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@HiveField(6)
|
|
||||||
final String type;
|
final String type;
|
||||||
@override
|
@override
|
||||||
@HiveField(7)
|
|
||||||
final SnChannel channel;
|
final SnChannel channel;
|
||||||
@override
|
@override
|
||||||
@HiveField(8)
|
|
||||||
final SnChannelMember sender;
|
final SnChannelMember sender;
|
||||||
@override
|
@override
|
||||||
@HiveField(9)
|
|
||||||
final int channelId;
|
final int channelId;
|
||||||
@override
|
@override
|
||||||
@HiveField(10)
|
|
||||||
final int senderId;
|
final int senderId;
|
||||||
@override
|
@override
|
||||||
@HiveField(11)
|
|
||||||
final int? quoteEventId;
|
final int? quoteEventId;
|
||||||
@override
|
@override
|
||||||
@HiveField(12)
|
|
||||||
final int? relatedEventId;
|
final int? relatedEventId;
|
||||||
@override
|
@override
|
||||||
final SnChatMessagePreload? preload;
|
final SnChatMessagePreload? preload;
|
||||||
@@ -1495,19 +1394,19 @@ class _$SnChatMessageImpl extends _SnChatMessage {
|
|||||||
|
|
||||||
abstract class _SnChatMessage extends SnChatMessage {
|
abstract class _SnChatMessage extends SnChatMessage {
|
||||||
const factory _SnChatMessage(
|
const factory _SnChatMessage(
|
||||||
{@HiveField(0) required final int id,
|
{required final int id,
|
||||||
@HiveField(1) required final DateTime createdAt,
|
required final DateTime createdAt,
|
||||||
@HiveField(2) required final DateTime updatedAt,
|
required final DateTime updatedAt,
|
||||||
@HiveField(3) required final DateTime? deletedAt,
|
required final DateTime? deletedAt,
|
||||||
@HiveField(4) required final String uuid,
|
required final String uuid,
|
||||||
@HiveField(5) final Map<String, dynamic> body,
|
final Map<String, dynamic> body,
|
||||||
@HiveField(6) required final String type,
|
required final String type,
|
||||||
@HiveField(7) required final SnChannel channel,
|
required final SnChannel channel,
|
||||||
@HiveField(8) required final SnChannelMember sender,
|
required final SnChannelMember sender,
|
||||||
@HiveField(9) required final int channelId,
|
required final int channelId,
|
||||||
@HiveField(10) required final int senderId,
|
required final int senderId,
|
||||||
@HiveField(11) required final int? quoteEventId,
|
required final int? quoteEventId,
|
||||||
@HiveField(12) required final int? relatedEventId,
|
required final int? relatedEventId,
|
||||||
final SnChatMessagePreload? preload}) = _$SnChatMessageImpl;
|
final SnChatMessagePreload? preload}) = _$SnChatMessageImpl;
|
||||||
const _SnChatMessage._() : super._();
|
const _SnChatMessage._() : super._();
|
||||||
|
|
||||||
@@ -1515,43 +1414,30 @@ abstract class _SnChatMessage extends SnChatMessage {
|
|||||||
_$SnChatMessageImpl.fromJson;
|
_$SnChatMessageImpl.fromJson;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@HiveField(0)
|
|
||||||
int get id;
|
int get id;
|
||||||
@override
|
@override
|
||||||
@HiveField(1)
|
|
||||||
DateTime get createdAt;
|
DateTime get createdAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(2)
|
|
||||||
DateTime get updatedAt;
|
DateTime get updatedAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(3)
|
|
||||||
DateTime? get deletedAt;
|
DateTime? get deletedAt;
|
||||||
@override
|
@override
|
||||||
@HiveField(4)
|
|
||||||
String get uuid;
|
String get uuid;
|
||||||
@override
|
@override
|
||||||
@HiveField(5)
|
|
||||||
Map<String, dynamic> get body;
|
Map<String, dynamic> get body;
|
||||||
@override
|
@override
|
||||||
@HiveField(6)
|
|
||||||
String get type;
|
String get type;
|
||||||
@override
|
@override
|
||||||
@HiveField(7)
|
|
||||||
SnChannel get channel;
|
SnChannel get channel;
|
||||||
@override
|
@override
|
||||||
@HiveField(8)
|
|
||||||
SnChannelMember get sender;
|
SnChannelMember get sender;
|
||||||
@override
|
@override
|
||||||
@HiveField(9)
|
|
||||||
int get channelId;
|
int get channelId;
|
||||||
@override
|
@override
|
||||||
@HiveField(10)
|
|
||||||
int get senderId;
|
int get senderId;
|
||||||
@override
|
@override
|
||||||
@HiveField(11)
|
|
||||||
int? get quoteEventId;
|
int? get quoteEventId;
|
||||||
@override
|
@override
|
||||||
@HiveField(12)
|
|
||||||
int? get relatedEventId;
|
int? get relatedEventId;
|
||||||
@override
|
@override
|
||||||
SnChatMessagePreload? get preload;
|
SnChatMessagePreload? get preload;
|
||||||
|
|||||||
@@ -2,214 +2,6 @@
|
|||||||
|
|
||||||
part of 'chat.dart';
|
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
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'check_in.freezed.dart';
|
part 'check_in.freezed.dart';
|
||||||
|
|
||||||
part 'check_in.g.dart';
|
part 'check_in.g.dart';
|
||||||
|
|
||||||
const List<String> kCheckInResultTierSymbols = ['大凶', '凶', '中平', '吉', '大吉'];
|
final List<String> kCheckInResultTierSymbols = [
|
||||||
|
'checkInResultTier1',
|
||||||
|
'checkInResultTier2',
|
||||||
|
'checkInResultTier3',
|
||||||
|
'checkInResultTier4',
|
||||||
|
'checkInResultTier5'
|
||||||
|
].map((e) => e.tr()).toList();
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class SnCheckInRecord with _$SnCheckInRecord {
|
class SnCheckInRecord with _$SnCheckInRecord {
|
||||||
@@ -16,12 +24,12 @@ class SnCheckInRecord with _$SnCheckInRecord {
|
|||||||
required DateTime? deletedAt,
|
required DateTime? deletedAt,
|
||||||
required int resultTier,
|
required int resultTier,
|
||||||
required int resultExperience,
|
required int resultExperience,
|
||||||
|
required double resultCoin,
|
||||||
required List<int> resultModifiers,
|
required List<int> resultModifiers,
|
||||||
required int accountId,
|
required int accountId,
|
||||||
}) = _SnCheckInRecord;
|
}) = _SnCheckInRecord;
|
||||||
|
|
||||||
factory SnCheckInRecord.fromJson(Map<String, dynamic> json) =>
|
factory SnCheckInRecord.fromJson(Map<String, dynamic> json) => _$SnCheckInRecordFromJson(json);
|
||||||
_$SnCheckInRecordFromJson(json);
|
|
||||||
|
|
||||||
String get symbol => kCheckInResultTierSymbols[resultTier];
|
String get symbol => kCheckInResultTierSymbols[resultTier];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ mixin _$SnCheckInRecord {
|
|||||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||||
int get resultTier => throw _privateConstructorUsedError;
|
int get resultTier => throw _privateConstructorUsedError;
|
||||||
int get resultExperience => throw _privateConstructorUsedError;
|
int get resultExperience => throw _privateConstructorUsedError;
|
||||||
|
double get resultCoin => throw _privateConstructorUsedError;
|
||||||
List<int> get resultModifiers => throw _privateConstructorUsedError;
|
List<int> get resultModifiers => throw _privateConstructorUsedError;
|
||||||
int get accountId => throw _privateConstructorUsedError;
|
int get accountId => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ abstract class $SnCheckInRecordCopyWith<$Res> {
|
|||||||
DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
int resultTier,
|
int resultTier,
|
||||||
int resultExperience,
|
int resultExperience,
|
||||||
|
double resultCoin,
|
||||||
List<int> resultModifiers,
|
List<int> resultModifiers,
|
||||||
int accountId});
|
int accountId});
|
||||||
}
|
}
|
||||||
@@ -77,6 +79,7 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
|||||||
Object? deletedAt = freezed,
|
Object? deletedAt = freezed,
|
||||||
Object? resultTier = null,
|
Object? resultTier = null,
|
||||||
Object? resultExperience = null,
|
Object? resultExperience = null,
|
||||||
|
Object? resultCoin = null,
|
||||||
Object? resultModifiers = null,
|
Object? resultModifiers = null,
|
||||||
Object? accountId = null,
|
Object? accountId = null,
|
||||||
}) {
|
}) {
|
||||||
@@ -105,6 +108,10 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
|
|||||||
? _value.resultExperience
|
? _value.resultExperience
|
||||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
resultCoin: null == resultCoin
|
||||||
|
? _value.resultCoin
|
||||||
|
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
resultModifiers: null == resultModifiers
|
resultModifiers: null == resultModifiers
|
||||||
? _value.resultModifiers
|
? _value.resultModifiers
|
||||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -132,6 +139,7 @@ abstract class _$$SnCheckInRecordImplCopyWith<$Res>
|
|||||||
DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
int resultTier,
|
int resultTier,
|
||||||
int resultExperience,
|
int resultExperience,
|
||||||
|
double resultCoin,
|
||||||
List<int> resultModifiers,
|
List<int> resultModifiers,
|
||||||
int accountId});
|
int accountId});
|
||||||
}
|
}
|
||||||
@@ -155,6 +163,7 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
|||||||
Object? deletedAt = freezed,
|
Object? deletedAt = freezed,
|
||||||
Object? resultTier = null,
|
Object? resultTier = null,
|
||||||
Object? resultExperience = null,
|
Object? resultExperience = null,
|
||||||
|
Object? resultCoin = null,
|
||||||
Object? resultModifiers = null,
|
Object? resultModifiers = null,
|
||||||
Object? accountId = null,
|
Object? accountId = null,
|
||||||
}) {
|
}) {
|
||||||
@@ -183,6 +192,10 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
|
|||||||
? _value.resultExperience
|
? _value.resultExperience
|
||||||
: resultExperience // ignore: cast_nullable_to_non_nullable
|
: resultExperience // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
resultCoin: null == resultCoin
|
||||||
|
? _value.resultCoin
|
||||||
|
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
resultModifiers: null == resultModifiers
|
resultModifiers: null == resultModifiers
|
||||||
? _value._resultModifiers
|
? _value._resultModifiers
|
||||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -205,6 +218,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
|||||||
required this.deletedAt,
|
required this.deletedAt,
|
||||||
required this.resultTier,
|
required this.resultTier,
|
||||||
required this.resultExperience,
|
required this.resultExperience,
|
||||||
|
required this.resultCoin,
|
||||||
required final List<int> resultModifiers,
|
required final List<int> resultModifiers,
|
||||||
required this.accountId})
|
required this.accountId})
|
||||||
: _resultModifiers = resultModifiers,
|
: _resultModifiers = resultModifiers,
|
||||||
@@ -225,6 +239,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
|||||||
final int resultTier;
|
final int resultTier;
|
||||||
@override
|
@override
|
||||||
final int resultExperience;
|
final int resultExperience;
|
||||||
|
@override
|
||||||
|
final double resultCoin;
|
||||||
final List<int> _resultModifiers;
|
final List<int> _resultModifiers;
|
||||||
@override
|
@override
|
||||||
List<int> get resultModifiers {
|
List<int> get resultModifiers {
|
||||||
@@ -238,7 +254,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultModifiers: $resultModifiers, accountId: $accountId)';
|
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -257,6 +273,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
|||||||
other.resultTier == resultTier) &&
|
other.resultTier == resultTier) &&
|
||||||
(identical(other.resultExperience, resultExperience) ||
|
(identical(other.resultExperience, resultExperience) ||
|
||||||
other.resultExperience == resultExperience) &&
|
other.resultExperience == resultExperience) &&
|
||||||
|
(identical(other.resultCoin, resultCoin) ||
|
||||||
|
other.resultCoin == resultCoin) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other._resultModifiers, _resultModifiers) &&
|
.equals(other._resultModifiers, _resultModifiers) &&
|
||||||
(identical(other.accountId, accountId) ||
|
(identical(other.accountId, accountId) ||
|
||||||
@@ -273,6 +291,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
|
|||||||
deletedAt,
|
deletedAt,
|
||||||
resultTier,
|
resultTier,
|
||||||
resultExperience,
|
resultExperience,
|
||||||
|
resultCoin,
|
||||||
const DeepCollectionEquality().hash(_resultModifiers),
|
const DeepCollectionEquality().hash(_resultModifiers),
|
||||||
accountId);
|
accountId);
|
||||||
|
|
||||||
@@ -301,6 +320,7 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
|
|||||||
required final DateTime? deletedAt,
|
required final DateTime? deletedAt,
|
||||||
required final int resultTier,
|
required final int resultTier,
|
||||||
required final int resultExperience,
|
required final int resultExperience,
|
||||||
|
required final double resultCoin,
|
||||||
required final List<int> resultModifiers,
|
required final List<int> resultModifiers,
|
||||||
required final int accountId}) = _$SnCheckInRecordImpl;
|
required final int accountId}) = _$SnCheckInRecordImpl;
|
||||||
const _SnCheckInRecord._() : super._();
|
const _SnCheckInRecord._() : super._();
|
||||||
@@ -321,6 +341,8 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
|
|||||||
@override
|
@override
|
||||||
int get resultExperience;
|
int get resultExperience;
|
||||||
@override
|
@override
|
||||||
|
double get resultCoin;
|
||||||
|
@override
|
||||||
List<int> get resultModifiers;
|
List<int> get resultModifiers;
|
||||||
@override
|
@override
|
||||||
int get accountId;
|
int get accountId;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ _$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson(
|
|||||||
: DateTime.parse(json['deleted_at'] as String),
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
resultTier: (json['result_tier'] as num).toInt(),
|
resultTier: (json['result_tier'] as num).toInt(),
|
||||||
resultExperience: (json['result_experience'] as num).toInt(),
|
resultExperience: (json['result_experience'] as num).toInt(),
|
||||||
|
resultCoin: (json['result_coin'] as num).toDouble(),
|
||||||
resultModifiers: (json['result_modifiers'] as List<dynamic>)
|
resultModifiers: (json['result_modifiers'] as List<dynamic>)
|
||||||
.map((e) => (e as num).toInt())
|
.map((e) => (e as num).toInt())
|
||||||
.toList(),
|
.toList(),
|
||||||
@@ -32,6 +33,7 @@ Map<String, dynamic> _$$SnCheckInRecordImplToJson(
|
|||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
'result_tier': instance.resultTier,
|
'result_tier': instance.resultTier,
|
||||||
'result_experience': instance.resultExperience,
|
'result_experience': instance.resultExperience,
|
||||||
|
'result_coin': instance.resultCoin,
|
||||||
'result_modifiers': instance.resultModifiers,
|
'result_modifiers': instance.resultModifiers,
|
||||||
'account_id': instance.accountId,
|
'account_id': instance.accountId,
|
||||||
};
|
};
|
||||||
|
|||||||
45
lib/types/poll.dart
Normal file
45
lib/types/poll.dart
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'poll.freezed.dart';
|
||||||
|
part 'poll.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnPoll with _$SnPoll {
|
||||||
|
const factory SnPoll({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required dynamic deletedAt,
|
||||||
|
required dynamic expiredAt,
|
||||||
|
required List<SnPollOption> options,
|
||||||
|
required int accountId,
|
||||||
|
required SnPollMetric metric,
|
||||||
|
}) = _SnPoll;
|
||||||
|
|
||||||
|
factory SnPoll.fromJson(Map<String, Object?> json) => _$SnPollFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnPollMetric with _$SnPollMetric {
|
||||||
|
const factory SnPollMetric({
|
||||||
|
required int totalAnswer,
|
||||||
|
@Default({}) Map<String, int> byOptions,
|
||||||
|
@Default({}) Map<String, double> byOptionsPercentage,
|
||||||
|
}) = _SnPollMetric;
|
||||||
|
|
||||||
|
factory SnPollMetric.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnPollMetricFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnPollOption with _$SnPollOption {
|
||||||
|
const factory SnPollOption({
|
||||||
|
required String id,
|
||||||
|
required String icon,
|
||||||
|
required String name,
|
||||||
|
required String description,
|
||||||
|
}) = _SnPollOption;
|
||||||
|
|
||||||
|
factory SnPollOption.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnPollOptionFromJson(json);
|
||||||
|
}
|
||||||
761
lib/types/poll.freezed.dart
Normal file
761
lib/types/poll.freezed.dart
Normal file
@@ -0,0 +1,761 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'poll.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
SnPoll _$SnPollFromJson(Map<String, dynamic> json) {
|
||||||
|
return _SnPoll.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnPoll {
|
||||||
|
int get id => throw _privateConstructorUsedError;
|
||||||
|
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||||
|
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||||
|
dynamic get deletedAt => throw _privateConstructorUsedError;
|
||||||
|
dynamic get expiredAt => throw _privateConstructorUsedError;
|
||||||
|
List<SnPollOption> get options => throw _privateConstructorUsedError;
|
||||||
|
int get accountId => throw _privateConstructorUsedError;
|
||||||
|
SnPollMetric get metric => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this SnPoll to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of SnPoll
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$SnPollCopyWith<SnPoll> get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $SnPollCopyWith<$Res> {
|
||||||
|
factory $SnPollCopyWith(SnPoll value, $Res Function(SnPoll) then) =
|
||||||
|
_$SnPollCopyWithImpl<$Res, SnPoll>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int id,
|
||||||
|
DateTime createdAt,
|
||||||
|
DateTime updatedAt,
|
||||||
|
dynamic deletedAt,
|
||||||
|
dynamic expiredAt,
|
||||||
|
List<SnPollOption> options,
|
||||||
|
int accountId,
|
||||||
|
SnPollMetric metric});
|
||||||
|
|
||||||
|
$SnPollMetricCopyWith<$Res> get metric;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnPollCopyWithImpl<$Res, $Val extends SnPoll>
|
||||||
|
implements $SnPollCopyWith<$Res> {
|
||||||
|
_$SnPollCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPoll
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? createdAt = null,
|
||||||
|
Object? updatedAt = null,
|
||||||
|
Object? deletedAt = freezed,
|
||||||
|
Object? expiredAt = freezed,
|
||||||
|
Object? options = null,
|
||||||
|
Object? accountId = null,
|
||||||
|
Object? metric = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
createdAt: null == createdAt
|
||||||
|
? _value.createdAt
|
||||||
|
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
updatedAt: null == updatedAt
|
||||||
|
? _value.updatedAt
|
||||||
|
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
deletedAt: freezed == deletedAt
|
||||||
|
? _value.deletedAt
|
||||||
|
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as dynamic,
|
||||||
|
expiredAt: freezed == expiredAt
|
||||||
|
? _value.expiredAt
|
||||||
|
: expiredAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as dynamic,
|
||||||
|
options: null == options
|
||||||
|
? _value.options
|
||||||
|
: options // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnPollOption>,
|
||||||
|
accountId: null == accountId
|
||||||
|
? _value.accountId
|
||||||
|
: accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
metric: null == metric
|
||||||
|
? _value.metric
|
||||||
|
: metric // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnPollMetric,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnPoll
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPollMetricCopyWith<$Res> get metric {
|
||||||
|
return $SnPollMetricCopyWith<$Res>(_value.metric, (value) {
|
||||||
|
return _then(_value.copyWith(metric: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$SnPollImplCopyWith<$Res> implements $SnPollCopyWith<$Res> {
|
||||||
|
factory _$$SnPollImplCopyWith(
|
||||||
|
_$SnPollImpl value, $Res Function(_$SnPollImpl) then) =
|
||||||
|
__$$SnPollImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int id,
|
||||||
|
DateTime createdAt,
|
||||||
|
DateTime updatedAt,
|
||||||
|
dynamic deletedAt,
|
||||||
|
dynamic expiredAt,
|
||||||
|
List<SnPollOption> options,
|
||||||
|
int accountId,
|
||||||
|
SnPollMetric metric});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$SnPollMetricCopyWith<$Res> get metric;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$SnPollImplCopyWithImpl<$Res>
|
||||||
|
extends _$SnPollCopyWithImpl<$Res, _$SnPollImpl>
|
||||||
|
implements _$$SnPollImplCopyWith<$Res> {
|
||||||
|
__$$SnPollImplCopyWithImpl(
|
||||||
|
_$SnPollImpl _value, $Res Function(_$SnPollImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of SnPoll
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? createdAt = null,
|
||||||
|
Object? updatedAt = null,
|
||||||
|
Object? deletedAt = freezed,
|
||||||
|
Object? expiredAt = freezed,
|
||||||
|
Object? options = null,
|
||||||
|
Object? accountId = null,
|
||||||
|
Object? metric = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$SnPollImpl(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
createdAt: null == createdAt
|
||||||
|
? _value.createdAt
|
||||||
|
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
updatedAt: null == updatedAt
|
||||||
|
? _value.updatedAt
|
||||||
|
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
deletedAt: freezed == deletedAt
|
||||||
|
? _value.deletedAt
|
||||||
|
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as dynamic,
|
||||||
|
expiredAt: freezed == expiredAt
|
||||||
|
? _value.expiredAt
|
||||||
|
: expiredAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as dynamic,
|
||||||
|
options: null == options
|
||||||
|
? _value._options
|
||||||
|
: options // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnPollOption>,
|
||||||
|
accountId: null == accountId
|
||||||
|
? _value.accountId
|
||||||
|
: accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
metric: null == metric
|
||||||
|
? _value.metric
|
||||||
|
: metric // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnPollMetric,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$SnPollImpl implements _SnPoll {
|
||||||
|
const _$SnPollImpl(
|
||||||
|
{required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.deletedAt,
|
||||||
|
required this.expiredAt,
|
||||||
|
required final List<SnPollOption> options,
|
||||||
|
required this.accountId,
|
||||||
|
required this.metric})
|
||||||
|
: _options = options;
|
||||||
|
|
||||||
|
factory _$SnPollImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$SnPollImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final int id;
|
||||||
|
@override
|
||||||
|
final DateTime createdAt;
|
||||||
|
@override
|
||||||
|
final DateTime updatedAt;
|
||||||
|
@override
|
||||||
|
final dynamic deletedAt;
|
||||||
|
@override
|
||||||
|
final dynamic expiredAt;
|
||||||
|
final List<SnPollOption> _options;
|
||||||
|
@override
|
||||||
|
List<SnPollOption> get options {
|
||||||
|
if (_options is EqualUnmodifiableListView) return _options;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final int accountId;
|
||||||
|
@override
|
||||||
|
final SnPollMetric metric;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPoll(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, expiredAt: $expiredAt, options: $options, accountId: $accountId, metric: $metric)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$SnPollImpl &&
|
||||||
|
(identical(other.id, id) || other.id == id) &&
|
||||||
|
(identical(other.createdAt, createdAt) ||
|
||||||
|
other.createdAt == createdAt) &&
|
||||||
|
(identical(other.updatedAt, updatedAt) ||
|
||||||
|
other.updatedAt == updatedAt) &&
|
||||||
|
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
||||||
|
const DeepCollectionEquality().equals(other.expiredAt, expiredAt) &&
|
||||||
|
const DeepCollectionEquality().equals(other._options, _options) &&
|
||||||
|
(identical(other.accountId, accountId) ||
|
||||||
|
other.accountId == accountId) &&
|
||||||
|
(identical(other.metric, metric) || other.metric == metric));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
id,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
const DeepCollectionEquality().hash(deletedAt),
|
||||||
|
const DeepCollectionEquality().hash(expiredAt),
|
||||||
|
const DeepCollectionEquality().hash(_options),
|
||||||
|
accountId,
|
||||||
|
metric);
|
||||||
|
|
||||||
|
/// Create a copy of SnPoll
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$SnPollImplCopyWith<_$SnPollImpl> get copyWith =>
|
||||||
|
__$$SnPollImplCopyWithImpl<_$SnPollImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$SnPollImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _SnPoll implements SnPoll {
|
||||||
|
const factory _SnPoll(
|
||||||
|
{required final int id,
|
||||||
|
required final DateTime createdAt,
|
||||||
|
required final DateTime updatedAt,
|
||||||
|
required final dynamic deletedAt,
|
||||||
|
required final dynamic expiredAt,
|
||||||
|
required final List<SnPollOption> options,
|
||||||
|
required final int accountId,
|
||||||
|
required final SnPollMetric metric}) = _$SnPollImpl;
|
||||||
|
|
||||||
|
factory _SnPoll.fromJson(Map<String, dynamic> json) = _$SnPollImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get id;
|
||||||
|
@override
|
||||||
|
DateTime get createdAt;
|
||||||
|
@override
|
||||||
|
DateTime get updatedAt;
|
||||||
|
@override
|
||||||
|
dynamic get deletedAt;
|
||||||
|
@override
|
||||||
|
dynamic get expiredAt;
|
||||||
|
@override
|
||||||
|
List<SnPollOption> get options;
|
||||||
|
@override
|
||||||
|
int get accountId;
|
||||||
|
@override
|
||||||
|
SnPollMetric get metric;
|
||||||
|
|
||||||
|
/// Create a copy of SnPoll
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$SnPollImplCopyWith<_$SnPollImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
SnPollMetric _$SnPollMetricFromJson(Map<String, dynamic> json) {
|
||||||
|
return _SnPollMetric.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnPollMetric {
|
||||||
|
int get totalAnswer => throw _privateConstructorUsedError;
|
||||||
|
Map<String, int> get byOptions => throw _privateConstructorUsedError;
|
||||||
|
Map<String, double> get byOptionsPercentage =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this SnPollMetric to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of SnPollMetric
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$SnPollMetricCopyWith<SnPollMetric> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $SnPollMetricCopyWith<$Res> {
|
||||||
|
factory $SnPollMetricCopyWith(
|
||||||
|
SnPollMetric value, $Res Function(SnPollMetric) then) =
|
||||||
|
_$SnPollMetricCopyWithImpl<$Res, SnPollMetric>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int totalAnswer,
|
||||||
|
Map<String, int> byOptions,
|
||||||
|
Map<String, double> byOptionsPercentage});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnPollMetricCopyWithImpl<$Res, $Val extends SnPollMetric>
|
||||||
|
implements $SnPollMetricCopyWith<$Res> {
|
||||||
|
_$SnPollMetricCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPollMetric
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? totalAnswer = null,
|
||||||
|
Object? byOptions = null,
|
||||||
|
Object? byOptionsPercentage = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
totalAnswer: null == totalAnswer
|
||||||
|
? _value.totalAnswer
|
||||||
|
: totalAnswer // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
byOptions: null == byOptions
|
||||||
|
? _value.byOptions
|
||||||
|
: byOptions // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, int>,
|
||||||
|
byOptionsPercentage: null == byOptionsPercentage
|
||||||
|
? _value.byOptionsPercentage
|
||||||
|
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, double>,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$SnPollMetricImplCopyWith<$Res>
|
||||||
|
implements $SnPollMetricCopyWith<$Res> {
|
||||||
|
factory _$$SnPollMetricImplCopyWith(
|
||||||
|
_$SnPollMetricImpl value, $Res Function(_$SnPollMetricImpl) then) =
|
||||||
|
__$$SnPollMetricImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int totalAnswer,
|
||||||
|
Map<String, int> byOptions,
|
||||||
|
Map<String, double> byOptionsPercentage});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$SnPollMetricImplCopyWithImpl<$Res>
|
||||||
|
extends _$SnPollMetricCopyWithImpl<$Res, _$SnPollMetricImpl>
|
||||||
|
implements _$$SnPollMetricImplCopyWith<$Res> {
|
||||||
|
__$$SnPollMetricImplCopyWithImpl(
|
||||||
|
_$SnPollMetricImpl _value, $Res Function(_$SnPollMetricImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of SnPollMetric
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? totalAnswer = null,
|
||||||
|
Object? byOptions = null,
|
||||||
|
Object? byOptionsPercentage = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$SnPollMetricImpl(
|
||||||
|
totalAnswer: null == totalAnswer
|
||||||
|
? _value.totalAnswer
|
||||||
|
: totalAnswer // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
byOptions: null == byOptions
|
||||||
|
? _value._byOptions
|
||||||
|
: byOptions // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, int>,
|
||||||
|
byOptionsPercentage: null == byOptionsPercentage
|
||||||
|
? _value._byOptionsPercentage
|
||||||
|
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, double>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$SnPollMetricImpl implements _SnPollMetric {
|
||||||
|
const _$SnPollMetricImpl(
|
||||||
|
{required this.totalAnswer,
|
||||||
|
final Map<String, int> byOptions = const {},
|
||||||
|
final Map<String, double> byOptionsPercentage = const {}})
|
||||||
|
: _byOptions = byOptions,
|
||||||
|
_byOptionsPercentage = byOptionsPercentage;
|
||||||
|
|
||||||
|
factory _$SnPollMetricImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$SnPollMetricImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final int totalAnswer;
|
||||||
|
final Map<String, int> _byOptions;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<String, int> get byOptions {
|
||||||
|
if (_byOptions is EqualUnmodifiableMapView) return _byOptions;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_byOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, double> _byOptionsPercentage;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Map<String, double> get byOptionsPercentage {
|
||||||
|
if (_byOptionsPercentage is EqualUnmodifiableMapView)
|
||||||
|
return _byOptionsPercentage;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(_byOptionsPercentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPollMetric(totalAnswer: $totalAnswer, byOptions: $byOptions, byOptionsPercentage: $byOptionsPercentage)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$SnPollMetricImpl &&
|
||||||
|
(identical(other.totalAnswer, totalAnswer) ||
|
||||||
|
other.totalAnswer == totalAnswer) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._byOptions, _byOptions) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._byOptionsPercentage, _byOptionsPercentage));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
totalAnswer,
|
||||||
|
const DeepCollectionEquality().hash(_byOptions),
|
||||||
|
const DeepCollectionEquality().hash(_byOptionsPercentage));
|
||||||
|
|
||||||
|
/// Create a copy of SnPollMetric
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$SnPollMetricImplCopyWith<_$SnPollMetricImpl> get copyWith =>
|
||||||
|
__$$SnPollMetricImplCopyWithImpl<_$SnPollMetricImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$SnPollMetricImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _SnPollMetric implements SnPollMetric {
|
||||||
|
const factory _SnPollMetric(
|
||||||
|
{required final int totalAnswer,
|
||||||
|
final Map<String, int> byOptions,
|
||||||
|
final Map<String, double> byOptionsPercentage}) = _$SnPollMetricImpl;
|
||||||
|
|
||||||
|
factory _SnPollMetric.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$SnPollMetricImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get totalAnswer;
|
||||||
|
@override
|
||||||
|
Map<String, int> get byOptions;
|
||||||
|
@override
|
||||||
|
Map<String, double> get byOptionsPercentage;
|
||||||
|
|
||||||
|
/// Create a copy of SnPollMetric
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$SnPollMetricImplCopyWith<_$SnPollMetricImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
SnPollOption _$SnPollOptionFromJson(Map<String, dynamic> json) {
|
||||||
|
return _SnPollOption.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnPollOption {
|
||||||
|
String get id => throw _privateConstructorUsedError;
|
||||||
|
String get icon => throw _privateConstructorUsedError;
|
||||||
|
String get name => throw _privateConstructorUsedError;
|
||||||
|
String get description => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this SnPollOption to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of SnPollOption
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$SnPollOptionCopyWith<SnPollOption> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $SnPollOptionCopyWith<$Res> {
|
||||||
|
factory $SnPollOptionCopyWith(
|
||||||
|
SnPollOption value, $Res Function(SnPollOption) then) =
|
||||||
|
_$SnPollOptionCopyWithImpl<$Res, SnPollOption>;
|
||||||
|
@useResult
|
||||||
|
$Res call({String id, String icon, String name, String description});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnPollOptionCopyWithImpl<$Res, $Val extends SnPollOption>
|
||||||
|
implements $SnPollOptionCopyWith<$Res> {
|
||||||
|
_$SnPollOptionCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPollOption
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? icon = null,
|
||||||
|
Object? name = null,
|
||||||
|
Object? description = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
icon: null == icon
|
||||||
|
? _value.icon
|
||||||
|
: icon // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
name: null == name
|
||||||
|
? _value.name
|
||||||
|
: name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
description: null == description
|
||||||
|
? _value.description
|
||||||
|
: description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$SnPollOptionImplCopyWith<$Res>
|
||||||
|
implements $SnPollOptionCopyWith<$Res> {
|
||||||
|
factory _$$SnPollOptionImplCopyWith(
|
||||||
|
_$SnPollOptionImpl value, $Res Function(_$SnPollOptionImpl) then) =
|
||||||
|
__$$SnPollOptionImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({String id, String icon, String name, String description});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$SnPollOptionImplCopyWithImpl<$Res>
|
||||||
|
extends _$SnPollOptionCopyWithImpl<$Res, _$SnPollOptionImpl>
|
||||||
|
implements _$$SnPollOptionImplCopyWith<$Res> {
|
||||||
|
__$$SnPollOptionImplCopyWithImpl(
|
||||||
|
_$SnPollOptionImpl _value, $Res Function(_$SnPollOptionImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of SnPollOption
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? icon = null,
|
||||||
|
Object? name = null,
|
||||||
|
Object? description = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$SnPollOptionImpl(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
icon: null == icon
|
||||||
|
? _value.icon
|
||||||
|
: icon // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
name: null == name
|
||||||
|
? _value.name
|
||||||
|
: name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
description: null == description
|
||||||
|
? _value.description
|
||||||
|
: description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$SnPollOptionImpl implements _SnPollOption {
|
||||||
|
const _$SnPollOptionImpl(
|
||||||
|
{required this.id,
|
||||||
|
required this.icon,
|
||||||
|
required this.name,
|
||||||
|
required this.description});
|
||||||
|
|
||||||
|
factory _$SnPollOptionImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$SnPollOptionImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String id;
|
||||||
|
@override
|
||||||
|
final String icon;
|
||||||
|
@override
|
||||||
|
final String name;
|
||||||
|
@override
|
||||||
|
final String description;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPollOption(id: $id, icon: $icon, name: $name, description: $description)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$SnPollOptionImpl &&
|
||||||
|
(identical(other.id, id) || other.id == id) &&
|
||||||
|
(identical(other.icon, icon) || other.icon == icon) &&
|
||||||
|
(identical(other.name, name) || other.name == name) &&
|
||||||
|
(identical(other.description, description) ||
|
||||||
|
other.description == description));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, id, icon, name, description);
|
||||||
|
|
||||||
|
/// Create a copy of SnPollOption
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$SnPollOptionImplCopyWith<_$SnPollOptionImpl> get copyWith =>
|
||||||
|
__$$SnPollOptionImplCopyWithImpl<_$SnPollOptionImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$SnPollOptionImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _SnPollOption implements SnPollOption {
|
||||||
|
const factory _SnPollOption(
|
||||||
|
{required final String id,
|
||||||
|
required final String icon,
|
||||||
|
required final String name,
|
||||||
|
required final String description}) = _$SnPollOptionImpl;
|
||||||
|
|
||||||
|
factory _SnPollOption.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$SnPollOptionImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id;
|
||||||
|
@override
|
||||||
|
String get icon;
|
||||||
|
@override
|
||||||
|
String get name;
|
||||||
|
@override
|
||||||
|
String get description;
|
||||||
|
|
||||||
|
/// Create a copy of SnPollOption
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$SnPollOptionImplCopyWith<_$SnPollOptionImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user