Compare commits
70 Commits
2.4.2+84
...
b5155ebc5f
| Author | SHA1 | Date | |
|---|---|---|---|
| b5155ebc5f | |||
| ed1b75bacf | |||
| f311c1898c | |||
| 4c9f3e799b | |||
| e645db1630 | |||
| d5cf2478d8 | |||
| cf34a285b4 | |||
| a75083d916 | |||
| 919ff5e464 | |||
| 00863b94e8 | |||
| 1ad42e6505 | |||
| 1cec1bf82e | |||
| a4ecf30c5b | |||
| 5da7ccc8ef | |||
| b5f42863ce | |||
| 69d5e95565 | |||
| 3e3442fc89 | |||
| 8181010b0b | |||
| 269caf7555 | |||
| ae0809ad35 | |||
| 4005f03cf8 | |||
| 4bd8ec54f1 | |||
| 51a387851f | |||
| 8ed847d870 | |||
| dfe13de220 | |||
| b02a54c1e9 | |||
| 55a7e7d900 | |||
| 3585941ccb | |||
| 7c6f2cc4ab | |||
|
|
61dbf92909 | ||
|
|
b69e4002e0 | ||
|
|
49aa24b79d | ||
| ceb5c53229 | |||
| 908f0cb59e | |||
| 7c2b8de931 | |||
|
|
ddd0a4c3d3 | ||
|
|
99e07de243 | ||
| 6bb9c21759 | |||
| 8f2fc55608 | |||
| a1c4e5eca0 | |||
|
|
10bf0883e5 | ||
| 595050f89f | |||
| 0722c99f21 | |||
| 12d03836f9 | |||
|
|
f78d3f4fd5 | ||
|
|
e798a8ba76 | ||
| c28a664373 | |||
| 4589722c3b | |||
| 38e1c51b45 | |||
| 610ddec05c | |||
| d0276f9ac6 | |||
| c1e89a2ee6 | |||
| ecc79368a1 | |||
| e6d732c86a | |||
| dd055fb077 | |||
| 280840c6d8 | |||
| bde62a7b2c | |||
| 5445c570a2 | |||
| b2302f5b3c | |||
| d7359cfd0d | |||
| 9cc577adbe | |||
| dd196b7754 | |||
| 16c07c2133 | |||
| 6bcb658d44 | |||
| 9311bfc3b5 | |||
| 8dd6435a30 | |||
| 21a1d4a2ad | |||
| 603875b1af | |||
| 4209a13c84 | |||
| 55b79bfd8f |
26
.github/workflows/nightly.yml
vendored
26
.github/workflows/nightly.yml
vendored
@@ -48,14 +48,15 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: stable
|
channel: stable
|
||||||
cache: true
|
|
||||||
- run: |
|
- run: |
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y ninja-build libgtk-3-dev
|
sudo apt-get install -y ninja-build libgtk-3-dev
|
||||||
sudo apt-get install libmpv-dev mpv
|
sudo apt-get install -y libmpv-dev mpv
|
||||||
sudo apt-get install libayatana-appindicator3-dev
|
sudo apt-get install -y libayatana-appindicator3-dev
|
||||||
sudo apt-get install keybinder-3.0
|
sudo apt-get install -y keybinder-3.0
|
||||||
sudo apt-get install libnotify-dev
|
sudo apt-get install -y libnotify-dev
|
||||||
|
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
|
sudo apt-get install -y gstreamer-1.0
|
||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
- run: flutter build linux
|
- run: flutter build linux
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
@@ -63,3 +64,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: build-output-linux
|
name: build-output-linux
|
||||||
path: build/linux/x64/release/bundle
|
path: build/linux/x64/release/bundle
|
||||||
|
- name: Build AppImage
|
||||||
|
run: |
|
||||||
|
rm -r Solian.AppDir | true
|
||||||
|
mkdir Solian.AppDir
|
||||||
|
cp -r build/linux/x64/release/bundle/* Solian.AppDir
|
||||||
|
cp -r buildtools/appimage_config/* Solian.AppDir
|
||||||
|
cp assets/icon/icon-light-radius.png Solian.AppDir
|
||||||
|
sudo chmod +x buildtools/appimagetool-x86_64.AppImage
|
||||||
|
sudo chmod +x Solian.AppDir/AppRun
|
||||||
|
./buildtools/appimagetool-x86_64.AppImage Solian.AppDir
|
||||||
|
- name: Archive production artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-output-linux-appimage
|
||||||
|
path: './*.AppImage*'
|
||||||
|
|||||||
34
CODE_OF_CONDUCT.md
Normal file
34
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
Welcome to the Solar Network / HyperNet project!
|
||||||
|
|
||||||
|
We're welcome for any contribution, from bug reports to feature requests to code contributions.
|
||||||
|
|
||||||
|
To get started, start from fork the repository.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
The current repository you're visiting is the front-end project for the Solar Network project. It's built by Flutter and also manages all feature requests and issues reports in this repository.
|
||||||
|
|
||||||
|
The backend of the Solar Network is written in Go and is a microservices app. The code is stored separately in different repositories. They're linked in the README.MD, you can have a look and try to contribute if you want.
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
We're using the gitmoji to clarify the reason and changes of the commit. To learn more about gitmoji, visit https://gitmoji.dev
|
||||||
|
|
||||||
|
## Translations & Localization
|
||||||
|
|
||||||
|
We're not accepting translation and localization improvements, or fixes on the GitHub or Solsynth Git Repository. If you want to contribute to those, please head to our Weblate: https://i18n.solsynth.dev. You will able to sign up / in via your Solar Network Account (Solarpass)
|
||||||
|
|
||||||
|
## New Features
|
||||||
|
|
||||||
|
To contribute new features, please create an issue or mention the feature you want in our official development chat channel. You should discuss the feature with us and the community first. You shouldn't just create a Pull Request for the feature you want, it will not be merged.
|
||||||
|
|
||||||
|
## Bug Reports / Ask for help
|
||||||
|
|
||||||
|
Read the error message, check for the update (including pre-releases), and wiki before creating an issue. At the same time, be respectful and don't argue with our developers and contributors in the development chat or GitHub issue. Otherwise your issue may got deleted and your Solar Network Account may got a strike.
|
||||||
|
|
||||||
|
-----------
|
||||||
|
|
||||||
|
We appreciate every single commit you contributed. Let's work together and create a better Solar Network!
|
||||||
|
|
||||||
51
README.md
51
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
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!
|
Hello there! Welcome to the main repository of the HyperNet (also known as the Solar Network). The code here is mainly about the front-end app (also known as Solian). But you can still post issues here to get help and request new features!
|
||||||
|
|
||||||
## Sub Projects
|
## Sub Projects
|
||||||
|
|
||||||
@@ -14,14 +14,55 @@ HyperNet, the Solar Network is a microservices project in which the backends are
|
|||||||
- The Messaging Service: [Messaging](https://github.com/Solsynth/HyperNet.Messaging)
|
- The Messaging Service: [Messaging](https://github.com/Solsynth/HyperNet.Messaging)
|
||||||
- The Wallet Service: [Wallet](https://github.com/Solsynth/HyperNet.Wallet)
|
- The Wallet Service: [Wallet](https://github.com/Solsynth/HyperNet.Wallet)
|
||||||
- The Crawler: [Reader](https://github.com/Solsynth/HyperNet.Reader)
|
- 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.
|
- The Attachments Service: [Paperclip](https://github.com/Solsynth/HyperNet.Paperclip)
|
||||||
|
- Some others may not be listed, you can search in the organization with `HyperNet.` It's the prefix of all HyperNet projects.
|
||||||
|
|
||||||
## Tech Stack
|
## 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.
|
For those people who want to know the tech stack of this project, the front-end was built by Flutter, which provides cross-platform ability.
|
||||||
|
|
||||||
The backend was built in Go and PostgreSQL with our very own microservice framework included in the nexus.
|
The backend was built in Go and PostgreSQL with our very own microservice framework included in the nexus.
|
||||||
|
|
||||||
-----
|
If you want to contribute to the project, learn more about the [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
The content below will lead you to the world of Solar Network.
|
||||||
|
|
||||||
|
### For Normal Users
|
||||||
|
|
||||||
|
1. Go to the Github Releases page, and download the latest release / pre-release according to your platform.
|
||||||
|
- **What's the difference between stable and pre-release?** The pre-release is untested by the other users and includes the new cutting-edge features, usually the pre-release is the feature drop. At the same time, due to we're not doing the API versioning, some breaking changes may break the stable release, so use the pre-release one instead.
|
||||||
|
2. Create an account on the Solar Network
|
||||||
|
3. Go to your email inbox to confirm your registration
|
||||||
|
4. Start exploring!
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
|
||||||
|
To make the Solar Network App run in debug mode on your machine, you need to install the flutter development environment, for more environments, head to https://flutter.dev.
|
||||||
|
|
||||||
|
For the Linux platform, you need to install those extra development libs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y ninja-build libgtk-3-dev
|
||||||
|
sudo apt-get install -y libmpv-dev mpv
|
||||||
|
sudo apt-get install -y libayatana-appindicator3-dev
|
||||||
|
sudo apt-get install -y keybinder-3.0
|
||||||
|
sudo apt-get install -y libnotify-dev
|
||||||
|
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
|
sudo apt-get install -y gstreamer-1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, use the flutter run for the app running in debug mode.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to build the release version, use the flutter build command. Learn more from the flutter docs.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter build <platform>
|
||||||
|
```
|
||||||
|
|
||||||
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.
|
|
||||||
20
api/Passport/Give Punishment.bru
Normal file
20
api/Passport/Give Punishment.bru
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
meta {
|
||||||
|
name: Give Punishment
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/id/punishments
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"reason": "吹哨管理条例 / 滥用吹哨功能,累积三次复核无效吹哨。处以禁用吹哨功能 30 天。",
|
||||||
|
"type": 1,
|
||||||
|
"perm_nodes": {"FlagPost":false},
|
||||||
|
"account_id": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
assets/audio/notify/metal-pipe.mp3
Normal file
BIN
assets/audio/notify/metal-pipe.mp3
Normal file
Binary file not shown.
BIN
assets/audio/sfx/launch-done.mp3
Normal file
BIN
assets/audio/sfx/launch-done.mp3
Normal file
Binary file not shown.
BIN
assets/audio/sfx/launch-intro.mp3
Normal file
BIN
assets/audio/sfx/launch-intro.mp3
Normal file
Binary file not shown.
BIN
assets/icon/kanban-1st.jpg
Executable file
BIN
assets/icon/kanban-1st.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 509 KiB |
@@ -639,6 +639,7 @@
|
|||||||
"postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
|
"postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
|
||||||
"postQuestionAnswered": "Answered Question",
|
"postQuestionAnswered": "Answered Question",
|
||||||
"postQuestionAnswerSelect": "Select as Answer",
|
"postQuestionAnswerSelect": "Select as Answer",
|
||||||
|
"postQuestionAnswerTitle": "Selected Question",
|
||||||
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
|
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
|
||||||
"postVideoUpload": "Upload Video",
|
"postVideoUpload": "Upload Video",
|
||||||
"realmJoin": "Join Realm",
|
"realmJoin": "Join Realm",
|
||||||
@@ -890,5 +891,62 @@
|
|||||||
},
|
},
|
||||||
"settingsHideBottomNav": "Hide Bottom Navigation",
|
"settingsHideBottomNav": "Hide Bottom Navigation",
|
||||||
"settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.",
|
"settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.",
|
||||||
"reCaptcha": "reCaptcha"
|
"reCaptcha": "reCaptcha",
|
||||||
|
"friends": "Friends",
|
||||||
|
"friendsDescription": "Manage your friendships.",
|
||||||
|
"album": "Album",
|
||||||
|
"albumDescription": "View albums and manage attachments.",
|
||||||
|
"stickers": "Stickers",
|
||||||
|
"stickersDescription": "View sticker packs and manage stickers.",
|
||||||
|
"navBottomUnauthorizedCaption": "Or create an account",
|
||||||
|
"walletCurrencyGoldenShort": "GDP",
|
||||||
|
"walletCurrencyGolden": {
|
||||||
|
"one": "{} Golden Point",
|
||||||
|
"other": "{} Golden Points"
|
||||||
|
},
|
||||||
|
"walletTransactionTypeNormal": "Source Point",
|
||||||
|
"walletTransactionTypeGolden": "Golden Point",
|
||||||
|
"accountProgram": "Programs",
|
||||||
|
"accountProgramDescription": "Explore the available member programs.",
|
||||||
|
"accountProgramJoin": "Join Program",
|
||||||
|
"accountProgramJoinRequirements": "Requirements",
|
||||||
|
"accountProgramJoinPricing": "Pricing",
|
||||||
|
"accountProgramJoinPricingHint": "Billed every (30 days) month.",
|
||||||
|
"accountProgramLeaveHint": "After leaving the program, the source points will not be refunded.",
|
||||||
|
"accountProgramJoined": "Joined Program.",
|
||||||
|
"accountProgramAlreadyJoined": "Joined",
|
||||||
|
"accountProgramLeft": "Left Program.",
|
||||||
|
"leave": "Leave",
|
||||||
|
"attachmentFailedToLoadMedia": "Unable to load media file, please try again later. If this error occurs repeatedly, the source file may not exist or the network connection may be abnormal.",
|
||||||
|
"accountPunishments": "Punishments",
|
||||||
|
"accountPunishmentsDescription": "View your account's reputation status.",
|
||||||
|
"punishmentType0": "Strike",
|
||||||
|
"punishmentType1": "Limited",
|
||||||
|
"punishmentType2": "Banned",
|
||||||
|
"punishmentOverall": "Overall Status",
|
||||||
|
"punishmentStatusNormal": "All abilities normal",
|
||||||
|
"punishmentStatusWarned": "All abilities normal, but at least one strike is in effect",
|
||||||
|
"punishmentStatusLimited": "Some abilities limited, at least one limited punishment is in effect",
|
||||||
|
"punishmentStatusLimitedFully": "All abilities limited, at least one completely limited punishment is in effect",
|
||||||
|
"punishmentStatusBanned": "All services are terminated, banned",
|
||||||
|
"punishmentCreatedAt": "Applied since {}",
|
||||||
|
"punishmentExpiredAt": "Expired at {}",
|
||||||
|
"punishmentExpiredNever": "Never expired",
|
||||||
|
"punishmentModerator": "Moderator who made this punishment",
|
||||||
|
"punishmentMadeBySystem": "Made by auto-mod system",
|
||||||
|
"settingsAprilFoolFeatures": "April Fool Features",
|
||||||
|
"settingsAprilFoolFeaturesDescription": "Enable April Fool features during April Fool, this option will only be visible during April Fool.",
|
||||||
|
"settingsSoundEffects": "Sound Effects",
|
||||||
|
"settingsSoundEffectsDescription": "Enable the sound effects around the app.",
|
||||||
|
"settingsResetMemorizedWindowSize": "Reset Window Size",
|
||||||
|
"settingsResetMemorizedWindowSizeDescription": "Reset the memorized window size, and set it to the default size.",
|
||||||
|
"chatDirect": "Direct Messages",
|
||||||
|
"back": "Back",
|
||||||
|
"badgeProgramDeveloper": "Developer Program Member",
|
||||||
|
"badgeProgramStellar": "A Stellar",
|
||||||
|
"badgeProgramModerator": "Community Moderator",
|
||||||
|
"postEditedHint": "edited",
|
||||||
|
"splashScreenServer": "Server",
|
||||||
|
"splashScreenServerName": "Potato",
|
||||||
|
"splashScreenCaption": "Trying to establishing connection with HyperNet™"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -888,5 +888,62 @@
|
|||||||
},
|
},
|
||||||
"settingsHideBottomNav": "隐藏底部导航栏",
|
"settingsHideBottomNav": "隐藏底部导航栏",
|
||||||
"settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。",
|
"settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。",
|
||||||
"reCaptcha": "人机验证"
|
"reCaptcha": "人机验证",
|
||||||
|
"friends": "好友",
|
||||||
|
"friendsDescription": "管理好友关系。",
|
||||||
|
"album": "相册",
|
||||||
|
"albumDescription": "查看相册与管理上传附件。",
|
||||||
|
"stickers": "贴图",
|
||||||
|
"stickersDescription": "查看贴图包与管理贴图。",
|
||||||
|
"navBottomUnauthorizedCaption": "或者注册一个账号",
|
||||||
|
"walletCurrencyGoldenShort": "金点",
|
||||||
|
"walletCurrencyGolden": {
|
||||||
|
"one": "{} 金点",
|
||||||
|
"other": "{} 金点"
|
||||||
|
},
|
||||||
|
"walletTransactionTypeNormal": "源点",
|
||||||
|
"walletTransactionTypeGolden": "金点",
|
||||||
|
"accountProgram": "计划",
|
||||||
|
"accountProgramDescription": "了解可用的成员计划。",
|
||||||
|
"accountProgramJoin": "加入计划",
|
||||||
|
"accountProgramJoinRequirements": "要求",
|
||||||
|
"accountProgramJoinPricing": "价格",
|
||||||
|
"accountProgramJoinPricingHint": "按月(30 天)收费",
|
||||||
|
"accountProgramLeaveHint": "离开计划后,之前花费的源点不会退款。",
|
||||||
|
"accountProgramJoined": "已加入计划。",
|
||||||
|
"accountProgramLeft": "已离开计划。",
|
||||||
|
"accountProgramAlreadyJoined": "已加入",
|
||||||
|
"leave": "离开",
|
||||||
|
"attachmentFailedToLoadMedia": "无法加载媒体文件,请稍后重试。若此错误重复出现,可能源文件不存在或者网络连接异常。",
|
||||||
|
"accountPunishments": "处分",
|
||||||
|
"accountPunishmentsDescription": "查看你帐号的信誉状态。",
|
||||||
|
"punishmentType0": "警告",
|
||||||
|
"punishmentType1": "停权",
|
||||||
|
"punishmentType2": "封禁",
|
||||||
|
"punishmentOverall": "总体状态",
|
||||||
|
"punishmentStatusNormal": "所有功能正常",
|
||||||
|
"punishmentStatusWarned": "所有功能正常,但有警告生效",
|
||||||
|
"punishmentStatusLimited": "部份功能暂时受限,有至少一个停权生效",
|
||||||
|
"punishmentStatusLimitedFully": "所有功能暂时受限,有至少一个完全停权生效",
|
||||||
|
"punishmentStatusBanned": "所有服务终止,已被封禁",
|
||||||
|
"punishmentCreatedAt": "宣布于 {}",
|
||||||
|
"punishmentExpiredAt": "到期于 {}",
|
||||||
|
"punishmentExpiredNever": "永久生效",
|
||||||
|
"punishmentModerator": "责任管理员",
|
||||||
|
"punishmentMadeBySystem": "由系统自动裁决",
|
||||||
|
"settingsAprilFoolFeatures": "愚人节特性",
|
||||||
|
"settingsAprilFoolFeaturesDescription": "在愚人节期间启用愚人节特性,该选项只会在愚人节期间显示。",
|
||||||
|
"settingsSoundEffects": "声音效果",
|
||||||
|
"settingsSoundEffectsDescription": "在一些场合下启用声音特效。",
|
||||||
|
"settingsResetMemorizedWindowSize": "重置窗口大小",
|
||||||
|
"settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。",
|
||||||
|
"chatDirect": "私信",
|
||||||
|
"back": "返回",
|
||||||
|
"badgeProgramDeveloper": "开发者计划成员",
|
||||||
|
"badgeProgramStellar": "一颗恒星",
|
||||||
|
"badgeProgramModerator": "社区管理员",
|
||||||
|
"postEditedHint": "已编辑",
|
||||||
|
"splashScreenServer": "服务器",
|
||||||
|
"splashScreenServerName": "土豆",
|
||||||
|
"splashScreenCaption": "正在尝试与 HyperNet™ 取得太阳链连接"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -888,5 +888,12 @@
|
|||||||
},
|
},
|
||||||
"settingsHideBottomNav": "隱藏底部導航欄",
|
"settingsHideBottomNav": "隱藏底部導航欄",
|
||||||
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
|
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
|
||||||
"reCaptcha": "人機驗證"
|
"reCaptcha": "人機驗證",
|
||||||
|
"friends": "好友",
|
||||||
|
"friendsDescription": "管理好友關係。",
|
||||||
|
"album": "相冊",
|
||||||
|
"albumDescription": "查看相冊與管理上傳附件。",
|
||||||
|
"stickers": "貼圖",
|
||||||
|
"stickersDescription": "查看貼圖包與管理貼圖。",
|
||||||
|
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -888,5 +888,12 @@
|
|||||||
},
|
},
|
||||||
"settingsHideBottomNav": "隱藏底部導航欄",
|
"settingsHideBottomNav": "隱藏底部導航欄",
|
||||||
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
|
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
|
||||||
"reCaptcha": "人機驗證"
|
"reCaptcha": "人機驗證",
|
||||||
|
"friends": "好友",
|
||||||
|
"friendsDescription": "管理好友關係。",
|
||||||
|
"album": "相冊",
|
||||||
|
"albumDescription": "查看相冊與管理上傳附件。",
|
||||||
|
"stickers": "貼圖",
|
||||||
|
"stickersDescription": "查看貼圖包與管理貼圖。",
|
||||||
|
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
|
||||||
}
|
}
|
||||||
|
|||||||
4
buildtools/appimage_config/AppRun
Executable file
4
buildtools/appimage_config/AppRun
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
exec ./surface
|
||||||
8
buildtools/appimage_config/Solian.desktop
Normal file
8
buildtools/appimage_config/Solian.desktop
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Type=Application
|
||||||
|
Terminal=false
|
||||||
|
Name=Solian
|
||||||
|
Exec=surface %u
|
||||||
|
Icon=icon-light-radius
|
||||||
|
Categories=Network;
|
||||||
BIN
buildtools/appimagetool-x86_64.AppImage
Executable file
BIN
buildtools/appimagetool-x86_64.AppImage
Executable file
Binary file not shown.
189
ios/Podfile.lock
189
ios/Podfile.lock
@@ -1,5 +1,7 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Alamofire (5.10.2)
|
- Alamofire (5.10.2)
|
||||||
|
- audioplayers_darwin (0.0.1):
|
||||||
|
- Flutter
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- croppy (0.0.1):
|
- croppy (0.0.1):
|
||||||
@@ -44,58 +46,58 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- file_saver (0.0.1):
|
- file_saver (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Firebase/Analytics (11.8.0):
|
- Firebase/Analytics (11.10.0):
|
||||||
- Firebase/Core
|
- Firebase/Core
|
||||||
- Firebase/Core (11.8.0):
|
- Firebase/Core (11.10.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseAnalytics (~> 11.8.0)
|
- FirebaseAnalytics (~> 11.10.0)
|
||||||
- Firebase/CoreOnly (11.8.0):
|
- Firebase/CoreOnly (11.10.0):
|
||||||
- FirebaseCore (~> 11.8.0)
|
- FirebaseCore (~> 11.10.0)
|
||||||
- Firebase/Messaging (11.8.0):
|
- Firebase/Messaging (11.10.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 11.8.0)
|
- FirebaseMessaging (~> 11.10.0)
|
||||||
- firebase_analytics (11.4.4):
|
- firebase_analytics (11.4.5):
|
||||||
- Firebase/Analytics (= 11.8.0)
|
- Firebase/Analytics (= 11.10.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_core (3.12.1):
|
- firebase_core (3.13.0):
|
||||||
- Firebase/CoreOnly (= 11.8.0)
|
- Firebase/CoreOnly (= 11.10.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_messaging (15.2.4):
|
- firebase_messaging (15.2.5):
|
||||||
- Firebase/Messaging (= 11.8.0)
|
- Firebase/Messaging (= 11.10.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- FirebaseAnalytics (11.8.0):
|
- FirebaseAnalytics (11.10.0):
|
||||||
- FirebaseAnalytics/AdIdSupport (= 11.8.0)
|
- FirebaseAnalytics/AdIdSupport (= 11.10.0)
|
||||||
- FirebaseCore (~> 11.8.0)
|
- FirebaseCore (~> 11.10.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.8.0):
|
- FirebaseAnalytics/AdIdSupport (11.10.0):
|
||||||
- FirebaseCore (~> 11.8.0)
|
- FirebaseCore (~> 11.10.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleAppMeasurement (= 11.8.0)
|
- GoogleAppMeasurement (= 11.10.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.8.1):
|
- FirebaseCore (11.10.0):
|
||||||
- FirebaseCoreInternal (~> 11.8.0)
|
- FirebaseCoreInternal (~> 11.10.0)
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- GoogleUtilities/Logger (~> 8.0)
|
- GoogleUtilities/Logger (~> 8.0)
|
||||||
- FirebaseCoreInternal (11.8.0):
|
- FirebaseCoreInternal (11.10.0):
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- FirebaseInstallations (11.8.0):
|
- FirebaseInstallations (11.10.0):
|
||||||
- FirebaseCore (~> 11.8.0)
|
- FirebaseCore (~> 11.10.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.8.0):
|
- FirebaseMessaging (11.10.0):
|
||||||
- FirebaseCore (~> 11.8.0)
|
- FirebaseCore (~> 11.10.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleDataTransport (~> 10.0)
|
- GoogleDataTransport (~> 10.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
@@ -126,21 +128,21 @@ PODS:
|
|||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- GoogleAppMeasurement (11.8.0):
|
- GoogleAppMeasurement (11.10.0):
|
||||||
- GoogleAppMeasurement/AdIdSupport (= 11.8.0)
|
- GoogleAppMeasurement/AdIdSupport (= 11.10.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.8.0):
|
- GoogleAppMeasurement/AdIdSupport (11.10.0):
|
||||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.8.0)
|
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.10.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.8.0):
|
- GoogleAppMeasurement/WithoutAdIdSupport (11.10.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)
|
||||||
@@ -182,14 +184,17 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.2.0)
|
- Kingfisher (8.3.1)
|
||||||
- livekit_client (2.4.1):
|
- livekit_client (2.4.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
- media_kit_libs_ios_video (1.0.4):
|
- livekit_noise_filter (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- media_kit_native_event_loop (1.0.0):
|
- flutter_webrtc
|
||||||
|
- LiveKitKrispNoiseFilter (= 0.0.7)
|
||||||
|
- LiveKitKrispNoiseFilter (0.0.7)
|
||||||
|
- media_kit_libs_ios_video (1.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- media_kit_video (0.0.1):
|
- media_kit_video (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -212,11 +217,9 @@ PODS:
|
|||||||
- receive_sharing_intent (1.8.1):
|
- receive_sharing_intent (1.8.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
- screen_brightness_ios (0.1.0):
|
- SDWebImage (5.21.0):
|
||||||
- Flutter
|
- SDWebImage/Core (= 5.21.0)
|
||||||
- SDWebImage (5.20.1):
|
- SDWebImage/Core (5.21.0)
|
||||||
- SDWebImage/Core (= 5.20.1)
|
|
||||||
- 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):
|
||||||
@@ -262,6 +265,7 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Alamofire
|
- Alamofire
|
||||||
|
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`)
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- 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`)
|
||||||
@@ -284,15 +288,14 @@ DEPENDENCIES:
|
|||||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||||
- Kingfisher (~> 8.0)
|
- Kingfisher (~> 8.0)
|
||||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
||||||
|
- livekit_noise_filter (from `.symlinks/plugins/livekit_noise_filter/ios`)
|
||||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
|
||||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
|
||||||
- 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`)
|
||||||
@@ -318,6 +321,7 @@ SPEC REPOS:
|
|||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
|
- LiveKitKrispNoiseFilter
|
||||||
- nanopb
|
- nanopb
|
||||||
- OrderedSet
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
@@ -328,6 +332,8 @@ SPEC REPOS:
|
|||||||
- WebRTC-SDK
|
- WebRTC-SDK
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
audioplayers_darwin:
|
||||||
|
:path: ".symlinks/plugins/audioplayers_darwin/ios"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
croppy:
|
croppy:
|
||||||
@@ -370,10 +376,10 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/in_app_review/ios"
|
:path: ".symlinks/plugins/in_app_review/ios"
|
||||||
livekit_client:
|
livekit_client:
|
||||||
:path: ".symlinks/plugins/livekit_client/ios"
|
:path: ".symlinks/plugins/livekit_client/ios"
|
||||||
|
livekit_noise_filter:
|
||||||
|
:path: ".symlinks/plugins/livekit_noise_filter/ios"
|
||||||
media_kit_libs_ios_video:
|
media_kit_libs_ios_video:
|
||||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||||
media_kit_native_event_loop:
|
|
||||||
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
|
|
||||||
media_kit_video:
|
media_kit_video:
|
||||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
@@ -386,8 +392,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
receive_sharing_intent:
|
receive_sharing_intent:
|
||||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||||
screen_brightness_ios:
|
|
||||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
|
||||||
share_plus:
|
share_plus:
|
||||||
:path: ".symlinks/plugins/share_plus/ios"
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
@@ -409,65 +413,66 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab
|
||||||
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
||||||
|
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
fast_rsa: dc48fb05f26bb108863de122b2a9f5554e8e2591
|
fast_rsa: d99f8e1809a4a312fa9216d830186869b2e9eb65
|
||||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||||
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
|
||||||
firebase_analytics: e3b6782e70e32b7fa18f7cd233e3201975dd86aa
|
firebase_analytics: 1998960b8fa16fd0cd9e77a6f9fd35a2009ad65e
|
||||||
firebase_core: ac395f994af4e28f6a38b59e05a88ca57abeb874
|
firebase_core: 2d4534e7b489907dcede540c835b48981d890943
|
||||||
firebase_messaging: 7e223f4ee7ca053bf4ce43748e84a6d774ec9728
|
firebase_messaging: 75bc93a4df25faccad67f6662ae872ac9ae69b64
|
||||||
FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b
|
FirebaseAnalytics: 4e42333f02cf78ed93703a5c36f36dd518aebdef
|
||||||
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7
|
||||||
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679
|
||||||
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
FirebaseInstallations: 9980995bdd06ec8081dfb6ab364162bdd64245c3
|
||||||
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
|
FirebaseMessaging: 2b9f56aa4ed286e1f0ce2ee1d413aabb8f9f5cb9
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
||||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||||
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282
|
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||||
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
flutter_webrtc: 57f32415b8744e806f9c2a96ccdb60c6a627ba33
|
||||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||||
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
|
GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457
|
||||||
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
|
Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3
|
||||||
livekit_client: 170022ce5f7c8c70d7f862ac9c17e11508ad5fbc
|
livekit_client: 08755cabfa4da4ed455642f460cfbb39bc518070
|
||||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
livekit_noise_filter: a26aeb1c1eae6db0a023fd2f6ea3ff108c3ecbb0
|
||||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84
|
||||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||||
|
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
|
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||||
SDWebImage: 33d0f23bddeb5d209ae959153883247be6703713
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
|
||||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||||
sqlite3_flutter_libs: 487032b9008b28de37c72a3aa66849ef3745f3e6
|
sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||||
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
video_compress: f2133a07762889d67f0711ac831faa26f956980e
|
||||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||||
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
|
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||||
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
||||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
|
||||||
|
|
||||||
PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc
|
PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc
|
||||||
|
|
||||||
|
|||||||
@@ -241,7 +241,9 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
contentController.text = post.body['content'] ?? '';
|
contentController.text = post.body['content'] ?? '';
|
||||||
aliasController.text = post.alias ?? '';
|
aliasController.text = post.alias ?? '';
|
||||||
rewardController.text = post.body['reward']?.toString() ?? '';
|
rewardController.text = post.body['reward']?.toString() ?? '';
|
||||||
videoAttachment = post.preload?.video;
|
videoAttachment = post.body['video'] != null
|
||||||
|
? SnAttachment.fromJson(post.body['video'])
|
||||||
|
: null;
|
||||||
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);
|
||||||
@@ -252,17 +254,22 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
categories =
|
categories =
|
||||||
List.from(post.categories.map((ele) => ele.alias), growable: true);
|
List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||||
attachments.addAll(
|
attachments.addAll(
|
||||||
post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
post.body['attachments']
|
||||||
poll = post.preload?.poll;
|
?.map((ele) => SnAttachment.fromJson(ele))
|
||||||
|
?.map((ele) => PostWriteMedia(ele))
|
||||||
|
?.cast<PostWriteMedia>() ??
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
poll = post.poll;
|
||||||
|
|
||||||
editingDraft = post.isDraft;
|
editingDraft = post.isDraft;
|
||||||
|
|
||||||
if (post.preload?.thumbnail != null &&
|
if (post.body['thumbnail'] != null) {
|
||||||
(post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
thumbnail =
|
||||||
thumbnail = PostWriteMedia(post.preload!.thumbnail);
|
PostWriteMedia(SnAttachment.fromJson(post.body['thumbnail']));
|
||||||
}
|
}
|
||||||
if (post.preload?.realm != null) {
|
if (post.realm != null) {
|
||||||
realm = post.preload!.realm!;
|
realm = post.realm!;
|
||||||
}
|
}
|
||||||
|
|
||||||
editingPost = post;
|
editingPost = post;
|
||||||
|
|||||||
504
lib/main.dart
504
lib/main.dart
@@ -1,9 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' hide log;
|
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:croppy/croppy.dart';
|
import 'package:croppy/croppy.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -15,6 +15,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.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:google_fonts/google_fonts.dart';
|
||||||
import 'package:hotkey_manager/hotkey_manager.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';
|
||||||
@@ -49,6 +50,7 @@ import 'package:surface/router.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:surface/widgets/menu_bar.dart';
|
import 'package:surface/widgets/menu_bar.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/version_label.dart';
|
import 'package:surface/widgets/version_label.dart';
|
||||||
import 'package:tray_manager/tray_manager.dart';
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
@@ -57,6 +59,7 @@ import 'package:in_app_review/in_app_review.dart';
|
|||||||
import 'package:image_picker_android/image_picker_android.dart';
|
import 'package:image_picker_android/image_picker_android.dart';
|
||||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||||
import 'package:local_notifier/local_notifier.dart';
|
import 'package:local_notifier/local_notifier.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void appBackgroundDispatcher() {
|
void appBackgroundDispatcher() {
|
||||||
@@ -75,13 +78,40 @@ void appBackgroundDispatcher() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Desktop size tools
|
||||||
|
|
||||||
|
Future<Size> _getSavedWindowSize() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
String? sizeString = prefs.getString(kAppWindowSize);
|
||||||
|
|
||||||
|
if (sizeString != null) {
|
||||||
|
List<String> parts = sizeString.split('x');
|
||||||
|
if (parts.length == 2) {
|
||||||
|
double? width = double.tryParse(parts[0]);
|
||||||
|
double? height = double.tryParse(parts[1]);
|
||||||
|
if (width != null && height != null) {
|
||||||
|
return Size(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return const Size(1280, 720); // Default size
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveWindowSize() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final size = appWindow.size;
|
||||||
|
await prefs.setString(kAppWindowSize, '${size.width}x${size.height}');
|
||||||
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||||
|
final Size savedSize = await _getSavedWindowSize();
|
||||||
doWhenWindowReady(() {
|
doWhenWindowReady(() {
|
||||||
appWindow.minSize = Size(480, 640);
|
appWindow.minSize = Size(480, 640);
|
||||||
appWindow.size = Size(1280, 720);
|
appWindow.size = savedSize;
|
||||||
appWindow.alignment = Alignment.center;
|
appWindow.alignment = Alignment.center;
|
||||||
appWindow.show();
|
appWindow.show();
|
||||||
});
|
});
|
||||||
@@ -91,18 +121,15 @@ void main() async {
|
|||||||
|
|
||||||
if (!kIsWeb && !Platform.isLinux) {
|
if (!kIsWeb && !Platform.isLinux) {
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
options: DefaultFirebaseOptions.currentPlatform,
|
options: DefaultFirebaseOptions.currentPlatform);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GoRouter.optionURLReflectsImperativeAPIs = true;
|
GoRouter.optionURLReflectsImperativeAPIs = true;
|
||||||
usePathUrlStrategy();
|
usePathUrlStrategy();
|
||||||
|
|
||||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||||
Workmanager().initialize(
|
Workmanager()
|
||||||
appBackgroundDispatcher,
|
.initialize(appBackgroundDispatcher, isInDebugMode: kDebugMode);
|
||||||
isInDebugMode: kDebugMode,
|
|
||||||
);
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
Workmanager().registerPeriodicTask(
|
Workmanager().registerPeriodicTask(
|
||||||
"widget-update-random-post",
|
"widget-update-random-post",
|
||||||
@@ -137,7 +164,7 @@ class SolianApp extends StatelessWidget {
|
|||||||
Locale('en', 'US'),
|
Locale('en', 'US'),
|
||||||
Locale('zh', 'CN'),
|
Locale('zh', 'CN'),
|
||||||
Locale('zh', 'TW'),
|
Locale('zh', 'TW'),
|
||||||
Locale('zh', 'HK'),
|
Locale('zh', 'HK')
|
||||||
],
|
],
|
||||||
fallbackLocale: Locale('en', 'US'),
|
fallbackLocale: Locale('en', 'US'),
|
||||||
useFallbackTranslations: true,
|
useFallbackTranslations: true,
|
||||||
@@ -161,7 +188,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)),
|
ChangeNotifierProvider(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)),
|
||||||
@@ -233,6 +260,7 @@ class _AppSplashScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
|
double _initPercentage = 0;
|
||||||
String _phaseText = 'appInitStarting';
|
String _phaseText = 'appInitStarting';
|
||||||
|
|
||||||
void _tryRequestRating() async {
|
void _tryRequestRating() async {
|
||||||
@@ -263,12 +291,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
final localVersionString = '${info.version}+${info.buildNumber}';
|
final localVersionString = '${info.version}+${info.buildNumber}';
|
||||||
final resp = await Dio(
|
final resp = await Dio(
|
||||||
BaseOptions(
|
BaseOptions(
|
||||||
sendTimeout: const Duration(seconds: 60),
|
sendTimeout: const Duration(seconds: 60),
|
||||||
receiveTimeout: const Duration(seconds: 60),
|
receiveTimeout: const Duration(seconds: 60)),
|
||||||
),
|
|
||||||
).get(
|
).get(
|
||||||
'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest',
|
'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest');
|
||||||
);
|
|
||||||
final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0';
|
final remoteVersionString = resp.data?['tag_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);
|
||||||
@@ -283,9 +309,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
mounted) {
|
mounted) {
|
||||||
final config = context.read<ConfigProvider>();
|
final config = context.read<ConfigProvider>();
|
||||||
config.setUpdate(
|
config.setUpdate(
|
||||||
remoteVersionString,
|
remoteVersionString, resp.data?['body'] ?? 'No changelog');
|
||||||
resp.data?['body'] ?? 'No changelog',
|
|
||||||
);
|
|
||||||
logging.info("[Update] Update available: $remoteVersionString");
|
logging.info("[Update] Update available: $remoteVersionString");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -311,45 +335,57 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
// The Network initialization must be done after the HomeWidget initialization
|
// The Network initialization must be done after the HomeWidget initialization
|
||||||
// The Network initialization will save the server url to the HomeWidget
|
// The Network initialization will save the server url to the HomeWidget
|
||||||
// The Network initialization will also save initialize the Config, so it not need to be initialized again
|
// The Network initialization will also save initialize the Config, so it not need to be initialized again
|
||||||
|
_initPercentage = 0.1;
|
||||||
_setPhaseText('network');
|
_setPhaseText('network');
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.initializeUserAgent();
|
await sn.initializeUserAgent();
|
||||||
await sn.setConfigWithNative();
|
await sn.setConfigWithNative();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_initPercentage = 0.2;
|
||||||
_setPhaseText('userdata');
|
_setPhaseText('userdata');
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
await ua.initialize();
|
await ua.initialize();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_initPercentage = 0.3;
|
||||||
_setPhaseText('websocket');
|
_setPhaseText('websocket');
|
||||||
final ws = context.read<WebSocketProvider>();
|
final ws = context.read<WebSocketProvider>();
|
||||||
await ws.tryConnect();
|
await ws.tryConnect();
|
||||||
if (!mounted) return;
|
try {
|
||||||
_setPhaseText('notification');
|
if (!mounted) return;
|
||||||
final notify = context.read<NotificationProvider>();
|
_initPercentage = 0.9;
|
||||||
notify.listen();
|
_setPhaseText('keyPair');
|
||||||
await notify.registerPushNotifications();
|
final kp = context.read<KeyPairProvider>();
|
||||||
if (!mounted) return;
|
kp.reloadActive();
|
||||||
_setPhaseText('keyPair');
|
kp.listen();
|
||||||
final kp = context.read<KeyPairProvider>();
|
} catch (_) {}
|
||||||
await kp.reloadActive();
|
if (ua.isAuthorized) {
|
||||||
kp.listen();
|
if (!mounted) return;
|
||||||
if (!mounted) return;
|
_setPhaseText('notification');
|
||||||
_setPhaseText('stickers');
|
final notify = context.read<NotificationProvider>();
|
||||||
final sticker = context.read<SnStickerProvider>();
|
notify.listen();
|
||||||
await sticker.listSticker();
|
try {
|
||||||
if (!mounted) return;
|
notify.registerPushNotifications();
|
||||||
_setPhaseText('userDirectory');
|
if (!mounted) return;
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
_setPhaseText('stickers');
|
||||||
await ud.loadAccountCache();
|
final sticker = context.read<SnStickerProvider>();
|
||||||
if (!mounted) return;
|
await sticker.listSticker();
|
||||||
_setPhaseText('realm');
|
if (!mounted) return;
|
||||||
final rm = context.read<SnRealmProvider>();
|
_setPhaseText('userDirectory');
|
||||||
await rm.refreshAvailableRealms();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
if (!mounted) return;
|
await ud.loadAccountCache();
|
||||||
_setPhaseText('chat');
|
if (!mounted) return;
|
||||||
final ct = context.read<ChatChannelProvider>();
|
_setPhaseText('realm');
|
||||||
await ct.refreshAvailableChannels();
|
final rm = context.read<SnRealmProvider>();
|
||||||
_setPhaseText('done');
|
await rm.refreshAvailableRealms();
|
||||||
|
if (!mounted) return;
|
||||||
|
_setPhaseText('chat');
|
||||||
|
final ct = context.read<ChatChannelProvider>();
|
||||||
|
await ct.refreshAvailableChannels();
|
||||||
|
_initPercentage = 1;
|
||||||
|
_setPhaseText('done');
|
||||||
|
} catch (_) {}
|
||||||
|
_playIntro();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await context.showErrorDialog(err);
|
await context.showErrorDialog(err);
|
||||||
@@ -365,28 +401,42 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
// The quit key has been removed, and the logic of the quit key is moved to system menu bar activator.
|
// The quit key has been removed, and the logic of the quit key is moved to system menu bar activator.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _playIntro() async {
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
if (!cfg.soundEffects) return;
|
||||||
|
|
||||||
|
final date = DateTime.now();
|
||||||
|
final player = AudioPlayer(playerId: 'launch-done-player');
|
||||||
|
await player.play(
|
||||||
|
(cfg.aprilFoolFeatures && date.month == 4 && date.day == 1)
|
||||||
|
? AssetSource('audio/sfx/launch-intro.mp3')
|
||||||
|
: AssetSource('audio/sfx/launch-done.mp3'),
|
||||||
|
volume: 0.8,
|
||||||
|
ctx: AudioContext(
|
||||||
|
android: AudioContextAndroid(
|
||||||
|
contentType: AndroidContentType.sonification,
|
||||||
|
usageType: AndroidUsageType.notificationEvent,
|
||||||
|
),
|
||||||
|
iOS: AudioContextIOS(category: AVAudioSessionCategory.ambient),
|
||||||
|
),
|
||||||
|
mode: PlayerMode.lowLatency,
|
||||||
|
);
|
||||||
|
player.onPlayerComplete.listen((_) {
|
||||||
|
player.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
final Menu _appTrayMenu = Menu(
|
final Menu _appTrayMenu = Menu(
|
||||||
items: [
|
items: [
|
||||||
MenuItem(
|
MenuItem(key: 'version_label', label: 'Solian', disabled: true),
|
||||||
key: 'version_label',
|
|
||||||
label: 'Solian',
|
|
||||||
disabled: true,
|
|
||||||
),
|
|
||||||
MenuItem.separator(),
|
MenuItem.separator(),
|
||||||
MenuItem.checkbox(
|
MenuItem.checkbox(
|
||||||
checked: false,
|
checked: false,
|
||||||
key: 'mute_notification',
|
key: 'mute_notification',
|
||||||
label: 'trayMenuMuteNotification'.tr(),
|
label: 'trayMenuMuteNotification'.tr()),
|
||||||
),
|
|
||||||
MenuItem.separator(),
|
MenuItem.separator(),
|
||||||
MenuItem(
|
MenuItem(key: 'window_show', label: 'trayMenuShow'.tr()),
|
||||||
key: 'window_show',
|
MenuItem(key: 'exit', label: 'trayMenuExit'.tr()),
|
||||||
label: 'trayMenuShow'.tr(),
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
key: 'exit',
|
|
||||||
label: 'trayMenuExit'.tr(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -414,9 +464,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||||
|
|
||||||
await localNotifier.setup(
|
await localNotifier.setup(
|
||||||
appName: 'Solian',
|
appName: 'Solian', shortcutPolicy: ShortcutPolicy.requireCreate);
|
||||||
shortcutPolicy: ShortcutPolicy.requireCreate,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppLifecycleListener? _appLifecycleListener;
|
AppLifecycleListener? _appLifecycleListener;
|
||||||
@@ -427,20 +475,26 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
|
|
||||||
_isBusy = true;
|
_isBusy = true;
|
||||||
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
||||||
_appLifecycleListener = AppLifecycleListener(
|
_appLifecycleListener =
|
||||||
onExitRequested: _onExitRequested,
|
AppLifecycleListener(onExitRequested: _onExitRequested);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_trayInitialization();
|
try {
|
||||||
_hotkeyInitialization();
|
_trayInitialization();
|
||||||
_notifyInitialization();
|
_hotkeyInitialization();
|
||||||
_initialize().then((_) {
|
_notifyInitialization();
|
||||||
_postInitialization();
|
_initialize().then((_) {
|
||||||
_tryRequestRating();
|
_postInitialization();
|
||||||
_checkForUpdate();
|
_tryRequestRating();
|
||||||
setState(() => _isBusy = false);
|
_checkForUpdate();
|
||||||
});
|
setState(() => _isBusy = false);
|
||||||
|
}).catchError((err) {
|
||||||
|
logging.error('[Bootstrap] Unable to initialize app', err);
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logging.error('[Bootstrap] Unable to initialize (pre-stage) app', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AppExitResponse> _onExitRequested() async {
|
Future<AppExitResponse> _onExitRequested() async {
|
||||||
@@ -449,6 +503,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _quitApp() {
|
void _quitApp() {
|
||||||
|
_saveWindowSize();
|
||||||
_appLifecycleListener?.dispose();
|
_appLifecycleListener?.dispose();
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
appWindow.close();
|
appWindow.close();
|
||||||
@@ -530,41 +585,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
});
|
});
|
||||||
return SizeChangedLayoutNotifier(
|
return SizeChangedLayoutNotifier(
|
||||||
child: _isBusy
|
child: _isBusy
|
||||||
? Material(
|
? _AppLoadingScreen(
|
||||||
key: Key('app-splash-screen-$_isBusy'),
|
isBusy: _isBusy,
|
||||||
child: Stack(
|
initPercentage: _initPercentage,
|
||||||
children: [
|
phaseText: _phaseText,
|
||||||
CustomPaint(painter: GraphPainter()),
|
|
||||||
Center(
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
maxWidth: 240,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Image.asset(
|
|
||||||
'assets/icon/icon.png',
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
Text('Solar Network').bold(),
|
|
||||||
AppVersionLabel(),
|
|
||||||
Gap(8),
|
|
||||||
Text(
|
|
||||||
_phaseText,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
Gap(16),
|
|
||||||
const LinearProgressIndicator(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: widget.child,
|
: widget.child,
|
||||||
);
|
);
|
||||||
@@ -575,43 +599,233 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GraphPainter extends CustomPainter {
|
class _AppLoadingScreen extends StatelessWidget {
|
||||||
final Random random = Random();
|
const _AppLoadingScreen({
|
||||||
final int numNodes = 20;
|
required this.isBusy,
|
||||||
final double maxDistance = 100; // Max distance to draw a line
|
required this.initPercentage,
|
||||||
|
required this.phaseText,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isBusy;
|
||||||
|
final double initPercentage;
|
||||||
|
final String phaseText;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
Widget build(BuildContext context) {
|
||||||
final paintNode = Paint()..color = Colors.white;
|
if (ResponsiveScaffold.getIsExpand(context)) {
|
||||||
final paintEdge = Paint()
|
return Material(
|
||||||
..color = Colors.white.withOpacity(0.3)
|
key: Key('app-splash-screen-$isBusy'),
|
||||||
..strokeWidth = 1;
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage('assets/icon/kanban-1st.jpg'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
opacity: 0.1,
|
||||||
|
),
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
backgroundBlendMode: BlendMode.darken,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(begin: 0, end: initPercentage),
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
builder: (context, value, _) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('${(value * 100).toStringAsFixed(0)}%')
|
||||||
|
.padding(left: 32, bottom: 4),
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: value,
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(0),
|
||||||
|
),
|
||||||
|
stopIndicatorColor: Colors.transparent,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
const Gap(24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(begin: 0, end: initPercentage),
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
builder: (context, value, _) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text('${(value * 100).toStringAsFixed(0)}%')
|
||||||
|
.padding(right: 32, bottom: 4),
|
||||||
|
Transform.flip(
|
||||||
|
flipX: true,
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: value,
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(0),
|
||||||
|
),
|
||||||
|
stopIndicatorColor: Colors.transparent,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 240, minWidth: 160),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.surface.withOpacity(0.85),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'splashScreenServer',
|
||||||
|
style: GoogleFonts.notoSerifHk(height: 1, fontSize: 11),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr().opacity(0.85),
|
||||||
|
Text(
|
||||||
|
'splashScreenServerName',
|
||||||
|
style: GoogleFonts.notoSerifHk(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr().opacity(0.85),
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: '#',
|
||||||
|
style: GoogleFonts.notoSerifHk(),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: '0',
|
||||||
|
style: GoogleFonts.notoSerifHk(
|
||||||
|
fontSize: 80,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).padding(vertical: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: MediaQuery.of(context).size.height * 0.2,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
phaseText,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
AnimateWidgetExtensions(Text(
|
||||||
|
'splashScreenCaption',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr())
|
||||||
|
.animate(onPlay: (e) => e.repeat())
|
||||||
|
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
|
||||||
|
.then()
|
||||||
|
.fadeOut(
|
||||||
|
duration: 500.ms,
|
||||||
|
delay: 1000.ms,
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 8,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/icon/icon.png',
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
).padding(all: 4),
|
||||||
|
const Gap(4),
|
||||||
|
Text('Solar Network').bold(),
|
||||||
|
Expanded(child: const SizedBox()),
|
||||||
|
AppVersionLabel(),
|
||||||
|
const Gap(12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate random points
|
return Material(
|
||||||
List<Offset> nodes = List.generate(
|
key: Key('app-splash-screen-$isBusy'),
|
||||||
numNodes,
|
child: Stack(
|
||||||
(_) => Offset(
|
children: [
|
||||||
random.nextDouble() * size.width,
|
Container(
|
||||||
random.nextDouble() * size.height,
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage('assets/icon/kanban-1st.jpg'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
opacity: 0.1,
|
||||||
|
),
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
backgroundBlendMode: BlendMode.darken,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 240),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/icon/icon.png',
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
Text('Solar Network').bold(),
|
||||||
|
AppVersionLabel(),
|
||||||
|
Gap(8),
|
||||||
|
Text(phaseText, textAlign: TextAlign.center),
|
||||||
|
Gap(16),
|
||||||
|
TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(begin: 0, end: initPercentage),
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
builder: (context, value, _) =>
|
||||||
|
LinearProgressIndicator(value: value),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw edges between close nodes
|
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
|
||||||
for (var j = i + 1; j < nodes.length; j++) {
|
|
||||||
double distance = (nodes[i] - nodes[j]).distance;
|
|
||||||
if (distance < maxDistance) {
|
|
||||||
canvas.drawLine(nodes[i], nodes[j], paintEdge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw nodes
|
|
||||||
for (var node in nodes) {
|
|
||||||
canvas.drawCircle(node, 4, paintNode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addAvailableChannel(SnChannel channel) {
|
||||||
|
_availableChannels.add(channel);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
|
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
channels.map(
|
channels.map(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:livekit_noise_filter/livekit_noise_filter.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
@@ -131,10 +132,14 @@ class ChatCallProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
void initRoom() {
|
void initRoom() {
|
||||||
initHardware();
|
initHardware();
|
||||||
|
final timeout = const Duration(seconds: 60);
|
||||||
_room = Room(
|
_room = Room(
|
||||||
roomOptions: const RoomOptions(
|
roomOptions: RoomOptions(
|
||||||
dynacast: true,
|
dynacast: true,
|
||||||
adaptiveStream: true,
|
adaptiveStream: true,
|
||||||
|
defaultAudioCaptureOptions: AudioCaptureOptions(
|
||||||
|
processor: LiveKitNoiseFilter(),
|
||||||
|
),
|
||||||
defaultAudioPublishOptions: AudioPublishOptions(
|
defaultAudioPublishOptions: AudioPublishOptions(
|
||||||
name: 'call_voice',
|
name: 'call_voice',
|
||||||
stream: 'call_stream',
|
stream: 'call_stream',
|
||||||
@@ -154,6 +159,16 @@ class ChatCallProvider extends ChangeNotifier {
|
|||||||
params: VideoParametersPresets.h1080_169,
|
params: VideoParametersPresets.h1080_169,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
connectOptions: ConnectOptions(
|
||||||
|
autoSubscribe: true,
|
||||||
|
timeouts: Timeouts(
|
||||||
|
connection: timeout,
|
||||||
|
debounce: timeout,
|
||||||
|
publish: timeout,
|
||||||
|
peerConnection: timeout,
|
||||||
|
iceRestart: timeout,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
_listener = _room.createListener();
|
_listener = _room.createListener();
|
||||||
WakelockPlus.enable();
|
WakelockPlus.enable();
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const kNetworkServerStoreKey = 'app_server_url';
|
|||||||
const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
||||||
const kAppBackgroundStoreKey = 'app_has_background';
|
const kAppBackgroundStoreKey = 'app_has_background';
|
||||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
||||||
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';
|
||||||
@@ -22,6 +21,9 @@ const kAppCustomFonts = 'app_custom_fonts';
|
|||||||
const kAppMixedFeed = 'app_mixed_feed';
|
const kAppMixedFeed = 'app_mixed_feed';
|
||||||
const kAppAutoTranslate = 'app_auto_translate';
|
const kAppAutoTranslate = 'app_auto_translate';
|
||||||
const kAppHideBottomNav = 'app_hide_bottom_nav';
|
const kAppHideBottomNav = 'app_hide_bottom_nav';
|
||||||
|
const kAppSoundEffects = 'app_sound_effects';
|
||||||
|
const kAppAprilFoolFeatures = 'app_april_fool_features';
|
||||||
|
const kAppWindowSize = 'app_window_size';
|
||||||
|
|
||||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||||
'settingsImageQualityLowest': FilterQuality.none,
|
'settingsImageQualityLowest': FilterQuality.none,
|
||||||
@@ -44,27 +46,17 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool drawerIsCollapsed = false;
|
bool drawerIsCollapsed = false;
|
||||||
bool drawerIsExpanded = false;
|
|
||||||
|
|
||||||
void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
|
void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
|
||||||
bool newDrawerIsCollapsed = false;
|
bool newDrawerIsCollapsed = false;
|
||||||
bool newDrawerIsExpanded = false;
|
|
||||||
if (withMediaQuery) {
|
if (withMediaQuery) {
|
||||||
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600;
|
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600;
|
||||||
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);
|
||||||
newDrawerIsExpanded = rpb.largerThan(TABLET)
|
|
||||||
? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
|
|
||||||
? false
|
|
||||||
: true
|
|
||||||
: false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newDrawerIsExpanded != drawerIsExpanded ||
|
if (newDrawerIsCollapsed != drawerIsCollapsed) {
|
||||||
newDrawerIsCollapsed != drawerIsCollapsed) {
|
|
||||||
drawerIsExpanded = newDrawerIsExpanded;
|
|
||||||
drawerIsCollapsed = newDrawerIsCollapsed;
|
drawerIsCollapsed = newDrawerIsCollapsed;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -96,6 +88,24 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
return prefs.getBool(kAppHideBottomNav) ?? false;
|
return prefs.getBool(kAppHideBottomNav) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get aprilFoolFeatures {
|
||||||
|
return prefs.getBool(kAppAprilFoolFeatures) ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get soundEffects {
|
||||||
|
return prefs.getBool(kAppSoundEffects) ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
set soundEffects(bool value) {
|
||||||
|
prefs.setBool(kAppSoundEffects, value);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
set aprilFoolFeatures(bool value) {
|
||||||
|
prefs.setBool(kAppAprilFoolFeatures, value);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
set hideBottomNav(bool value) {
|
set hideBottomNav(bool value) {
|
||||||
prefs.setBool(kAppHideBottomNav, value);
|
prefs.setBool(kAppHideBottomNav, value);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class KeyPairProvider {
|
|||||||
|
|
||||||
Future<SnKeyPair?> reloadActive({bool autoEnroll = true}) async {
|
Future<SnKeyPair?> reloadActive({bool autoEnroll = true}) async {
|
||||||
final kp = await (_dt.db.snLocalKeyPair.select()
|
final kp = await (_dt.db.snLocalKeyPair.select()
|
||||||
..where((e) => e.accountId.equals(_ua.user!.id))
|
..where((e) => e.accountId.equals(_ua.user?.id ?? 0))
|
||||||
..where((e) => e.privateKey.isNotNull())
|
..where((e) => e.privateKey.isNotNull())
|
||||||
..where((e) => e.isActive.equals(true))
|
..where((e) => e.isActive.equals(true))
|
||||||
..limit(1))
|
..limit(1))
|
||||||
|
|||||||
@@ -4,7 +4,20 @@ import 'package:flutter/material.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:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
|
||||||
|
class AppNavListItem {
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final String screen;
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
const AppNavListItem({
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.screen,
|
||||||
|
required this.icon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class AppNavDestination {
|
class AppNavDestination {
|
||||||
final String label;
|
final String label;
|
||||||
@@ -46,11 +59,6 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
screen: 'chat',
|
screen: 'chat',
|
||||||
label: 'screenChat',
|
label: 'screenChat',
|
||||||
),
|
),
|
||||||
AppNavDestination(
|
|
||||||
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
|
|
||||||
screen: 'account',
|
|
||||||
label: 'screenAccount',
|
|
||||||
),
|
|
||||||
AppNavDestination(
|
AppNavDestination(
|
||||||
icon: Icon(Symbols.group, weight: 400, opticalSize: 20),
|
icon: Icon(Symbols.group, weight: 400, opticalSize: 20),
|
||||||
screen: 'realm',
|
screen: 'realm',
|
||||||
@@ -62,24 +70,9 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
label: 'screenNews',
|
label: 'screenNews',
|
||||||
),
|
),
|
||||||
AppNavDestination(
|
AppNavDestination(
|
||||||
icon: Icon(Symbols.emoji_emotions, weight: 400, opticalSize: 20),
|
icon: Icon(Symbols.settings, weight: 400, opticalSize: 20),
|
||||||
screen: 'stickers',
|
screen: 'settings',
|
||||||
label: 'screenStickers',
|
label: 'screenSettings',
|
||||||
),
|
|
||||||
AppNavDestination(
|
|
||||||
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
|
||||||
screen: 'album',
|
|
||||||
label: 'screenAlbum',
|
|
||||||
),
|
|
||||||
AppNavDestination(
|
|
||||||
icon: Icon(Symbols.diversity_4, weight: 400, opticalSize: 20),
|
|
||||||
screen: 'friend',
|
|
||||||
label: 'screenFriend',
|
|
||||||
),
|
|
||||||
AppNavDestination(
|
|
||||||
icon: Icon(Symbols.notifications, weight: 400, opticalSize: 20),
|
|
||||||
screen: 'notification',
|
|
||||||
label: 'screenNotification',
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
static const List<String> kDefaultPinnedDestination = [
|
static const List<String> kDefaultPinnedDestination = [
|
||||||
@@ -141,11 +134,4 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
_currentIndex = idx;
|
_currentIndex = idx;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
SnRealm? focusedRealm;
|
|
||||||
|
|
||||||
void setFocusedRealm(SnRealm? realm) {
|
|
||||||
focusedRealm = realm;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@@ -22,6 +23,8 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
late final WebSocketProvider _ws;
|
late final WebSocketProvider _ws;
|
||||||
late final ConfigProvider _cfg;
|
late final ConfigProvider _cfg;
|
||||||
|
|
||||||
|
final AudioPlayer _notifySoundPlayer = AudioPlayer(playerId: 'notify-sound');
|
||||||
|
|
||||||
NotificationProvider(BuildContext context) {
|
NotificationProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
_ua = context.read<UserProvider>();
|
_ua = context.read<UserProvider>();
|
||||||
@@ -66,14 +69,19 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
logging.info('[Push Notification] Device Push Token is $token');
|
logging.info('[Push Notification] Device Push Token is $token');
|
||||||
|
|
||||||
await _sn.client.post(
|
try {
|
||||||
'/cgi/id/notifications/subscription',
|
await _sn.client.post(
|
||||||
data: {
|
'/cgi/id/notifications/subscription',
|
||||||
'provider': provider,
|
data: {
|
||||||
'device_token': token,
|
'provider': provider,
|
||||||
'device_id': deviceUuid,
|
'device_token': token,
|
||||||
},
|
'device_id': deviceUuid
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logging.error(
|
||||||
|
'[Push Notification] Unable to register push notifications: $err');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int showingCount = 0;
|
int showingCount = 0;
|
||||||
@@ -91,6 +99,25 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||||
if (doHaptic) HapticFeedback.mediumImpact();
|
if (doHaptic) HapticFeedback.mediumImpact();
|
||||||
|
|
||||||
|
// April fool notification sfx
|
||||||
|
if (_cfg.prefs.getBool(kAppAprilFoolFeatures) ?? true) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (now.day == 1 && now.month == 4) {
|
||||||
|
_notifySoundPlayer.play(
|
||||||
|
AssetSource('audio/notify/metal-pipe.mp3'),
|
||||||
|
volume: 0.6,
|
||||||
|
ctx: AudioContext(
|
||||||
|
android: AudioContextAndroid(
|
||||||
|
contentType: AndroidContentType.sonification,
|
||||||
|
usageType: AndroidUsageType.notificationEvent,
|
||||||
|
),
|
||||||
|
iOS: AudioContextIOS(category: AVAudioSessionCategory.ambient),
|
||||||
|
),
|
||||||
|
mode: PlayerMode.lowLatency,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (notification.topic == 'messaging.message' &&
|
if (notification.topic == 'messaging.message' &&
|
||||||
skippableNotifyChannel != null) {
|
skippableNotifyChannel != null) {
|
||||||
if (notification.metadata['channel_id'] != null &&
|
if (notification.metadata['channel_id'] != null &&
|
||||||
|
|||||||
@@ -1,144 +1,31 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
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_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/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 SnAttachmentProvider _attach;
|
|
||||||
late final SnRealmProvider _realm;
|
|
||||||
|
|
||||||
SnPostContentProvider(BuildContext context) {
|
SnPostContentProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
_ud = context.read<UserDirectoryProvider>();
|
|
||||||
_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 {
|
||||||
Set<String> rids = {};
|
|
||||||
Set<int> uids = {};
|
|
||||||
for (var i = 0; i < out.length; i++) {
|
|
||||||
rids.addAll(out[i].body['attachments']?.cast<String>() ?? []);
|
|
||||||
if (out[i].body['thumbnail'] != null) {
|
|
||||||
rids.add(out[i].body['thumbnail']);
|
|
||||||
}
|
|
||||||
if (out[i].body['video'] != null) {
|
|
||||||
rids.add(out[i].body['video']);
|
|
||||||
}
|
|
||||||
if (out[i].repostTo != null) {
|
|
||||||
out[i] = out[i].copyWith(
|
|
||||||
repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (out[i].publisher.type == 0) {
|
|
||||||
uids.add(out[i].publisher.accountId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final attachments = await _attach.getMultiple(rids.toList());
|
|
||||||
for (var i = 0; i < out.length; i++) {
|
|
||||||
SnPoll? poll;
|
|
||||||
SnRealm? realm;
|
|
||||||
if (out[i].pollId != null) {
|
|
||||||
poll = await _fetchPoll(out[i].pollId!);
|
|
||||||
}
|
|
||||||
if (out[i].realmId != null) {
|
|
||||||
realm = await _realm.getRealm(out[i].realmId!);
|
|
||||||
}
|
|
||||||
|
|
||||||
out[i] = out[i].copyWith(
|
|
||||||
preload: SnPostPreload(
|
|
||||||
thumbnail: attachments
|
|
||||||
.where((ele) => ele?.rid == out[i].body['thumbnail'])
|
|
||||||
.firstOrNull,
|
|
||||||
attachments: attachments
|
|
||||||
.where((ele) =>
|
|
||||||
out[i].body['attachments']?.contains(ele?.rid) ?? false)
|
|
||||||
.toList(),
|
|
||||||
video: attachments
|
|
||||||
.where((ele) => ele?.rid == out[i].body['video'])
|
|
||||||
.firstOrNull,
|
|
||||||
poll: poll,
|
|
||||||
realm: realm,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
uids.addAll(
|
|
||||||
attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
|
|
||||||
await _ud.listAccount(uids);
|
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SnPost> _preloadRelatedDataSingle(SnPost out) async {
|
Future<SnPost> _preloadRelatedDataSingle(SnPost out) async {
|
||||||
Set<String> rids = {};
|
|
||||||
Set<int> uids = {};
|
|
||||||
rids.addAll(out.body['attachments']?.cast<String>() ?? []);
|
|
||||||
if (out.body['thumbnail'] != null) {
|
|
||||||
rids.add(out.body['thumbnail']);
|
|
||||||
}
|
|
||||||
if (out.body['video'] != null) {
|
|
||||||
rids.add(out.body['video']);
|
|
||||||
}
|
|
||||||
if (out.repostTo != null) {
|
|
||||||
out = out.copyWith(
|
|
||||||
repostTo: await _preloadRelatedDataSingle(out.repostTo!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (out.publisher.type == 0) {
|
|
||||||
uids.add(out.publisher.accountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
final attachments = await _attach.getMultiple(rids.toList());
|
|
||||||
|
|
||||||
SnPoll? poll;
|
|
||||||
SnRealm? realm;
|
|
||||||
if (out.pollId != null) {
|
|
||||||
poll = await _fetchPoll(out.pollId!);
|
|
||||||
}
|
|
||||||
if (out.realmId != null) {
|
|
||||||
realm = await _realm.getRealm(out.realmId!);
|
|
||||||
}
|
|
||||||
|
|
||||||
out = out.copyWith(
|
|
||||||
preload: SnPostPreload(
|
|
||||||
thumbnail: attachments
|
|
||||||
.where((ele) => ele?.rid == out.body['thumbnail'])
|
|
||||||
.firstOrNull,
|
|
||||||
attachments: attachments
|
|
||||||
.where(
|
|
||||||
(ele) => out.body['attachments']?.contains(ele?.rid) ?? false)
|
|
||||||
.toList(),
|
|
||||||
video: attachments
|
|
||||||
.where((ele) => ele?.rid == out.body['video'])
|
|
||||||
.firstOrNull,
|
|
||||||
poll: poll,
|
|
||||||
realm: realm,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
uids.addAll(
|
|
||||||
attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
|
|
||||||
await _ud.listAccount(uids);
|
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SnPost>> listRecommendations() async {
|
Future<List<SnPost>> listRecommendations() async {
|
||||||
final resp = await _sn.client.get('/cgi/co/recommendations');
|
final resp = await _sn.client.get(
|
||||||
|
'/cgi/co/recommendations',
|
||||||
|
options: Options(headers: {
|
||||||
|
'X-API-Version': '2',
|
||||||
|
}),
|
||||||
|
);
|
||||||
final out = _preloadRelatedDataInBatch(
|
final out = _preloadRelatedDataInBatch(
|
||||||
List.from(resp.data.map((ele) => SnPost.fromJson(ele))),
|
List.from(resp.data.map((ele) => SnPost.fromJson(ele))),
|
||||||
);
|
);
|
||||||
@@ -146,11 +33,14 @@ class SnPostContentProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SnFeedEntry>> getFeed({int take = 20, DateTime? cursor}) async {
|
Future<List<SnFeedEntry>> getFeed({int take = 20, DateTime? cursor}) async {
|
||||||
final resp =
|
final resp = await _sn.client.get(
|
||||||
await _sn.client.get('/cgi/co/recommendations/feed', queryParameters: {
|
'/cgi/co/recommendations/feed',
|
||||||
'take': take,
|
queryParameters: {
|
||||||
if (cursor != null) 'cursor': cursor.toUtc().millisecondsSinceEpoch,
|
'take': take,
|
||||||
});
|
if (cursor != null) 'cursor': cursor.toUtc().millisecondsSinceEpoch,
|
||||||
|
},
|
||||||
|
options: Options(headers: {'X-API-Version': '2'}),
|
||||||
|
);
|
||||||
final List<SnFeedEntry> out =
|
final List<SnFeedEntry> out =
|
||||||
List.from(resp.data.map((ele) => SnFeedEntry.fromJson(ele)));
|
List.from(resp.data.map((ele) => SnFeedEntry.fromJson(ele)));
|
||||||
|
|
||||||
@@ -202,6 +92,9 @@ class SnPostContentProvider {
|
|||||||
if (realm != null) 'realm': realm,
|
if (realm != null) 'realm': realm,
|
||||||
if (channel != null) 'channel': channel,
|
if (channel != null) 'channel': channel,
|
||||||
},
|
},
|
||||||
|
options: Options(headers: {
|
||||||
|
'X-API-Version': '2',
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
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)) ?? []),
|
||||||
@@ -215,11 +108,16 @@ class SnPostContentProvider {
|
|||||||
int take = 10,
|
int take = 10,
|
||||||
int offset = 0,
|
int offset = 0,
|
||||||
}) async {
|
}) async {
|
||||||
final resp = await _sn.client
|
final resp = await _sn.client.get(
|
||||||
.get('/cgi/co/posts/$parentId/replies', queryParameters: {
|
'/cgi/co/posts/$parentId/replies',
|
||||||
'take': take,
|
queryParameters: {
|
||||||
'offset': offset,
|
'take': take,
|
||||||
});
|
'offset': offset,
|
||||||
|
},
|
||||||
|
options: Options(headers: {
|
||||||
|
'X-API-Version': '2',
|
||||||
|
}),
|
||||||
|
);
|
||||||
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)) ?? []),
|
||||||
);
|
);
|
||||||
@@ -234,13 +132,20 @@ class SnPostContentProvider {
|
|||||||
Iterable<String>? tags,
|
Iterable<String>? tags,
|
||||||
Iterable<String>? categories,
|
Iterable<String>? categories,
|
||||||
}) async {
|
}) async {
|
||||||
final resp = await _sn.client.get('/cgi/co/posts/search', queryParameters: {
|
final resp = await _sn.client.get(
|
||||||
'take': take,
|
'/cgi/co/posts/search',
|
||||||
'offset': offset,
|
queryParameters: {
|
||||||
'probe': searchTerm,
|
'take': take,
|
||||||
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
'offset': offset,
|
||||||
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
|
'probe': searchTerm,
|
||||||
});
|
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
||||||
|
if (categories?.isNotEmpty ?? false)
|
||||||
|
'categories': categories!.join(','),
|
||||||
|
},
|
||||||
|
options: Options(headers: {
|
||||||
|
'X-API-Version': '2',
|
||||||
|
}),
|
||||||
|
);
|
||||||
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)) ?? []),
|
||||||
);
|
);
|
||||||
@@ -249,7 +154,12 @@ class SnPostContentProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<SnPost> getPost(dynamic id) async {
|
Future<SnPost> getPost(dynamic id) async {
|
||||||
final resp = await _sn.client.get('/cgi/co/posts/$id');
|
final resp = await _sn.client.get(
|
||||||
|
'/cgi/co/posts/$id',
|
||||||
|
options: Options(headers: {
|
||||||
|
'X-API-Version': '2',
|
||||||
|
}),
|
||||||
|
);
|
||||||
final out = _preloadRelatedDataSingle(
|
final out = _preloadRelatedDataSingle(
|
||||||
SnPost.fromJson(resp.data),
|
SnPost.fromJson(resp.data),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -249,8 +249,11 @@ class SnNetworkProvider {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getAttachmentUrl(String ky) {
|
String getAttachmentUrl(String ky, {bool preview = true}) {
|
||||||
if (ky.startsWith("http")) return ky;
|
if (ky.startsWith("http")) return ky;
|
||||||
|
if (!preview) {
|
||||||
|
return '${client.options.baseUrl}/cgi/uc/attachments/$ky?preview=false';
|
||||||
|
}
|
||||||
return '${client.options.baseUrl}/cgi/uc/attachments/$ky';
|
return '${client.options.baseUrl}/cgi/uc/attachments/$ky';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:surface/providers/database.dart';
|
|||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
|
|
||||||
class SnRealmProvider {
|
class SnRealmProvider extends ChangeNotifier {
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
late final DatabaseProvider _dt;
|
late final DatabaseProvider _dt;
|
||||||
|
|
||||||
@@ -39,6 +39,11 @@ class SnRealmProvider {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addAvailableRealm(SnRealm realm) {
|
||||||
|
_availableRealms.add(realm);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<SnRealm> getRealm(dynamic aliasOrId) async {
|
Future<SnRealm> getRealm(dynamic aliasOrId) async {
|
||||||
if (_cache.containsKey(aliasOrId.toString())) {
|
if (_cache.containsKey(aliasOrId.toString())) {
|
||||||
return _cache[aliasOrId.toString()]!;
|
return _cache[aliasOrId.toString()]!;
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import 'package:crypto/crypto.dart';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:surface/logger.dart';
|
import 'package:surface/logger.dart';
|
||||||
|
|
||||||
// TODO self host translate api
|
const kTranslateApiBaseUrl = 'https://translate.solsynth.dev';
|
||||||
const kTranslateApiBaseUrl = 'https://translate.disroot.org';
|
|
||||||
|
|
||||||
class SnTranslator {
|
class SnTranslator {
|
||||||
final Dio client = Dio(
|
final Dio client = Dio(
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class UserProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<SnAccount?> refreshUser() async {
|
Future<SnAccount?> refreshUser() async {
|
||||||
|
if (!isAuthorized) return null;
|
||||||
final resp = await _sn.client.get('/cgi/id/users/me');
|
final resp = await _sn.client.get('/cgi/id/users/me');
|
||||||
final out = SnAccount.fromJson(resp.data);
|
final out = SnAccount.fromJson(resp.data);
|
||||||
|
|
||||||
|
|||||||
306
lib/router.dart
306
lib/router.dart
@@ -1,9 +1,9 @@
|
|||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
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/punishments.dart';
|
||||||
|
import 'package:surface/screens/account/settings.dart';
|
||||||
import 'package:surface/screens/account/action_events.dart';
|
import 'package:surface/screens/account/action_events.dart';
|
||||||
import 'package:surface/screens/account/badges.dart';
|
import 'package:surface/screens/account/badges.dart';
|
||||||
import 'package:surface/screens/account/contact_methods.dart';
|
import 'package:surface/screens/account/contact_methods.dart';
|
||||||
@@ -13,6 +13,7 @@ import 'package:surface/screens/account/prefs/notify.dart';
|
|||||||
import 'package:surface/screens/account/prefs/security.dart';
|
import 'package:surface/screens/account/prefs/security.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/programs.dart';
|
||||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||||
import 'package:surface/screens/account/publishers/publisher_new.dart';
|
import 'package:surface/screens/account/publishers/publisher_new.dart';
|
||||||
import 'package:surface/screens/account/publishers/publishers.dart';
|
import 'package:surface/screens/account/publishers/publishers.dart';
|
||||||
@@ -52,16 +53,6 @@ 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(BuildContext context, Animation<double> animation,
|
|
||||||
Animation<double> secondaryAnimation, Widget child) {
|
|
||||||
return FadeThroughTransition(
|
|
||||||
animation: animation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final _appRoutes = [
|
final _appRoutes = [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -70,8 +61,8 @@ final _appRoutes = [
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/posts',
|
path: '/posts',
|
||||||
name: 'explore',
|
name: 'posts',
|
||||||
builder: (context, state) => const ExploreScreen(),
|
builder: (_, __) => const SizedBox.shrink(),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/draft',
|
path: '/draft',
|
||||||
@@ -109,156 +100,201 @@ final _appRoutes = [
|
|||||||
state.uri.queryParameters['categories']?.split(','),
|
state.uri.queryParameters['categories']?.split(','),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ShellRoute(
|
||||||
|
builder: (context, state, child) => ResponsiveScaffold(
|
||||||
|
asideFlex: 2,
|
||||||
|
contentFlex: 3,
|
||||||
|
aside: const ExploreScreen(),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/explore',
|
||||||
|
name: 'explore',
|
||||||
|
builder: (context, state) => const ResponsiveScaffoldLanding(
|
||||||
|
child: ExploreScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/posts/:slug',
|
||||||
|
name: 'postDetail',
|
||||||
|
builder: (context, state) => PostDetailScreen(
|
||||||
|
key: ValueKey(state.pathParameters['slug']!),
|
||||||
|
slug: state.pathParameters['slug']!,
|
||||||
|
preload: state.extra as SnPost?,
|
||||||
|
),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/publishers/:name',
|
path: '/publishers/:name',
|
||||||
name: 'postPublisher',
|
name: 'postPublisher',
|
||||||
builder: (context, state) =>
|
builder: (context, state) =>
|
||||||
PostPublisherScreen(name: state.pathParameters['name']!),
|
PostPublisherScreen(name: state.pathParameters['name']!),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
path: '/:slug',
|
|
||||||
name: 'postDetail',
|
|
||||||
builder: (context, state) => PostDetailScreen(
|
|
||||||
slug: state.pathParameters['slug']!,
|
|
||||||
preload: state.extra as SnPost?,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
ShellRoute(
|
||||||
path: '/account',
|
builder: (context, state, child) => ResponsiveScaffold(
|
||||||
name: 'account',
|
aside: const AccountScreen(),
|
||||||
builder: (context, state) => const AccountScreen(),
|
child: child,
|
||||||
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/contacts',
|
path: '/account',
|
||||||
name: 'accountContactMethods',
|
name: 'account',
|
||||||
builder: (context, state) => const AccountContactMethod(),
|
builder: (context, state) =>
|
||||||
),
|
const ResponsiveScaffoldLanding(child: AccountScreen()),
|
||||||
GoRoute(
|
|
||||||
path: '/events',
|
|
||||||
name: 'accountActionEvents',
|
|
||||||
builder: (context, state) => const ActionEventScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/tickets',
|
|
||||||
name: 'accountAuthTickets',
|
|
||||||
builder: (context, state) => const AccountAuthTicket(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/badges',
|
|
||||||
name: 'accountBadges',
|
|
||||||
builder: (context, state) => const AccountBadgesScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/wallet',
|
|
||||||
name: 'accountWallet',
|
|
||||||
builder: (context, state) => const WalletScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/keypairs',
|
|
||||||
name: 'accountKeyPairs',
|
|
||||||
builder: (context, state) => const KeyPairScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/settings',
|
|
||||||
name: 'accountSettings',
|
|
||||||
builder: (context, state) => AccountSettingsScreen(),
|
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/notify',
|
path: '/punishments',
|
||||||
name: 'accountSettingsNotify',
|
name: 'accountPunishments',
|
||||||
builder: (context, state) => const AccountNotifyPrefsScreen(),
|
builder: (context, state) => const PunishmentsScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/auth',
|
path: '/programs',
|
||||||
name: 'accountSettingsSecurity',
|
name: 'accountProgram',
|
||||||
builder: (context, state) => const AccountSecurityPrefsScreen(),
|
builder: (context, state) => const AccountProgramScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/contacts',
|
||||||
|
name: 'accountContactMethods',
|
||||||
|
builder: (context, state) => const AccountContactMethod(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/events',
|
||||||
|
name: 'accountActionEvents',
|
||||||
|
builder: (context, state) => const ActionEventScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/tickets',
|
||||||
|
name: 'accountAuthTickets',
|
||||||
|
builder: (context, state) => const AccountAuthTicket(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/badges',
|
||||||
|
name: 'accountBadges',
|
||||||
|
builder: (context, state) => const AccountBadgesScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/wallet',
|
||||||
|
name: 'accountWallet',
|
||||||
|
builder: (context, state) => const WalletScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/keypairs',
|
||||||
|
name: 'accountKeyPairs',
|
||||||
|
builder: (context, state) => const KeyPairScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/settings',
|
||||||
|
name: 'accountSettings',
|
||||||
|
builder: (context, state) => AccountSettingsScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/notify',
|
||||||
|
name: 'accountSettingsNotify',
|
||||||
|
builder: (context, state) => const AccountNotifyPrefsScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/auth',
|
||||||
|
name: 'accountSettingsSecurity',
|
||||||
|
builder: (context, state) => const AccountSecurityPrefsScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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: '/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: '/profile/:name',
|
|
||||||
name: 'accountProfilePage',
|
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
|
||||||
child: UserScreen(name: state.pathParameters['name']!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/chat',
|
path: '/accounts/:name',
|
||||||
name: 'chat',
|
name: 'accountProfilePage',
|
||||||
builder: (context, state) => const ChatScreen(),
|
pageBuilder: (context, state) => NoTransitionPage(
|
||||||
|
child: UserScreen(name: state.pathParameters['name']!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ShellRoute(
|
||||||
|
builder: (context, state, child) =>
|
||||||
|
ResponsiveScaffold(aside: const ChatScreen(), child: child),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/:scope/:alias',
|
path: '/chat',
|
||||||
name: 'chatRoom',
|
name: 'chat',
|
||||||
builder: (context, state) => ChatRoomScreen(
|
builder: (context, state) => const ResponsiveScaffoldLanding(
|
||||||
scope: state.pathParameters['scope']!,
|
child: ChatScreen(),
|
||||||
alias: state.pathParameters['alias']!,
|
|
||||||
extra: state.extra as ChatRoomScreenExtra?,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/:scope/:alias/call',
|
|
||||||
name: 'chatCallRoom',
|
|
||||||
builder: (context, state) => CallRoomScreen(
|
|
||||||
scope: state.pathParameters['scope']!,
|
|
||||||
alias: state.pathParameters['alias']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/:scope/:alias/detail',
|
|
||||||
name: 'channelDetail',
|
|
||||||
builder: (context, state) => ChannelDetailScreen(
|
|
||||||
scope: state.pathParameters['scope']!,
|
|
||||||
alias: state.pathParameters['alias']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/manage',
|
|
||||||
name: 'chatManage',
|
|
||||||
builder: (context, state) => ChatManageScreen(
|
|
||||||
editingChannelAlias: state.uri.queryParameters['editing'],
|
|
||||||
),
|
),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/:scope/:alias',
|
||||||
|
name: 'chatRoom',
|
||||||
|
builder: (context, state) => ChatRoomScreen(
|
||||||
|
key: ValueKey(
|
||||||
|
'${state.pathParameters['scope']!}:${state.pathParameters['alias']!}',
|
||||||
|
),
|
||||||
|
scope: state.pathParameters['scope']!,
|
||||||
|
alias: state.pathParameters['alias']!,
|
||||||
|
extra: state.extra as ChatRoomScreenExtra?,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/:scope/:alias/call',
|
||||||
|
name: 'chatCallRoom',
|
||||||
|
builder: (context, state) => CallRoomScreen(
|
||||||
|
scope: state.pathParameters['scope']!,
|
||||||
|
alias: state.pathParameters['alias']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/:scope/:alias/detail',
|
||||||
|
name: 'channelDetail',
|
||||||
|
builder: (context, state) => ChannelDetailScreen(
|
||||||
|
scope: state.pathParameters['scope']!,
|
||||||
|
alias: state.pathParameters['alias']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/manage',
|
||||||
|
name: 'chatManage',
|
||||||
|
builder: (context, state) => ChatManageScreen(
|
||||||
|
editingChannelAlias: state.uri.queryParameters['editing'],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/realm',
|
path: '/realm',
|
||||||
name: 'realm',
|
name: 'realm',
|
||||||
pageBuilder: (context, state) => CustomTransitionPage(
|
builder: (context, state) => const RealmScreen(),
|
||||||
transitionsBuilder: _fadeThroughTransition,
|
|
||||||
child: const RealmScreen(),
|
|
||||||
),
|
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/:alias/community',
|
path: '/:alias/community',
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ 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/database.dart';
|
||||||
|
import 'package:surface/providers/navigation.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';
|
||||||
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_status.dart';
|
import 'package:surface/widgets/account/account_status.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';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
@@ -22,27 +22,97 @@ import 'package:surface/widgets/universal_image.dart';
|
|||||||
class AccountScreen extends StatelessWidget {
|
class AccountScreen extends StatelessWidget {
|
||||||
const AccountScreen({super.key});
|
const AccountScreen({super.key});
|
||||||
|
|
||||||
|
static const List<AppNavListItem> kNavList = [
|
||||||
|
AppNavListItem(
|
||||||
|
title: "accountPublishers",
|
||||||
|
subtitle: "accountPublishersSubtitle",
|
||||||
|
screen: "accountPublishers",
|
||||||
|
icon: Symbols.face,
|
||||||
|
),
|
||||||
|
AppNavListItem(
|
||||||
|
title: "accountProgram",
|
||||||
|
subtitle: "accountProgramDescription",
|
||||||
|
screen: "accountProgram",
|
||||||
|
icon: Symbols.communities,
|
||||||
|
),
|
||||||
|
AppNavListItem(
|
||||||
|
title: "friends",
|
||||||
|
subtitle: "friendsDescription",
|
||||||
|
screen: "friend",
|
||||||
|
icon: Symbols.person,
|
||||||
|
),
|
||||||
|
AppNavListItem(
|
||||||
|
title: "album",
|
||||||
|
subtitle: "albumDescription",
|
||||||
|
screen: "album",
|
||||||
|
icon: Symbols.photo_library,
|
||||||
|
),
|
||||||
|
AppNavListItem(
|
||||||
|
title: "stickers",
|
||||||
|
subtitle: "stickersDescription",
|
||||||
|
screen: "stickers",
|
||||||
|
icon: Symbols.emoji_emotions,
|
||||||
|
),
|
||||||
|
AppNavListItem(
|
||||||
|
title: "accountWallet",
|
||||||
|
subtitle: "accountWalletSubtitle",
|
||||||
|
screen: "accountWallet",
|
||||||
|
icon: Symbols.wallet,
|
||||||
|
),
|
||||||
|
AppNavListItem(
|
||||||
|
title: "accountBadges",
|
||||||
|
subtitle: "accountBadgesDescription",
|
||||||
|
screen: "accountBadges",
|
||||||
|
icon: Symbols.award_star,
|
||||||
|
),
|
||||||
|
AppNavListItem(
|
||||||
|
title: "accountKeyPairs",
|
||||||
|
subtitle: "accountKeyPairsDescription",
|
||||||
|
screen: "accountKeyPairs",
|
||||||
|
icon: Symbols.key,
|
||||||
|
),
|
||||||
|
AppNavListItem(
|
||||||
|
title: "accountPunishments",
|
||||||
|
subtitle: "accountPunishmentsDescription",
|
||||||
|
screen: "accountPunishments",
|
||||||
|
icon: Symbols.credit_score,
|
||||||
|
),
|
||||||
|
AppNavListItem(
|
||||||
|
title: "accountActionEvent",
|
||||||
|
subtitle: "accountActionEventDescription",
|
||||||
|
screen: "accountActionEvents",
|
||||||
|
icon: Symbols.history,
|
||||||
|
),
|
||||||
|
AppNavListItem(
|
||||||
|
title: "accountAuthTickets",
|
||||||
|
subtitle: "accountAuthTicketsDescription",
|
||||||
|
screen: "accountAuthTickets",
|
||||||
|
icon: Symbols.confirmation_number,
|
||||||
|
),
|
||||||
|
AppNavListItem(
|
||||||
|
title: "accountSettings",
|
||||||
|
subtitle: "accountSettingsSubtitle",
|
||||||
|
screen: "accountSettings",
|
||||||
|
icon: Symbols.manage_accounts,
|
||||||
|
),
|
||||||
|
AppNavListItem(
|
||||||
|
title: "abuseReport",
|
||||||
|
subtitle: "abuseReportActionDescription",
|
||||||
|
screen: "abuseReport",
|
||||||
|
icon: Symbols.flag,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
@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>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: const PageBackButton(),
|
||||||
title: Text(
|
title: Text("screenAccount").tr(),
|
||||||
"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
|
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
||||||
? Stack(
|
? Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
@@ -71,15 +141,6 @@ class AccountScreen extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.settings, fill: 1),
|
|
||||||
onPressed: () {
|
|
||||||
GoRouter.of(context).pushNamed('settings');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: ua.isAuthorized
|
child: ua.isAuthorized
|
||||||
@@ -118,7 +179,18 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AccountImage(content: ua.user!.avatar, radius: 28),
|
GestureDetector(
|
||||||
|
child: AccountImage(
|
||||||
|
content: ua.user!.avatar,
|
||||||
|
radius: 28,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context)
|
||||||
|
.pushNamed('accountProfilePage', pathParameters: {
|
||||||
|
'name': ua.user!.name,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
_AccountStatusWidget(account: ua.user!),
|
_AccountStatusWidget(account: ua.user!),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -147,115 +219,42 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}).padding(all: 20),
|
}).padding(all: 20),
|
||||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||||
ListTile(
|
for (final item in AccountScreen.kNavList)
|
||||||
title: Text('accountPublishers').tr(),
|
Tooltip(
|
||||||
subtitle: Text('accountPublishersSubtitle').tr(),
|
message: item.subtitle.tr(),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
child: ListTile(
|
||||||
leading: const Icon(Symbols.face),
|
minTileHeight: 48,
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
title: Text(item.title).tr(),
|
||||||
onTap: () {
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
GoRouter.of(context).pushNamed('accountPublishers');
|
leading: Icon(item.icon),
|
||||||
},
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
),
|
onTap: () {
|
||||||
ListTile(
|
GoRouter.of(context).pushNamed(item.screen);
|
||||||
title: Text('abuseReport').tr(),
|
},
|
||||||
subtitle: Text('abuseReportActionDescription').tr(),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
),
|
||||||
leading: const Icon(Symbols.flag),
|
Tooltip(
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
message: 'accountLogoutSubtitle'.tr(),
|
||||||
onTap: () {
|
child: ListTile(
|
||||||
GoRouter.of(context).pushNamed('abuseReport');
|
title: Text('accountLogout').tr(),
|
||||||
},
|
minTileHeight: 48,
|
||||||
),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
ListTile(
|
leading: const Icon(Symbols.logout),
|
||||||
title: Text('factorSettings').tr(),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
subtitle: Text('factorSettingsSubtitle').tr(),
|
onTap: () async {
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
final confirm = await context.showConfirmDialog(
|
||||||
leading: const Icon(Symbols.lock),
|
'accountLogoutConfirmTitle'.tr(),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
'accountLogoutConfirm'.tr(),
|
||||||
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('accountBadges').tr(),
|
|
||||||
subtitle: Text('accountBadgesDescription').tr(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
leading: const Icon(Symbols.award_star),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed('accountBadges');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text('accountKeyPairs').tr(),
|
|
||||||
subtitle: Text('accountKeyPairsDescription').tr(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
leading: const Icon(Symbols.key),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed('accountKeyPairs');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text('accountActionEvent').tr(),
|
|
||||||
subtitle: Text('accountActionEventDescription').tr(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
leading: const Icon(Symbols.history),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed('accountActionEvents');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text('accountAuthTickets').tr(),
|
|
||||||
subtitle: Text('accountAuthTicketsDescription').tr(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
leading: const Icon(Symbols.confirmation_number),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed('accountAuthTickets');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
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(
|
|
||||||
title: Text('accountLogout').tr(),
|
|
||||||
subtitle: Text('accountLogoutSubtitle').tr(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
leading: const Icon(Symbols.logout),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () async {
|
|
||||||
final confirm = await context.showConfirmDialog(
|
|
||||||
'accountLogoutConfirmTitle'.tr(),
|
|
||||||
'accountLogoutConfirm'.tr(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!confirm) return;
|
if (!confirm) return;
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
ua.logoutUser();
|
ua.logoutUser();
|
||||||
final ws = context.read<WebSocketProvider>();
|
final ws = context.read<WebSocketProvider>();
|
||||||
ws.disconnect();
|
ws.disconnect();
|
||||||
context.read<DatabaseProvider>().removeDatabase();
|
context.read<DatabaseProvider>().removeDatabase();
|
||||||
},
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -298,9 +297,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
|
|||||||
GoRouter.of(context).pushNamed('authLogin').then((value) {
|
GoRouter.of(context).pushNamed('authLogin').then((value) {
|
||||||
if (value == true && context.mounted) {
|
if (value == true && context.mounted) {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
context.showSnackbar('loginSuccess'.tr(args: [
|
ua.refreshUser();
|
||||||
'@${ua.user?.name} (${ua.user?.nick})',
|
|
||||||
]));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class _ActionEventScreenState extends State<ActionEventScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
title: Text('accountActionEvent').tr(),
|
title: Text('accountActionEvent').tr(),
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class _AccountAuthTicketState extends State<AccountAuthTicket> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
title: Text('accountAuthTickets').tr(),
|
title: Text('accountAuthTickets').tr(),
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('screenAccountBadges').tr(),
|
title: Text('screenAccountBadges').tr(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class _AccountContactMethodState extends State<AccountContactMethod> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
title: Text('accountContactMethods').tr(),
|
title: Text('accountContactMethods').tr(),
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
|
|||||||
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
|
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
|
||||||
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
|
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
|
||||||
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
|
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
|
||||||
3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active),
|
3: (
|
||||||
|
'authFactorInAppNotify',
|
||||||
|
'authFactorInAppNotifyDescription',
|
||||||
|
Symbols.notifications_active
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
class FactorSettingsScreen extends StatefulWidget {
|
class FactorSettingsScreen extends StatefulWidget {
|
||||||
@@ -36,7 +40,10 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/users/me/factors');
|
final resp = await sn.client.get('/cgi/id/users/me/factors');
|
||||||
_factors = List<SnAuthFactor>.from(
|
_factors = List<SnAuthFactor>.from(
|
||||||
resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [],
|
resp.data
|
||||||
|
?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -55,6 +62,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: PageBackButton(),
|
leading: PageBackButton(),
|
||||||
title: Text('screenFactorSettings').tr(),
|
title: Text('screenFactorSettings').tr(),
|
||||||
@@ -96,7 +104,8 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(kFactorTypes[ele.type]!.$1).tr(),
|
title: Text(kFactorTypes[ele.type]!.$1).tr(),
|
||||||
subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
|
subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
|
||||||
contentPadding: const EdgeInsets.only(left: 24, right: 12),
|
contentPadding:
|
||||||
|
const EdgeInsets.only(left: 24, right: 12),
|
||||||
leading: Icon(kFactorTypes[ele.type]!.$3),
|
leading: Icon(kFactorTypes[ele.type]!.$3),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(Symbols.close),
|
icon: const Icon(Symbols.close),
|
||||||
@@ -105,14 +114,17 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
|||||||
context
|
context
|
||||||
.showConfirmDialog(
|
.showConfirmDialog(
|
||||||
'authFactorDelete'.tr(),
|
'authFactorDelete'.tr(),
|
||||||
'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]),
|
'authFactorDeleteDescription'.tr(
|
||||||
|
args: [kFactorTypes[ele.type]!.$1.tr()]),
|
||||||
)
|
)
|
||||||
.then((val) async {
|
.then((val) async {
|
||||||
if (!val) return;
|
if (!val) return;
|
||||||
try {
|
try {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn =
|
||||||
await sn.client.delete('/cgi/id/users/me/factors/${ele.id}');
|
context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.delete(
|
||||||
|
'/cgi/id/users/me/factors/${ele.id}');
|
||||||
_fetchFactors();
|
_fetchFactors();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@@ -191,7 +203,9 @@ class _FactorNewDialogState extends State<_FactorNewDialog> {
|
|||||||
value: _factorType,
|
value: _factorType,
|
||||||
items: kFactorTypes.entries.map(
|
items: kFactorTypes.entries.map(
|
||||||
(ele) {
|
(ele) {
|
||||||
final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key);
|
final contains = widget.currentlyHave
|
||||||
|
.map((ele) => ele.type)
|
||||||
|
.contains(ele.key);
|
||||||
return DropdownMenuItem<int>(
|
return DropdownMenuItem<int>(
|
||||||
enabled: !contains,
|
enabled: !contains,
|
||||||
value: ele.key,
|
value: ele.key,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class _KeyPairScreenState extends State<KeyPairScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('screenKeyPairs').tr(),
|
title: Text('screenKeyPairs').tr(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class _AccountNotifyPrefsScreenState extends State<AccountNotifyPrefsScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
title: Text('accountSettingsNotify').tr(),
|
title: Text('accountSettingsNotify').tr(),
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class _AccountSecurityPrefsScreenState
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
title: Text('accountSettingsSecurity').tr(),
|
title: Text('accountSettingsSecurity').tr(),
|
||||||
|
|||||||
@@ -66,37 +66,40 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
_locationController.text = prof.profile!.location;
|
_locationController.text = prof.profile!.location;
|
||||||
_avatar = prof.avatar;
|
_avatar = prof.avatar;
|
||||||
_banner = prof.banner;
|
_banner = prof.banner;
|
||||||
_links = prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList();
|
_links =
|
||||||
|
prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList();
|
||||||
_birthday = prof.profile!.birthday?.toLocal();
|
_birthday = prof.profile!.birthday?.toLocal();
|
||||||
if (_birthday != null) {
|
if (_birthday != null) {
|
||||||
_birthdayController.text = DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal());
|
_birthdayController.text =
|
||||||
|
DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectBirthday() async {
|
void _selectBirthday() async {
|
||||||
await showCupertinoModalPopup<DateTime?>(
|
await showCupertinoModalPopup<DateTime?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder: (BuildContext context) => Container(
|
||||||
(BuildContext context) => Container(
|
height: 216,
|
||||||
height: 216,
|
padding: const EdgeInsets.only(top: 6.0),
|
||||||
padding: const EdgeInsets.only(top: 6.0),
|
margin:
|
||||||
margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: CupertinoDatePicker(
|
child: CupertinoDatePicker(
|
||||||
initialDateTime: _birthday?.toLocal(),
|
initialDateTime: _birthday?.toLocal(),
|
||||||
mode: CupertinoDatePickerMode.date,
|
mode: CupertinoDatePickerMode.date,
|
||||||
use24hFormat: true,
|
use24hFormat: true,
|
||||||
onDateTimeChanged: (DateTime newDate) {
|
onDateTimeChanged: (DateTime newDate) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_birthday = newDate;
|
_birthday = newDate;
|
||||||
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
|
_birthdayController.text =
|
||||||
});
|
DateFormat(_kDateFormat).format(_birthday!);
|
||||||
},
|
});
|
||||||
),
|
},
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,29 +112,32 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
|
|
||||||
Uint8List? rawBytes;
|
Uint8List? rawBytes;
|
||||||
if (!skipCrop) {
|
if (!skipCrop) {
|
||||||
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
final ImageProvider imageProvider =
|
||||||
final aspectRatios =
|
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||||
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
|
final aspectRatios = place == 'banner'
|
||||||
final result =
|
? [CropAspectRatio(width: 16, height: 7)]
|
||||||
(!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
: [CropAspectRatio(width: 1, height: 1)];
|
||||||
? await showCupertinoImageCropper(
|
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||||
// ignore: use_build_context_synchronously
|
? await showCupertinoImageCropper(
|
||||||
context,
|
// ignore: use_build_context_synchronously
|
||||||
allowedAspectRatios: aspectRatios,
|
context,
|
||||||
imageProvider: imageProvider,
|
allowedAspectRatios: aspectRatios,
|
||||||
)
|
imageProvider: imageProvider,
|
||||||
: await showMaterialImageCropper(
|
)
|
||||||
// ignore: use_build_context_synchronously
|
: await showMaterialImageCropper(
|
||||||
context,
|
// ignore: use_build_context_synchronously
|
||||||
allowedAspectRatios: aspectRatios,
|
context,
|
||||||
imageProvider: imageProvider,
|
allowedAspectRatios: aspectRatios,
|
||||||
);
|
imageProvider: imageProvider,
|
||||||
|
);
|
||||||
|
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||||
|
.buffer
|
||||||
|
.asUint8List();
|
||||||
} else {
|
} else {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
@@ -152,7 +158,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid});
|
await sn.client
|
||||||
|
.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid});
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
@@ -188,7 +195,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
'location': _locationController.value.text,
|
'location': _locationController.value.text,
|
||||||
'birthday': _birthday?.toUtc().toIso8601String(),
|
'birthday': _birthday?.toUtc().toIso8601String(),
|
||||||
'links': {
|
'links': {
|
||||||
for (final link in _links!.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) link.$1: link.$2,
|
for (final link in _links!
|
||||||
|
.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty))
|
||||||
|
link.$1: link.$2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -235,7 +244,10 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(leading: const PageBackButton(), title: Text('screenAccountProfileEdit').tr()),
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('screenAccountProfileEdit').tr()),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -251,13 +263,16 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 7,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: Theme.of(context)
|
||||||
child:
|
.colorScheme
|
||||||
_banner != null
|
.surfaceContainerHigh,
|
||||||
? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover)
|
child: _banner != null
|
||||||
: const SizedBox.shrink(),
|
? AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(_banner!),
|
||||||
|
fit: BoxFit.cover)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -294,12 +309,16 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
labelText: 'fieldUsername'.tr(),
|
labelText: 'fieldUsername'.tr(),
|
||||||
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _nicknameController,
|
controller: _nicknameController,
|
||||||
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldNickname'.tr()),
|
decoration: InputDecoration(
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldNickname'.tr()),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -311,7 +330,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldFirstName'.tr(),
|
labelText: 'fieldFirstName'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@@ -323,7 +343,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldLastName'.tr(),
|
labelText: 'fieldLastName'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -338,7 +359,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldGender'.tr(),
|
labelText: 'fieldGender'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
@@ -350,7 +372,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldPronouns'.tr(),
|
labelText: 'fieldPronouns'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -360,8 +383,11 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
minLines: 3,
|
minLines: 3,
|
||||||
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldDescription'.tr()),
|
decoration: InputDecoration(
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldDescription'.tr()),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@@ -373,18 +399,21 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldTimeZone'.tr(),
|
labelText: 'fieldTimeZone'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
StyledWidget(
|
StyledWidget(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.calendar_month),
|
icon: const Icon(Symbols.calendar_month),
|
||||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity:
|
||||||
|
VisualDensity(horizontal: -4, vertical: -4),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
_timezoneController.text = await FlutterTimezone.getLocalTimezone();
|
_timezoneController.text =
|
||||||
|
await FlutterTimezone.getLocalTimezone();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).padding(top: 6),
|
).padding(top: 6),
|
||||||
@@ -392,7 +421,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
StyledWidget(
|
StyledWidget(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.clear),
|
icon: const Icon(Symbols.clear),
|
||||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity:
|
||||||
|
VisualDensity(horizontal: -4, vertical: -4),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -404,13 +434,18 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _locationController,
|
controller: _locationController,
|
||||||
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldLocation'.tr()),
|
decoration: InputDecoration(
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldLocation'.tr()),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _birthdayController,
|
controller: _birthdayController,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldBirthday'.tr()),
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldBirthday'.tr()),
|
||||||
onTap: () => _selectBirthday(),
|
onTap: () => _selectBirthday(),
|
||||||
),
|
),
|
||||||
if (_links != null)
|
if (_links != null)
|
||||||
@@ -418,7 +453,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
margin: const EdgeInsets.only(top: 16, bottom: 4),
|
margin: const EdgeInsets.only(top: 16, bottom: 4),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16, vertical: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -427,13 +463,17 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'fieldLinks'.tr(),
|
'fieldLinks'.tr(),
|
||||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 17),
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium!
|
||||||
|
.copyWith(fontSize: 17),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity:
|
||||||
|
VisualDensity(horizontal: -4, vertical: -4),
|
||||||
icon: const Icon(Symbols.add),
|
icon: const Icon(Symbols.add),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() => _links!.add(('', '')));
|
setState(() => _links!.add(('', '')));
|
||||||
@@ -457,7 +497,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_links![idx] = (value, _links![idx].$2);
|
_links![idx] = (value, _links![idx].$2);
|
||||||
},
|
},
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) => FocusManager
|
||||||
|
.instance.primaryFocus
|
||||||
|
?.unfocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@@ -473,7 +515,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_links![idx] = (_links![idx].$1, value);
|
_links![idx] = (_links![idx].$1, value);
|
||||||
},
|
},
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) => FocusManager
|
||||||
|
.instance.primaryFocus
|
||||||
|
?.unfocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@@ -14,6 +15,7 @@ import 'package:surface/providers/experience.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/screens/abuse_report.dart';
|
import 'package:surface/screens/abuse_report.dart';
|
||||||
|
import 'package:surface/screens/account/punishments.dart';
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
import 'package:surface/types/check_in.dart';
|
import 'package:surface/types/check_in.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
@@ -60,6 +62,21 @@ final Map<String, (String, IconData, Color)> kBadgesMeta = {
|
|||||||
Symbols.thumb_up,
|
Symbols.thumb_up,
|
||||||
Colors.lightGreen,
|
Colors.lightGreen,
|
||||||
),
|
),
|
||||||
|
'programs.developers': (
|
||||||
|
'badgeProgramDeveloper',
|
||||||
|
Symbols.code,
|
||||||
|
Colors.blue,
|
||||||
|
),
|
||||||
|
'programs.stellar': (
|
||||||
|
'badgeProgramStellar',
|
||||||
|
Symbols.family_star,
|
||||||
|
Colors.orange,
|
||||||
|
),
|
||||||
|
'programs.moderator': (
|
||||||
|
'badgeProgramModerator',
|
||||||
|
Symbols.sword_rose,
|
||||||
|
Colors.blue,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserScreen extends StatefulWidget {
|
class UserScreen extends StatefulWidget {
|
||||||
@@ -227,7 +244,7 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
|
|
||||||
late final _appBarWidth = MediaQuery.of(context).size.width;
|
late final _appBarWidth = MediaQuery.of(context).size.width;
|
||||||
late final _appBarHeight =
|
late final _appBarHeight =
|
||||||
(_appBarWidth * kBannerAspectRatio).roundToDouble();
|
math.min((_appBarWidth * kBannerAspectRatio), 360).roundToDouble();
|
||||||
|
|
||||||
void _updateAppBarBlur() {
|
void _updateAppBarBlur() {
|
||||||
if (_scrollController.offset > _appBarHeight) return;
|
if (_scrollController.offset > _appBarHeight) return;
|
||||||
@@ -441,7 +458,7 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
],
|
],
|
||||||
).padding(right: 8),
|
).padding(right: 8),
|
||||||
if (_account!.profile!.description.isNotEmpty)
|
if (_account!.profile!.description.isNotEmpty)
|
||||||
const Gap(12)
|
const Gap(4)
|
||||||
else
|
else
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (_account!.profile!.description.isNotEmpty)
|
if (_account!.profile!.description.isNotEmpty)
|
||||||
@@ -487,14 +504,15 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
],
|
],
|
||||||
).padding(vertical: 8, horizontal: 12),
|
).padding(vertical: 8, horizontal: 12),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
if (_account!.badges.isNotEmpty) const Gap(8),
|
||||||
Wrap(
|
if (_account!.badges.isNotEmpty)
|
||||||
children: _account!.badges
|
Wrap(
|
||||||
.map(
|
spacing: 4,
|
||||||
(ele) => AccountBadge(badge: ele),
|
runSpacing: 4,
|
||||||
)
|
children: _account!.badges
|
||||||
.toList(),
|
.map((ele) => AccountBadge(badge: ele))
|
||||||
).padding(horizontal: 8),
|
.toList(),
|
||||||
|
).padding(horizontal: 8),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -603,6 +621,17 @@ class _UserScreenState extends State<UserScreen>
|
|||||||
],
|
],
|
||||||
).padding(all: 16),
|
).padding(all: 16),
|
||||||
),
|
),
|
||||||
|
if (_account?.punishments.isNotEmpty ?? false)
|
||||||
|
SliverToBoxAdapter(child: const Divider()),
|
||||||
|
if (_account?.punishments.isNotEmpty ?? false)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
for (final ele in _account!.punishments)
|
||||||
|
PunishmentInfoCard(ele: ele),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
if (_account?.profile?.links.isNotEmpty ?? false)
|
if (_account?.profile?.links.isNotEmpty ?? false)
|
||||||
SliverToBoxAdapter(child: const Divider()),
|
SliverToBoxAdapter(child: const Divider()),
|
||||||
if (_account?.profile?.links.isNotEmpty ?? false)
|
if (_account?.profile?.links.isNotEmpty ?? false)
|
||||||
|
|||||||
291
lib/screens/account/programs.dart
Normal file
291
lib/screens/account/programs.dart
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
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/experience.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/account.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/markdown_content.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
|
class AccountProgramScreen extends StatefulWidget {
|
||||||
|
const AccountProgramScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AccountProgramScreen> createState() => _AccountProgramScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccountProgramScreenState extends State<AccountProgramScreen> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
final List<SnProgram> _programs = List.empty(growable: true);
|
||||||
|
final List<SnProgramMember> _programMembers = List.empty(growable: true);
|
||||||
|
|
||||||
|
Future<void> _fetchPrograms() async {
|
||||||
|
_programs.clear();
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/id/programs');
|
||||||
|
_programs.addAll(
|
||||||
|
resp.data.map((ele) => SnProgram.fromJson(ele)).cast<SnProgram>(),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchProgramMembers() async {
|
||||||
|
_programMembers.clear();
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/id/programs/members');
|
||||||
|
_programMembers.addAll(
|
||||||
|
resp.data
|
||||||
|
.map((ele) => SnProgramMember.fromJson(ele))
|
||||||
|
.cast<SnProgramMember>(),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchPrograms();
|
||||||
|
_fetchProgramMembers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('accountProgram').tr(),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: _programs.length,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final ele = _programs[idx];
|
||||||
|
return Card(
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
isScrollControlled: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _ProgramJoinPopup(
|
||||||
|
program: ele,
|
||||||
|
isJoined:
|
||||||
|
_programMembers.any((e) => e.programId == ele.id),
|
||||||
|
),
|
||||||
|
).then((value) {
|
||||||
|
if (value == true) {
|
||||||
|
_fetchProgramMembers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (ele.appearance['banner'] != null)
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 16 / 5,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceVariant,
|
||||||
|
child: Image.network(
|
||||||
|
ele.appearance['banner'],
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
ele.name,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium,
|
||||||
|
).bold(),
|
||||||
|
Text(
|
||||||
|
ele.description,
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (_programMembers
|
||||||
|
.any((e) => e.programId == ele.id))
|
||||||
|
Text('accountProgramAlreadyJoined'.tr())
|
||||||
|
.opacity(0.75),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(horizontal: 8);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProgramJoinPopup extends StatefulWidget {
|
||||||
|
final SnProgram program;
|
||||||
|
final bool isJoined;
|
||||||
|
const _ProgramJoinPopup({required this.program, required this.isJoined});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ProgramJoinPopup> createState() => _ProgramJoinPopupState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProgramJoinPopupState extends State<_ProgramJoinPopup> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Future<void> _joinProgram() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.post('/cgi/id/programs/${widget.program.id}');
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
context.showSnackbar('accountProgramJoined'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _leaveProgram() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.delete('/cgi/id/programs/${widget.program.id}');
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
context.showSnackbar('accountProgramLeft'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.75,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.add, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text(
|
||||||
|
'accountProgramJoin',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
).tr(),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (widget.program.appearance['banner'] != null)
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 16 / 5,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
child: Image.network(
|
||||||
|
widget.program.appearance['banner'],
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(bottom: 12),
|
||||||
|
Text(
|
||||||
|
widget.program.name,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
).bold(),
|
||||||
|
MarkdownTextContent(content: widget.program.description),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'accountProgramJoinRequirements',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
).tr().bold(),
|
||||||
|
Text('≥EXP ${widget.program.expRequirement}'),
|
||||||
|
Text('≥Lv${getLevelFromExp(widget.program.expRequirement)}'),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'accountProgramJoinPricing',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
).tr().bold(),
|
||||||
|
Text('walletCurrency${widget.program.price['currency'].toString().capitalize().replaceFirst('Normal', '')}')
|
||||||
|
.plural(widget.program.price['amount'].toDouble()),
|
||||||
|
Text('accountProgramJoinPricingHint').tr().opacity(0.75),
|
||||||
|
const Gap(8),
|
||||||
|
if (widget.isJoined)
|
||||||
|
Text('accountProgramLeaveHint')
|
||||||
|
.tr()
|
||||||
|
.opacity(0.75)
|
||||||
|
.padding(bottom: 8),
|
||||||
|
if (!widget.isJoined)
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isBusy ? null : _joinProgram,
|
||||||
|
child: Text('join').tr(),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isBusy ? null : _leaveProgram,
|
||||||
|
child: Text('leave').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,10 +27,12 @@ class AccountPublisherEditScreen extends StatefulWidget {
|
|||||||
const AccountPublisherEditScreen({super.key, required this.name});
|
const AccountPublisherEditScreen({super.key, required this.name});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AccountPublisherEditScreen> createState() => _AccountPublisherEditScreenState();
|
State<AccountPublisherEditScreen> createState() =>
|
||||||
|
_AccountPublisherEditScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> {
|
class _AccountPublisherEditScreenState
|
||||||
|
extends State<AccountPublisherEditScreen> {
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
|
|
||||||
SnPublisher? _publisher;
|
SnPublisher? _publisher;
|
||||||
@@ -115,29 +117,32 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
|||||||
|
|
||||||
Uint8List? rawBytes;
|
Uint8List? rawBytes;
|
||||||
if (!skipCrop) {
|
if (!skipCrop) {
|
||||||
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
final ImageProvider imageProvider =
|
||||||
final aspectRatios =
|
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||||
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
|
final aspectRatios = place == 'banner'
|
||||||
final result =
|
? [CropAspectRatio(width: 16, height: 7)]
|
||||||
(!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
: [CropAspectRatio(width: 1, height: 1)];
|
||||||
? await showCupertinoImageCropper(
|
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||||
// ignore: use_build_context_synchronously
|
? await showCupertinoImageCropper(
|
||||||
context,
|
// ignore: use_build_context_synchronously
|
||||||
allowedAspectRatios: aspectRatios,
|
context,
|
||||||
imageProvider: imageProvider,
|
allowedAspectRatios: aspectRatios,
|
||||||
)
|
imageProvider: imageProvider,
|
||||||
: await showMaterialImageCropper(
|
)
|
||||||
// ignore: use_build_context_synchronously
|
: await showMaterialImageCropper(
|
||||||
context,
|
// ignore: use_build_context_synchronously
|
||||||
allowedAspectRatios: aspectRatios,
|
context,
|
||||||
imageProvider: imageProvider,
|
allowedAspectRatios: aspectRatios,
|
||||||
);
|
imageProvider: imageProvider,
|
||||||
|
);
|
||||||
|
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||||
|
.buffer
|
||||||
|
.asUint8List();
|
||||||
} else {
|
} else {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
@@ -191,7 +196,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()),
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: PageBackButton(),
|
||||||
|
title: Text('screenAccountPublisherEdit').tr()),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -206,13 +214,16 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 7,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: Theme.of(context)
|
||||||
child:
|
.colorScheme
|
||||||
_banner != null
|
.surfaceContainerHigh,
|
||||||
? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover)
|
child: _banner != null
|
||||||
: const SizedBox.shrink(),
|
? AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(_banner!),
|
||||||
|
fit: BoxFit.cover)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -245,13 +256,15 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
|||||||
labelText: 'fieldUsername'.tr(),
|
labelText: 'fieldUsername'.tr(),
|
||||||
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _nickController,
|
controller: _nickController,
|
||||||
decoration: InputDecoration(labelText: 'fieldNickname'.tr()),
|
decoration: InputDecoration(labelText: 'fieldNickname'.tr()),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
TextField(
|
TextField(
|
||||||
@@ -259,7 +272,8 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
|||||||
maxLines: null,
|
maxLines: null,
|
||||||
minLines: 3,
|
minLines: 3,
|
||||||
decoration: InputDecoration(labelText: 'fieldDescription'.tr()),
|
decoration: InputDecoration(labelText: 'fieldDescription'.tr()),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
title: Text('screenAccountPublisherNew').tr(),
|
title: Text('screenAccountPublisherNew').tr(),
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final resp = await sn.client.get('/cgi/co/publishers/me');
|
final resp = await sn.client.get('/cgi/co/publishers/me');
|
||||||
final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
final List<SnPublisher> out = List<SnPublisher>.from(
|
||||||
|
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
title: Text('screenAccountPublishers').tr(),
|
title: Text('screenAccountPublishers').tr(),
|
||||||
@@ -93,7 +95,9 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Symbols.add_circle),
|
leading: const Icon(Symbols.add_circle),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
|
GoRouter.of(context)
|
||||||
|
.pushNamed('accountPublisherNew')
|
||||||
|
.then((value) {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_publishers.clear();
|
_publishers.clear();
|
||||||
_fetchPublishers();
|
_fetchPublishers();
|
||||||
@@ -119,7 +123,8 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(publisher.nick),
|
title: Text(publisher.nick),
|
||||||
subtitle: Text('@${publisher.name}'),
|
subtitle: Text('@${publisher.name}'),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
leading: AccountImage(content: publisher.avatar),
|
leading: AccountImage(content: publisher.avatar),
|
||||||
trailing: PopupMenuButton(
|
trailing: PopupMenuButton(
|
||||||
itemBuilder: (BuildContext context) => [
|
itemBuilder: (BuildContext context) => [
|
||||||
|
|||||||
199
lib/screens/account/punishments.dart
Normal file
199
lib/screens/account/punishments.dart
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
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/types/account.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';
|
||||||
|
|
||||||
|
const kPunishmentIcons = [
|
||||||
|
Symbols.warning,
|
||||||
|
Symbols.emergency_home,
|
||||||
|
Symbols.dangerous,
|
||||||
|
];
|
||||||
|
|
||||||
|
class PunishmentsScreen extends StatefulWidget {
|
||||||
|
const PunishmentsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PunishmentsScreen> createState() => _PunishmentsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PunishmentsScreenState extends State<PunishmentsScreen> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
List<SnPunishment>? _punishments;
|
||||||
|
|
||||||
|
Future<void> _fetchPunishments() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/id/punishments');
|
||||||
|
if (!mounted) return;
|
||||||
|
_punishments = List.from(
|
||||||
|
resp.data.map((ele) => SnPunishment.fromJson(ele)),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchPunishments();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('accountPunishments').tr(),
|
||||||
|
leading: PageBackButton(),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
Card(
|
||||||
|
margin: EdgeInsets.only(bottom: 8, left: 8, right: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.visibility, size: 20),
|
||||||
|
const Gap(6),
|
||||||
|
Expanded(
|
||||||
|
child: Text('punishmentOverall').tr().fontSize(16).bold(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (_punishments == null) return Text('loading').tr();
|
||||||
|
if (_punishments!.any((ele) => ele.type == 2)) {
|
||||||
|
return Text('punishmentStatusBanned').tr();
|
||||||
|
}
|
||||||
|
if (_punishments!.any(
|
||||||
|
(ele) => ele.type == 1 && ele.permNodes.isEmpty,
|
||||||
|
)) {
|
||||||
|
return Text('punishmentStatusLimitedFully').tr();
|
||||||
|
} else if (_punishments!.any((ele) => ele.type == 1)) {
|
||||||
|
return Text('punishmentStatusLimited').tr();
|
||||||
|
}
|
||||||
|
if (_punishments!.any((ele) => ele.type == 0)) {
|
||||||
|
return Text('punishmentStatusWarned').tr();
|
||||||
|
}
|
||||||
|
return Text('punishmentStatusNormal').tr();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24, vertical: 16),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: _fetchPunishments,
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: _punishments?.length ?? 0,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final ele = _punishments![index];
|
||||||
|
return PunishmentInfoCard(ele: ele);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) => const Gap(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PunishmentInfoCard extends StatelessWidget {
|
||||||
|
const PunishmentInfoCard({
|
||||||
|
super.key,
|
||||||
|
required this.ele,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SnPunishment ele;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(kPunishmentIcons[ele.type], size: 20),
|
||||||
|
const Gap(6),
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
Text('punishmentType${ele.type}').tr().fontSize(16).bold(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(ele.reason),
|
||||||
|
const Gap(4),
|
||||||
|
Text(
|
||||||
|
'punishmentCreatedAt'.tr(args: [
|
||||||
|
DateFormat().format(
|
||||||
|
ele.createdAt.toLocal(),
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
).opacity(0.8),
|
||||||
|
Text(
|
||||||
|
ele.expiredAt == null
|
||||||
|
? 'punishmentExpiredNever'.tr()
|
||||||
|
: 'punishmentExpiredAt'.tr(args: [
|
||||||
|
DateFormat().format(
|
||||||
|
ele.expiredAt!.toLocal(),
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
).opacity(0.8),
|
||||||
|
const Gap(8),
|
||||||
|
if (ele.moderator != null)
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('punishmentModerator').tr().opacity(0.75),
|
||||||
|
InkWell(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AccountImage(
|
||||||
|
content: ele.moderator!.avatar,
|
||||||
|
radius: 8,
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
Text(ele.moderator?.nick ?? 'unknown'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'accountProfilePage',
|
||||||
|
pathParameters: {
|
||||||
|
'name': ele.moderator!.name,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text('punishmentMadeBySystem').tr().opacity(0.75),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24, vertical: 16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ class AccountSettingsScreen extends StatelessWidget {
|
|||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: PageBackButton(),
|
leading: PageBackButton(),
|
||||||
title: Text('screenAccountSettings').tr(),
|
title: Text('screenAccountSettings').tr(),
|
||||||
@@ -117,6 +118,16 @@ class AccountSettingsScreen extends StatelessWidget {
|
|||||||
GoRouter.of(context).pushNamed('accountSettingsSecurity');
|
GoRouter.of(context).pushNamed('accountSettingsSecurity');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
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(
|
ListTile(
|
||||||
title: Text('accountProfileEdit').tr(),
|
title: Text('accountProfileEdit').tr(),
|
||||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import 'package:dismissible_page/dismissible_page.dart';
|
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:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:path/path.dart' show withoutExtension;
|
||||||
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';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
|
||||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
|
||||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||||
|
import 'package:surface/widgets/attachment/attachment_zoom.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:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class AlbumScreen extends StatefulWidget {
|
class AlbumScreen extends StatefulWidget {
|
||||||
const AlbumScreen({super.key});
|
const AlbumScreen({super.key});
|
||||||
@@ -50,23 +50,23 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
|||||||
Future<void> _fetchAttachments() async {
|
Future<void> _fetchAttachments() async {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
|
||||||
const uuid = Uuid();
|
const uuid = Uuid();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
|
||||||
final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: {
|
final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: {
|
||||||
'take': 10,
|
'take': 10,
|
||||||
'offset': _attachments.length,
|
'offset': _attachments.length,
|
||||||
|
'author': ua.user?.name,
|
||||||
});
|
});
|
||||||
final attachments = List<SnAttachment>.from(
|
final attachments = List<SnAttachment>.from(
|
||||||
resp.data['data']?.map((e) => SnAttachment.fromJson(e)) ?? [],
|
resp.data['data']?.map((e) => SnAttachment.fromJson(e)) ?? [],
|
||||||
).where((e) => e.mimetype.startsWith('image')).toList();
|
);
|
||||||
_attachments.addAll(attachments);
|
_attachments.addAll(attachments);
|
||||||
_heroTags.addAll(_attachments.map((_) => uuid.v4()));
|
_heroTags.addAll(_attachments.map((_) => uuid.v4()));
|
||||||
|
|
||||||
await ud.listAccount(attachments.map((e) => e.accountId).toSet());
|
|
||||||
|
|
||||||
_totalCount = resp.data['count'] as int?;
|
_totalCount = resp.data['count'] as int?;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -102,92 +102,127 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
body: CustomScrollView(
|
appBar: AppBar(
|
||||||
controller: _scrollController,
|
leading: PageBackButton(),
|
||||||
slivers: [
|
title: Text('screenAlbum').tr(),
|
||||||
SliverAppBar(
|
),
|
||||||
leading: AutoAppBarLeading(),
|
body: Column(
|
||||||
title: Text('screenAlbum').tr(),
|
children: [
|
||||||
),
|
Card(
|
||||||
SliverToBoxAdapter(
|
margin: EdgeInsets.zero,
|
||||||
child: Card(
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
SizedBox(
|
||||||
SizedBox(
|
width: 80,
|
||||||
width: 80,
|
height: 80,
|
||||||
height: 80,
|
child: CircularProgressIndicator(
|
||||||
child: CircularProgressIndicator(
|
value: _billing?.includedRatio ?? 0,
|
||||||
value: _billing?.includedRatio ?? 0,
|
strokeWidth: 8,
|
||||||
strokeWidth: 8,
|
backgroundColor:
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
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),
|
||||||
|
).padding(horizontal: 8, top: 8),
|
||||||
|
Expanded(
|
||||||
|
child: InfiniteList(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
|
itemCount: _attachments.length,
|
||||||
|
isLoading: _isBusy,
|
||||||
|
hasReachedMax:
|
||||||
|
_totalCount != null && _attachments.length >= _totalCount!,
|
||||||
|
onFetchData: _fetchAttachments,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final ele = _attachments[index];
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: (ele.data['ratio'] ?? 1).toDouble(),
|
||||||
|
child: AttachmentItem(
|
||||||
|
data: ele,
|
||||||
|
heroTag: _heroTags[index],
|
||||||
|
onZoom: () {
|
||||||
|
context.pushTransparentRoute(
|
||||||
|
AttachmentZoomView(
|
||||||
|
data: [ele],
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.7),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
).padding(all: 12),
|
Row(
|
||||||
const Gap(24),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('attachmentBillingUploaded').tr().bold(),
|
Expanded(
|
||||||
Text(
|
child: Column(
|
||||||
(_billing?.currentBytes ?? 0).formatBytes(decimals: 4),
|
mainAxisSize: MainAxisSize.min,
|
||||||
style: GoogleFonts.robotoMono(),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(ele.name),
|
||||||
|
if (ele.alt != withoutExtension(ele.name))
|
||||||
|
Text(ele.alt),
|
||||||
|
Text(DateFormat().format(ele.createdAt)),
|
||||||
|
const Gap(4),
|
||||||
|
Text(ele.size.formatBytes()).fontSize(12),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 16, vertical: 12),
|
||||||
),
|
),
|
||||||
Text('attachmentBillingDiscount').tr().bold(),
|
Padding(
|
||||||
Text(
|
padding: EdgeInsets.only(left: 12, right: 12, top: 4),
|
||||||
'${(_billing?.discountFileSize ?? 0).formatBytes(decimals: 2)} · ${((_billing?.includedRatio ?? 0) * 100).toStringAsFixed(2)}%',
|
child: IconButton(
|
||||||
style: GoogleFonts.robotoMono(),
|
padding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Symbols.info),
|
||||||
|
onPressed: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AttachmentZoomDetailPopup(
|
||||||
|
data: ele,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
Tooltip(
|
);
|
||||||
message: 'attachmentBillingHint'.tr(),
|
},
|
||||||
child: IconButton(
|
separatorBuilder: (_, __) => const Gap(8),
|
||||||
icon: const Icon(Symbols.info),
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 24, vertical: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverMasonryGrid.extent(
|
|
||||||
childCount: _attachments.length,
|
|
||||||
maxCrossAxisExtent: 320,
|
|
||||||
mainAxisSpacing: 4,
|
|
||||||
crossAxisSpacing: 4,
|
|
||||||
itemBuilder: (context, idx) {
|
|
||||||
final attachment = _attachments[idx];
|
|
||||||
return GestureDetector(
|
|
||||||
child: ClipRRect(
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: attachment.metadata['ratio']?.toDouble() ?? 1,
|
|
||||||
child: AttachmentItem(
|
|
||||||
data: attachment,
|
|
||||||
heroTag: _heroTags[idx],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
context.pushTransparentRoute(
|
|
||||||
AttachmentZoomView(
|
|
||||||
data: [attachment],
|
|
||||||
heroTags: [_heroTags[idx]],
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.black.withOpacity(0.7),
|
|
||||||
rootNavigator: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (_isBusy)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
child: const CircularProgressIndicator(),
|
|
||||||
).center(),
|
|
||||||
),
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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';
|
||||||
import 'package:surface/screens/captcha.dart';
|
import 'package:surface/screens/captcha/captcha.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:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@@ -43,7 +43,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
|
|
||||||
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => TurnstileScreen(),
|
builder: (context) => CaptchaScreen(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (captchaTk == null) return;
|
if (captchaTk == null) return;
|
||||||
|
|||||||
1
lib/screens/captcha/captcha.dart
Normal file
1
lib/screens/captcha/captcha.dart
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export 'captcha_native.dart' if (dart.library.html) 'captcha_web.dart';
|
||||||
@@ -5,19 +5,18 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
class TurnstileScreen extends StatefulWidget {
|
class CaptchaScreen extends StatefulWidget {
|
||||||
const TurnstileScreen({
|
const CaptchaScreen({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TurnstileScreen> createState() => _TurnstileScreenState();
|
State<CaptchaScreen> createState() => _CaptchaScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TurnstileScreenState extends State<TurnstileScreen> {
|
class _CaptchaScreenState extends State<CaptchaScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cfg = context.read<ConfigProvider>();
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(title: Text("reCaptcha").tr()),
|
appBar: AppBar(title: Text("reCaptcha").tr()),
|
||||||
body: InAppWebView(
|
body: InAppWebView(
|
||||||
55
lib/screens/captcha/captcha_web.dart
Normal file
55
lib/screens/captcha/captcha_web.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// ignore: avoid_web_libraries_in_flutter
|
||||||
|
import 'dart:html' as html;
|
||||||
|
import 'dart:ui_web' as ui;
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
|
class CaptchaScreen extends StatefulWidget {
|
||||||
|
const CaptchaScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CaptchaScreen> createState() => _CaptchaScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CaptchaScreenState extends State<CaptchaScreen> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_setupWebListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupWebListener() {
|
||||||
|
html.window.onMessage.listen((event) {
|
||||||
|
if (event.data != null && event.data is String) {
|
||||||
|
final message = event.data as String;
|
||||||
|
if (message.startsWith("captcha_tk=")) {
|
||||||
|
String token = message.replaceFirst("captcha_tk=", "");
|
||||||
|
Navigator.pop(context, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final iframe = html.IFrameElement()
|
||||||
|
..src = '${context.read<ConfigProvider>().serverUrl}/captcha'
|
||||||
|
..style.border = 'none'
|
||||||
|
..width = '100%'
|
||||||
|
..height = '100%';
|
||||||
|
|
||||||
|
html.document.body!.append(iframe);
|
||||||
|
ui.platformViewRegistry.registerViewFactory(
|
||||||
|
'captcha-iframe',
|
||||||
|
(int viewId) => iframe,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(title: Text("reCaptcha").tr()),
|
||||||
|
body: HtmlElementView(viewType: 'captcha-iframe'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:animations/animations.dart';
|
||||||
|
import 'package:collection/collection.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';
|
||||||
@@ -6,21 +8,22 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.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:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.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/providers/userinfo.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/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/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:surface/widgets/universal_image.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class ChatScreen extends StatefulWidget {
|
class ChatScreen extends StatefulWidget {
|
||||||
@@ -38,6 +41,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
List<SnChannel>? _channels;
|
List<SnChannel>? _channels;
|
||||||
Map<int, SnChatMessage>? _lastMessages;
|
Map<int, SnChatMessage>? _lastMessages;
|
||||||
Map<int, int>? _unreadCounts;
|
Map<int, int>? _unreadCounts;
|
||||||
|
Map<int, int>? _unreadCountsGrouped;
|
||||||
|
|
||||||
Future<void> _fetchWhatsNew() async {
|
Future<void> _fetchWhatsNew() async {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
@@ -45,19 +49,48 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
if (resp.data == null) return;
|
if (resp.data == null) return;
|
||||||
final List<dynamic> out = resp.data;
|
final List<dynamic> out = resp.data;
|
||||||
setState(() {
|
setState(() {
|
||||||
_unreadCounts = {for (var v in out) v['channel_id']: v['count']};
|
_unreadCounts ??= {};
|
||||||
|
_unreadCountsGrouped ??= {};
|
||||||
|
for (var v in out) {
|
||||||
|
_unreadCounts![v['channel_id']] = v['count'];
|
||||||
|
final channel =
|
||||||
|
_channels?.firstWhereOrNull((ele) => ele.id == v['channel_id']);
|
||||||
|
if (channel != null) {
|
||||||
|
if (channel.realmId != null) {
|
||||||
|
_unreadCountsGrouped![channel.realmId!] ??= 0;
|
||||||
|
_unreadCountsGrouped![channel.realmId!] =
|
||||||
|
(_unreadCountsGrouped![channel.realmId!]! + v['count']).toInt();
|
||||||
|
}
|
||||||
|
if (channel.type == 1) {
|
||||||
|
_unreadCountsGrouped![0] ??= 0;
|
||||||
|
_unreadCountsGrouped![0] =
|
||||||
|
(_unreadCountsGrouped![0]! + v['count']).toInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _refreshChannels({bool noRemote = false}) {
|
void _refreshChannels({bool withBoost = false, bool noRemote = false}) {
|
||||||
|
final ct = context.read<ChatChannelProvider>();
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!withBoost) {
|
||||||
|
if (!noRemote) {
|
||||||
|
ct.refreshAvailableChannels();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_channels = ct.availableChannels;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
final chan = context.read<ChatChannelProvider>();
|
final chan = context.read<ChatChannelProvider>();
|
||||||
chan.fetchChannels(noRemote: noRemote).listen((channels) async {
|
chan.fetchChannels(noRemote: true).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) {
|
||||||
@@ -99,6 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
..onDone(() {
|
..onDone(() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
|
_fetchWhatsNew();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,40 +164,60 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SnChannel? _focusChannel;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_refreshChannels();
|
_refreshChannels(withBoost: true);
|
||||||
_fetchWhatsNew();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onTapChannel(SnChannel channel) {
|
void _onTapChannel(SnChannel channel) {
|
||||||
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
|
setState(() {
|
||||||
|
_unreadCounts?[channel.id] = 0;
|
||||||
if (doExpand) {
|
if (channel.realmId != null) {
|
||||||
setState(() => _focusChannel = channel);
|
_unreadCountsGrouped?[channel.realmId!] =
|
||||||
return;
|
(_unreadCountsGrouped?[channel.realmId!] ?? 0) -
|
||||||
}
|
(_unreadCounts?[channel.id] ?? 0);
|
||||||
GoRouter.of(context).pushNamed(
|
}
|
||||||
'chatRoom',
|
if (channel.type == 1) {
|
||||||
pathParameters: {
|
_unreadCountsGrouped?[0] =
|
||||||
'scope': channel.realm?.alias ?? 'global',
|
(_unreadCountsGrouped?[0] ?? 0) - (_unreadCounts?[channel.id] ?? 0);
|
||||||
'alias': channel.alias,
|
|
||||||
},
|
|
||||||
).then((value) {
|
|
||||||
if (mounted) {
|
|
||||||
_unreadCounts?[channel.id] = 0;
|
|
||||||
setState(() => _unreadCounts?[channel.id] = 0);
|
|
||||||
_refreshChannels(noRemote: true);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (ResponsiveScaffold.getIsExpand(context)) {
|
||||||
|
GoRouter.of(context).pushReplacementNamed(
|
||||||
|
'chatRoom',
|
||||||
|
pathParameters: {
|
||||||
|
'scope': channel.realm?.alias ?? 'global',
|
||||||
|
'alias': channel.alias,
|
||||||
|
},
|
||||||
|
).then((value) {
|
||||||
|
if (mounted && value == true) {
|
||||||
|
_refreshChannels();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'chatRoom',
|
||||||
|
pathParameters: {
|
||||||
|
'scope': channel.realm?.alias ?? 'global',
|
||||||
|
'alias': channel.alias,
|
||||||
|
},
|
||||||
|
).then((value) {
|
||||||
|
if (mounted && value == true) {
|
||||||
|
_refreshChannels();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SnRealm? _focusedRealm;
|
||||||
|
bool _isDirect = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final rel = context.read<SnRealmProvider>();
|
||||||
|
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
@@ -177,10 +231,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
final chatList = AppScaffold(
|
|
||||||
noBackground: doExpand,
|
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenChat').tr(),
|
title: Text('screenChat').tr(),
|
||||||
@@ -248,64 +300,198 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
LoadingIndicator(isActive: _isBusy),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
Expanded(
|
if (_channels != null && ResponsiveScaffold.getIsExpand(context))
|
||||||
child: MediaQuery.removePadding(
|
Expanded(
|
||||||
context: context,
|
|
||||||
removeTop: true,
|
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: () => Future.wait([
|
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||||
Future.sync(() => _refreshChannels()),
|
child: Builder(builder: (context) {
|
||||||
_fetchWhatsNew(),
|
final scopeList = ListView(
|
||||||
]),
|
key: const Key('realm-list-view'),
|
||||||
child: ListView.builder(
|
padding: EdgeInsets.zero,
|
||||||
itemCount: _channels?.length ?? 0,
|
children: [
|
||||||
itemBuilder: (context, idx) {
|
ListTile(
|
||||||
final channel = _channels![idx];
|
minTileHeight: 48,
|
||||||
final lastMessage = _lastMessages?[channel.id];
|
leading:
|
||||||
|
const Icon(Symbols.inbox_text).padding(right: 4),
|
||||||
|
contentPadding: EdgeInsets.only(left: 24, right: 24),
|
||||||
|
title: Text('chatDirect').tr(),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (_unreadCountsGrouped?[0] != null &&
|
||||||
|
(_unreadCountsGrouped?[0] ?? 0) > 0)
|
||||||
|
Badge(
|
||||||
|
label: Text(
|
||||||
|
_unreadCountsGrouped![0].toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _isDirect = true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
...rel.availableRealms.map((ele) {
|
||||||
|
return ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
contentPadding: EdgeInsets.only(left: 20, right: 24),
|
||||||
|
leading: AccountImage(
|
||||||
|
content: ele.avatar,
|
||||||
|
radius: 16,
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (_unreadCountsGrouped?[ele.id] != null &&
|
||||||
|
(_unreadCountsGrouped?[ele.id] ?? 0) > 0)
|
||||||
|
Badge(
|
||||||
|
label: Text(
|
||||||
|
_unreadCountsGrouped![ele.id].toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
title: Text(ele.name),
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _focusedRealm = ele);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return _ChatChannelEntry(
|
final directChatList = ListView(
|
||||||
channel: channel,
|
key: Key('direct-chat-list-view'),
|
||||||
lastMessage: lastMessage,
|
padding: EdgeInsets.zero,
|
||||||
unreadCount: _unreadCounts?[channel.id],
|
children: [
|
||||||
onTap: () {
|
ListTile(
|
||||||
if (doExpand) {
|
minTileHeight: 48,
|
||||||
_unreadCounts?[channel.id] = 0;
|
leading: const Icon(Symbols.arrow_left_alt),
|
||||||
setState(() => _focusChannel = channel);
|
contentPadding: EdgeInsets.only(left: 24),
|
||||||
return;
|
title: Text('back').tr(),
|
||||||
}
|
onTap: () {
|
||||||
_onTapChannel(channel);
|
setState(() => _isDirect = false);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
const Divider(height: 1),
|
||||||
|
..._channels!.where((ele) => ele.type == 1).map(
|
||||||
|
(ele) {
|
||||||
|
return _ChatChannelEntry(
|
||||||
|
channel: ele,
|
||||||
|
unreadCount: _unreadCounts?[ele.id],
|
||||||
|
lastMessage: _lastMessages?[ele.id],
|
||||||
|
isCompact: true,
|
||||||
|
onTap: () => _onTapChannel(ele),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final realmScopedChatList = _focusedRealm == null
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: ListView(
|
||||||
|
key: ValueKey(_focusedRealm),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
children: [
|
||||||
|
if (_focusedRealm!.banner != null)
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 16 / 7,
|
||||||
|
child: AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(
|
||||||
|
_focusedRealm!.banner!,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
tileColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer,
|
||||||
|
leading: AccountImage(
|
||||||
|
content: _focusedRealm!.avatar,
|
||||||
|
radius: 16,
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.only(
|
||||||
|
left: 20,
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Symbols.close),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: () {
|
||||||
|
setState(() => _focusedRealm = null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(_focusedRealm!.name),
|
||||||
|
),
|
||||||
|
...(_channels!
|
||||||
|
.where(
|
||||||
|
(ele) => ele.realmId == _focusedRealm?.id)
|
||||||
|
.map(
|
||||||
|
(ele) {
|
||||||
|
return _ChatChannelEntry(
|
||||||
|
channel: ele,
|
||||||
|
unreadCount: _unreadCounts?[ele.id],
|
||||||
|
lastMessage: _lastMessages?[ele.id],
|
||||||
|
onTap: () => _onTapChannel(ele),
|
||||||
|
isCompact: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return PageTransitionSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (Widget child,
|
||||||
|
Animation<double> primaryAnimation,
|
||||||
|
Animation<double> secondaryAnimation) {
|
||||||
|
return SharedAxisTransition(
|
||||||
|
animation: primaryAnimation,
|
||||||
|
secondaryAnimation: secondaryAnimation,
|
||||||
|
fillColor: Colors.transparent,
|
||||||
|
transitionType: SharedAxisTransitionType.horizontal,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: (_focusedRealm == null && !_isDirect)
|
||||||
|
? scopeList
|
||||||
|
: _isDirect
|
||||||
|
? directChatList
|
||||||
|
: realmScopedChatList,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (_channels != null)
|
||||||
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||||
|
child: ListView(
|
||||||
|
key: const Key('chat-list-view'),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
children: [
|
||||||
|
...(_channels!.map((ele) {
|
||||||
|
return _ChatChannelEntry(
|
||||||
|
channel: ele,
|
||||||
|
unreadCount: _unreadCounts?[ele.id],
|
||||||
|
lastMessage: _lastMessages?[ele.id],
|
||||||
|
onTap: () => _onTapChannel(ele),
|
||||||
|
);
|
||||||
|
}))
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,11 +500,13 @@ class _ChatChannelEntry extends StatelessWidget {
|
|||||||
final int? unreadCount;
|
final int? unreadCount;
|
||||||
final SnChatMessage? lastMessage;
|
final SnChatMessage? lastMessage;
|
||||||
final Function? onTap;
|
final Function? onTap;
|
||||||
|
final bool isCompact;
|
||||||
const _ChatChannelEntry({
|
const _ChatChannelEntry({
|
||||||
required this.channel,
|
required this.channel,
|
||||||
this.unreadCount,
|
this.unreadCount,
|
||||||
this.lastMessage,
|
this.lastMessage,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.isCompact = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -337,6 +525,34 @@ class _ChatChannelEntry extends StatelessWidget {
|
|||||||
? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name
|
? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name
|
||||||
: channel.name;
|
: channel.name;
|
||||||
|
|
||||||
|
if (isCompact) {
|
||||||
|
return ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.only(left: otherMember != null ? 20 : 24, right: 24),
|
||||||
|
leading: otherMember != null
|
||||||
|
? AccountImage(
|
||||||
|
content: ud.getFromCache(otherMember.accountId)?.avatar,
|
||||||
|
radius: 16,
|
||||||
|
)
|
||||||
|
: const Icon(Symbols.tag),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (unreadCount != null && (unreadCount ?? 0) > 0)
|
||||||
|
Badge(
|
||||||
|
label: Text(unreadCount.toString()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
title: Text(title),
|
||||||
|
onTap: () {
|
||||||
|
onTap?.call();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -399,7 +615,7 @@ class _ChatChannelEntry extends StatelessWidget {
|
|||||||
content: otherMember != null
|
content: otherMember != null
|
||||||
? ud.getFromCache(otherMember.accountId)?.avatar
|
? ud.getFromCache(otherMember.accountId)?.avatar
|
||||||
: channel.realm?.avatar,
|
: channel.realm?.avatar,
|
||||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
fallbackWidget: const Icon(Symbols.tag, size: 20),
|
||||||
),
|
),
|
||||||
onTap: () => onTap?.call(),
|
onTap: () => onTap?.call(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,17 +32,16 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildListLayout() {
|
Widget _buildMeetLayout() {
|
||||||
final call = context.read<ChatCallProvider>();
|
final call = context.read<ChatCallProvider>();
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
color:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
||||||
child: call.focusTrack != null
|
child: call.focusTrack != null
|
||||||
? InteractiveParticipantWidget(
|
? InteractiveParticipantWidget(
|
||||||
isFixedAvatar: false,
|
|
||||||
participant: call.focusTrack!,
|
participant: call.focusTrack!,
|
||||||
onTap: () {},
|
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
@@ -61,22 +60,18 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return SizedBox(
|
||||||
padding: const EdgeInsets.only(top: 8, left: 8),
|
height: 128,
|
||||||
child: ClipRRect(
|
width: 128,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
child: InteractiveParticipantWidget(
|
||||||
child: InteractiveParticipantWidget(
|
participant: track,
|
||||||
isFixedAvatar: true,
|
avatarSize: 32,
|
||||||
width: 120,
|
onTap: () {
|
||||||
height: 120,
|
if (track.participant.sid !=
|
||||||
color: Theme.of(context).cardColor,
|
call.focusTrack?.participant.sid) {
|
||||||
participant: track,
|
call.setFocusTrack(track);
|
||||||
onTap: () {
|
}
|
||||||
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
},
|
||||||
call.setFocusTrack(track);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -87,46 +82,26 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildGridLayout() {
|
Widget _buildListLayout() {
|
||||||
final call = context.read<ChatCallProvider>();
|
final call = context.read<ChatCallProvider>();
|
||||||
|
|
||||||
return LayoutBuilder(builder: (context, constraints) {
|
return LayoutBuilder(
|
||||||
double screenWidth = constraints.maxWidth;
|
builder: (context, constraints) {
|
||||||
double screenHeight = constraints.maxHeight;
|
return ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
int columns = (math.sqrt(call.participantTracks.length)).ceil();
|
itemCount: math.max(0, call.participantTracks.length),
|
||||||
int rows = (call.participantTracks.length / columns).ceil();
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final track = call.participantTracks[index];
|
||||||
double tileWidth = screenWidth / columns;
|
return InteractiveParticipantWidget(
|
||||||
double tileHeight = screenHeight / rows;
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
isList: true,
|
||||||
return StyledWidget(GridView.builder(
|
avatarSize: 24,
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
participant: track,
|
||||||
crossAxisCount: columns,
|
);
|
||||||
childAspectRatio: tileWidth / tileHeight,
|
},
|
||||||
crossAxisSpacing: 8,
|
);
|
||||||
mainAxisSpacing: 8,
|
},
|
||||||
),
|
);
|
||||||
itemCount: math.max(0, call.participantTracks.length),
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
final track = call.participantTracks[index];
|
|
||||||
return Card(
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: InteractiveParticipantWidget(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
|
|
||||||
participant: track,
|
|
||||||
onTap: () {
|
|
||||||
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
|
||||||
call.setFocusTrack(track);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)).padding(all: 8);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -149,6 +124,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
listenable: call,
|
listenable: call,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: RichText(
|
title: RichText(
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@@ -169,117 +145,129 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: GestureDetector(
|
body: Column(
|
||||||
behavior: HitTestBehavior.translucent,
|
children: [
|
||||||
child: Column(
|
SizedBox(
|
||||||
children: [
|
width: MediaQuery.of(context).size.width,
|
||||||
|
height: 64,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Builder(builder: (context) {
|
||||||
|
final call = context.read<ChatCallProvider>();
|
||||||
|
final connectionQuality =
|
||||||
|
call.room.localParticipant?.connectionQuality ??
|
||||||
|
livekit.ConnectionQuality.unknown;
|
||||||
|
return Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
call.channel?.name ?? 'unknown'.tr(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
Text(call.lastDuration.toString())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
{
|
||||||
|
livekit.ConnectionState.disconnected:
|
||||||
|
'callStatusDisconnected'.tr(),
|
||||||
|
livekit.ConnectionState.connected:
|
||||||
|
'callStatusConnected'.tr(),
|
||||||
|
livekit.ConnectionState.connecting:
|
||||||
|
'callStatusConnecting'.tr(),
|
||||||
|
livekit.ConnectionState.reconnecting:
|
||||||
|
'callStatusReconnecting'.tr(),
|
||||||
|
}[call.room.connectionState]!,
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
if (connectionQuality !=
|
||||||
|
livekit.ConnectionQuality.unknown)
|
||||||
|
Icon(
|
||||||
|
{
|
||||||
|
livekit.ConnectionQuality.excellent:
|
||||||
|
Icons.signal_cellular_alt,
|
||||||
|
livekit.ConnectionQuality.good:
|
||||||
|
Icons.signal_cellular_alt_2_bar,
|
||||||
|
livekit.ConnectionQuality.poor:
|
||||||
|
Icons.signal_cellular_alt_1_bar,
|
||||||
|
}[connectionQuality],
|
||||||
|
color: {
|
||||||
|
livekit.ConnectionQuality.excellent:
|
||||||
|
Colors.green,
|
||||||
|
livekit.ConnectionQuality.good:
|
||||||
|
Colors.orange,
|
||||||
|
livekit.ConnectionQuality.poor:
|
||||||
|
Colors.red,
|
||||||
|
}[connectionQuality],
|
||||||
|
size: 16,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
).padding(all: 3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: _layoutMode == 0
|
||||||
|
? const Icon(Icons.view_list)
|
||||||
|
: const Icon(Icons.grid_view),
|
||||||
|
onPressed: () {
|
||||||
|
_switchLayout();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(left: 20, right: 16),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Material(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
switch (_layoutMode) {
|
||||||
|
case 1:
|
||||||
|
return _buildListLayout();
|
||||||
|
default:
|
||||||
|
return _buildMeetLayout();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (call.room.localParticipant != null)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
height: 64,
|
child: ControlsWidget(
|
||||||
child: Row(
|
call.room,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
call.room.localParticipant!,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Builder(builder: (context) {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
final connectionQuality =
|
|
||||||
call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown;
|
|
||||||
return Expanded(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
call.channel?.name ?? 'unknown'.tr(),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(6),
|
|
||||||
Text(call.lastDuration.toString())
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
{
|
|
||||||
livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(),
|
|
||||||
livekit.ConnectionState.connected: 'callStatusConnected'.tr(),
|
|
||||||
livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(),
|
|
||||||
livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(),
|
|
||||||
}[call.room.connectionState]!,
|
|
||||||
),
|
|
||||||
const Gap(6),
|
|
||||||
if (connectionQuality != livekit.ConnectionQuality.unknown)
|
|
||||||
Icon(
|
|
||||||
{
|
|
||||||
livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt,
|
|
||||||
livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
|
|
||||||
livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
|
|
||||||
}[connectionQuality],
|
|
||||||
color: {
|
|
||||||
livekit.ConnectionQuality.excellent: Colors.green,
|
|
||||||
livekit.ConnectionQuality.good: Colors.orange,
|
|
||||||
livekit.ConnectionQuality.poor: Colors.red,
|
|
||||||
}[connectionQuality],
|
|
||||||
size: 16,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const SizedBox(
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: Colors.white,
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
).padding(all: 3),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view),
|
|
||||||
onPressed: () {
|
|
||||||
_switchLayout();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(left: 20, right: 16),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Material(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
switch (_layoutMode) {
|
|
||||||
case 1:
|
|
||||||
return _buildGridLayout();
|
|
||||||
default:
|
|
||||||
return _buildListLayout();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (call.room.localParticipant != null)
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
SizedBox(
|
],
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
child: ControlsWidget(
|
|
||||||
call.room,
|
|
||||||
call.room.localParticipant!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
|
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
|
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
if (_editingChannel != null) {
|
if (_editingChannel != null) {
|
||||||
_belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
|
_belongToRealm =
|
||||||
|
_realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (mounted) context.showErrorDialog(err);
|
if (mounted) context.showErrorDialog(err);
|
||||||
@@ -97,7 +98,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
'is_community': _isCommunity,
|
'is_community': _isCommunity,
|
||||||
if (_editingChannel != null && _belongToRealm == null)
|
if (_editingChannel != null && _belongToRealm == null)
|
||||||
'new_belongs_realm': 'global'
|
'new_belongs_realm': 'global'
|
||||||
else if (_editingChannel != null && _belongToRealm?.id != _editingChannel?.realm?.id)
|
else if (_editingChannel != null &&
|
||||||
|
_belongToRealm?.id != _editingChannel?.realm?.id)
|
||||||
'new_belongs_realm': _belongToRealm!.alias,
|
'new_belongs_realm': _belongToRealm!.alias,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,8 +141,11 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(),
|
title: widget.editingChannelAlias != null
|
||||||
|
? Text('screenChatManage').tr()
|
||||||
|
: Text('screenChatNew').tr(),
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -152,7 +157,8 @@ 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'.tr(args: ['#${_editingChannel!.alias}']),
|
'channelEditingNotice'
|
||||||
|
.tr(args: ['#${_editingChannel!.alias}']),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -192,12 +198,15 @@ 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).textTheme.bodyMedium!),
|
Text(item.name).textStyle(Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!),
|
||||||
Text(
|
Text(
|
||||||
item.description,
|
item.description,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
).textStyle(Theme.of(context).textTheme.bodySmall!),
|
).textStyle(
|
||||||
|
Theme.of(context).textTheme.bodySmall!),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -213,7 +222,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 16,
|
radius: 16,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onSurface,
|
||||||
child: const Icon(Symbols.clear),
|
child: const Icon(Symbols.clear),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
@@ -222,7 +232,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('fieldChatBelongToRealmUnset').tr().textStyle(
|
Text('fieldChatBelongToRealmUnset')
|
||||||
|
.tr()
|
||||||
|
.textStyle(
|
||||||
Theme.of(context).textTheme.bodyMedium!,
|
Theme.of(context).textTheme.bodyMedium!,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -257,7 +269,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
helperText: 'fieldChatAliasHint'.tr(),
|
helperText: 'fieldChatAliasHint'.tr(),
|
||||||
helperMaxLines: 2,
|
helperMaxLines: 2,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
TextField(
|
TextField(
|
||||||
@@ -266,7 +279,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldChatName'.tr(),
|
labelText: 'fieldChatName'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
TextField(
|
TextField(
|
||||||
@@ -277,7 +291,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldChatDescription'.tr(),
|
labelText: 'fieldChatDescription'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
_channel?.type == 1
|
_channel?.type == 1
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cfg = context.watch<ConfigProvider>();
|
final cfg = context.watch<ConfigProvider>();
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
floatingActionButtonLocation: ExpandableFab.location,
|
floatingActionButtonLocation: ExpandableFab.location,
|
||||||
floatingActionButton: ExpandableFab(
|
floatingActionButton: ExpandableFab(
|
||||||
key: _fabKey,
|
key: _fabKey,
|
||||||
@@ -243,6 +244,8 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
GoRouter.of(context).pushNamed('postShuffle');
|
GoRouter.of(context).pushNamed('postShuffle');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
|
||||||
|
const Gap(48),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@@ -534,6 +537,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
switch (ele.type) {
|
switch (ele.type) {
|
||||||
case 'interactive.post':
|
case 'interactive.post':
|
||||||
return OpenablePostItem(
|
return OpenablePostItem(
|
||||||
|
useReplace: true,
|
||||||
data: SnPost.fromJson(ele.data),
|
data: SnPost.fromJson(ele.data),
|
||||||
maxWidth: 640,
|
maxWidth: 640,
|
||||||
onChanged: (data) {
|
onChanged: (data) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ 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/account/account_select.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';
|
||||||
@@ -47,8 +46,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
|
final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
|
||||||
_relations = List<SnRelationship>.from(
|
_relations = List<SnRelationship>.from(
|
||||||
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []);
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -67,8 +65,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/users/me/relations?status=0,3');
|
final resp = await sn.client.get('/cgi/id/users/me/relations?status=0,3');
|
||||||
_requests = List<SnRelationship>.from(
|
_requests = List<SnRelationship>.from(
|
||||||
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []);
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -87,8 +84,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/users/me/relations?status=2');
|
final resp = await sn.client.get('/cgi/id/users/me/relations?status=2');
|
||||||
_blocks = List<SnRelationship>.from(
|
_blocks = List<SnRelationship>.from(
|
||||||
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []);
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -105,10 +101,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
try {
|
try {
|
||||||
final rel = context.read<SnRelationshipProvider>();
|
final rel = context.read<SnRelationshipProvider>();
|
||||||
await rel.updateRelationship(
|
await rel.updateRelationship(
|
||||||
relation.relatedId,
|
relation.relatedId, dstStatus, relation.permNodes);
|
||||||
dstStatus,
|
|
||||||
relation.permNodes,
|
|
||||||
);
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_fetchRelations();
|
_fetchRelations();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -122,9 +115,8 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
Future<void> _deleteRelation(SnRelationship relation) async {
|
Future<void> _deleteRelation(SnRelationship relation) async {
|
||||||
final confirm = await context.showConfirmDialog(
|
final confirm = await context.showConfirmDialog(
|
||||||
'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
|
'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
|
||||||
'friendDeleteDescription'.tr(args: [
|
'friendDeleteDescription'
|
||||||
relation.related?.nick ?? 'unknown'.tr(),
|
.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
if (!confirm) return;
|
if (!confirm) return;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -146,9 +138,11 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
|
|
||||||
void _showRequests() {
|
void _showRequests() {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _FriendshipListWidget(relations: _requests),
|
builder: (context) => _FriendshipListWidget(relations: _requests))
|
||||||
).then((value) {
|
.then((
|
||||||
|
value,
|
||||||
|
) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
_fetchRequests();
|
_fetchRequests();
|
||||||
_fetchRelations();
|
_fetchRelations();
|
||||||
@@ -158,9 +152,10 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
|
|
||||||
void _showBlocks() {
|
void _showBlocks() {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _FriendshipListWidget(relations: _blocks),
|
builder: (context) => _FriendshipListWidget(relations: _blocks)).then((
|
||||||
).then((value) {
|
value,
|
||||||
|
) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
_fetchBlocks();
|
_fetchBlocks();
|
||||||
_fetchRelations();
|
_fetchRelations();
|
||||||
@@ -173,9 +168,8 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.post('/cgi/id/users/me/relations', data: {
|
await sn.client
|
||||||
'related': user.name,
|
.post('/cgi/id/users/me/relations', data: {'related': user.name});
|
||||||
});
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('friendRequestSent'.tr());
|
context.showSnackbar('friendRequestSent'.tr());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -201,18 +195,16 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: PageBackButton(),
|
||||||
title: Text('screenFriend').tr(),
|
title: Text('screenFriend').tr(),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(child: UnauthorizedHint()),
|
||||||
child: UnauthorizedHint(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: PageBackButton(),
|
||||||
title: Text('screenFriend').tr(),
|
title: Text('screenFriend').tr(),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
@@ -220,9 +212,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final user = await showModalBottomSheet<SnAccount?>(
|
final user = await showModalBottomSheet<SnAccount?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AccountSelect(
|
builder: (context) => AccountSelect(title: 'friendNew'.tr()),
|
||||||
title: 'friendNew'.tr(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
@@ -235,9 +225,8 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
if (_requests.isNotEmpty)
|
if (_requests.isNotEmpty)
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('friendRequests').tr(),
|
title: Text('friendRequests').tr(),
|
||||||
subtitle: Text(
|
subtitle:
|
||||||
'friendRequestsDescription',
|
Text('friendRequestsDescription').plural(_requests.length),
|
||||||
).plural(_requests.length),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Symbols.group_add),
|
leading: const Icon(Symbols.group_add),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
@@ -246,31 +235,30 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
if (_blocks.isNotEmpty)
|
if (_blocks.isNotEmpty)
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('friendBlocklist').tr(),
|
title: Text('friendBlocklist').tr(),
|
||||||
subtitle: Text(
|
subtitle:
|
||||||
'friendBlocklistDescription',
|
Text('friendBlocklistDescription').plural(_blocks.length),
|
||||||
).plural(_blocks.length),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Symbols.block),
|
leading: const Icon(Symbols.block),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: _showBlocks,
|
onTap: _showBlocks,
|
||||||
),
|
),
|
||||||
if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1),
|
if (_requests.isNotEmpty || _blocks.isNotEmpty)
|
||||||
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: MediaQuery.removePadding(
|
child: MediaQuery.removePadding(
|
||||||
context: context,
|
context: context,
|
||||||
removeTop: true,
|
removeTop: true,
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: () => Future.wait([
|
onRefresh: () =>
|
||||||
_fetchRelations(),
|
Future.wait([_fetchRelations(), _fetchRequests()]),
|
||||||
_fetchRequests(),
|
|
||||||
]),
|
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: _relations.length,
|
itemCount: _relations.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final relation = _relations[index];
|
final relation = _relations[index];
|
||||||
final other = relation.related;
|
final other = relation.related;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
contentPadding:
|
||||||
|
const EdgeInsets.only(right: 24, left: 16),
|
||||||
leading: AccountImage(content: other?.avatar),
|
leading: AccountImage(content: other?.avatar),
|
||||||
title: Text(other?.nick ?? 'unknown'),
|
title: Text(other?.nick ?? 'unknown'),
|
||||||
subtitle: Text(other?.nick ?? 'unknown'),
|
subtitle: Text(other?.nick ?? 'unknown'),
|
||||||
@@ -286,12 +274,16 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: _isUpdating ? null : () => _changeRelation(relation, 2),
|
onTap: _isUpdating
|
||||||
|
? null
|
||||||
|
: () => _changeRelation(relation, 2),
|
||||||
child: Text('friendBlock').tr(),
|
child: Text('friendBlock').tr(),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: _isUpdating ? null : () => _deleteRelation(relation),
|
onTap: _isUpdating
|
||||||
|
? null
|
||||||
|
: () => _deleteRelation(relation),
|
||||||
child: Text('friendDeleteAction').tr(),
|
child: Text('friendDeleteAction').tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -361,10 +353,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
|||||||
try {
|
try {
|
||||||
final rel = context.read<SnRelationshipProvider>();
|
final rel = context.read<SnRelationshipProvider>();
|
||||||
await rel.updateRelationship(
|
await rel.updateRelationship(
|
||||||
relation.relatedId,
|
relation.relatedId, dstStatus, relation.permNodes);
|
||||||
dstStatus,
|
|
||||||
relation.permNodes,
|
|
||||||
);
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -378,9 +367,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
|||||||
Future<void> _deleteRelation(SnRelationship relation) async {
|
Future<void> _deleteRelation(SnRelationship relation) async {
|
||||||
final confirm = await context.showConfirmDialog(
|
final confirm = await context.showConfirmDialog(
|
||||||
'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
|
'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
|
||||||
'friendDeleteDescription'.tr(args: [
|
'friendDeleteDescription'
|
||||||
relation.related?.nick ?? 'unknown'.tr(),
|
.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
if (!confirm) return;
|
if (!confirm) return;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -420,7 +408,9 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75),
|
Text(kFriendStatus[relation.status] ?? 'unknown')
|
||||||
|
.tr()
|
||||||
|
.opacity(0.75),
|
||||||
if (relation.status == 0)
|
if (relation.status == 0)
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
@@ -441,7 +431,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: _isBusy ? null : () => _changeRelation(relation, 1),
|
onTap:
|
||||||
|
_isBusy ? null : () => _changeRelation(relation, 1),
|
||||||
child: Text('friendUnblock').tr(),
|
child: Text('friendUnblock').tr(),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import 'package:surface/providers/sn_network.dart';
|
|||||||
import 'package:surface/providers/special_day.dart';
|
import 'package:surface/providers/special_day.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/widget.dart';
|
import 'package:surface/providers/widget.dart';
|
||||||
import 'package:surface/screens/captcha.dart';
|
import 'package:surface/screens/captcha/captcha.dart';
|
||||||
import 'package:surface/types/check_in.dart';
|
import 'package:surface/types/check_in.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
@@ -396,35 +396,44 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
|
|||||||
: switch (_serviceStatus) {
|
: switch (_serviceStatus) {
|
||||||
ServiceStatus.operational => Row(
|
ServiceStatus.operational => Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Symbols.check,
|
Symbols.check,
|
||||||
size: 20,
|
size: 20,
|
||||||
|
color: Colors.green[900],
|
||||||
),
|
),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Text('serviceStatusOperational').tr(),
|
Text('serviceStatusOperational')
|
||||||
|
.tr()
|
||||||
|
.textColor(Colors.green[900]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ServiceStatus.failed => Tooltip(
|
ServiceStatus.failed => Tooltip(
|
||||||
message: 'serviceStatusFailedDescription'.tr(),
|
message: 'serviceStatusFailedDescription'.tr(),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Symbols.dangerous,
|
Symbols.dangerous,
|
||||||
size: 20,
|
size: 20,
|
||||||
|
color: Colors.red[900],
|
||||||
),
|
),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Text('serviceStatusFailed').tr(),
|
Text('serviceStatusFailed')
|
||||||
|
.tr()
|
||||||
|
.textColor(Colors.red[900]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_ => Row(
|
_ => Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Symbols.error,
|
Symbols.error,
|
||||||
size: 20,
|
size: 20,
|
||||||
|
color: Colors.orange[900],
|
||||||
),
|
),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Text('serviceStatusDowngraded').tr(),
|
Text('serviceStatusDowngraded')
|
||||||
|
.tr()
|
||||||
|
.textColor(Colors.orange[900]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -511,7 +520,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
Future<void> _doCheckIn() async {
|
Future<void> _doCheckIn() async {
|
||||||
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => TurnstileScreen(),
|
builder: (context) => CaptchaScreen(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (captchaTk == null) return;
|
if (captchaTk == null) return;
|
||||||
@@ -806,7 +815,7 @@ class _HomeDashNotificationWidgetState
|
|||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Symbols.arrow_right_alt),
|
icon: const Icon(Symbols.arrow_right_alt),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context).goNamed('notification');
|
GoRouter.of(context).pushNamed('notification');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/providers/notification.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/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/markdown_content.dart';
|
import 'package:surface/widgets/markdown_content.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';
|
||||||
|
|
||||||
import '../providers/userinfo.dart';
|
import '../providers/userinfo.dart';
|
||||||
@@ -149,15 +146,16 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: PageBackButton(),
|
||||||
title: Text('screenNotification').tr()),
|
title: Text('screenNotification').tr(),
|
||||||
|
),
|
||||||
body: Center(child: UnauthorizedHint()),
|
body: Center(child: UnauthorizedHint()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: PageBackButton(),
|
||||||
title: Text('screenNotification').tr(),
|
title: Text('screenNotification').tr(),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -218,34 +216,24 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
'interactive.subscription',
|
'interactive.subscription',
|
||||||
].contains(nty.topic) &&
|
].contains(nty.topic) &&
|
||||||
nty.metadata['related_post'] != null)
|
nty.metadata['related_post'] != null)
|
||||||
GestureDetector(
|
TextButton(
|
||||||
child: Container(
|
style: ButtonStyle(
|
||||||
decoration: BoxDecoration(
|
padding: WidgetStatePropertyAll(
|
||||||
borderRadius: const BorderRadius.all(
|
EdgeInsets.zero,
|
||||||
Radius.circular(8)),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1),
|
|
||||||
),
|
),
|
||||||
child: PostItem(
|
visualDensity: VisualDensity.compact,
|
||||||
data: SnPost.fromJson(
|
|
||||||
nty.metadata['related_post']!),
|
|
||||||
showComments: false,
|
|
||||||
showReactions: false,
|
|
||||||
showMenu: false,
|
|
||||||
).padding(vertical: 4),
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
child: Text('postReadMore').tr(),
|
||||||
|
onPressed: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postDetail',
|
'postDetail',
|
||||||
pathParameters: {
|
pathParameters: {
|
||||||
'slug': nty
|
'slug': nty.metadata['related_post']['id']
|
||||||
.metadata['related_post']!['id']
|
.toString(),
|
||||||
.toString()
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).padding(top: 8),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import 'package:surface/providers/userinfo.dart';
|
|||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.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/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';
|
||||||
@@ -66,115 +65,111 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
|
|
||||||
final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
|
final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
|
||||||
|
|
||||||
return AppBackground(
|
return AppScaffold(
|
||||||
isRoot: widget.onBack != null,
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
child: AppScaffold(
|
appBar: AppBar(
|
||||||
appBar: AppBar(
|
leading: BackButton(
|
||||||
leading: BackButton(
|
onPressed: () {
|
||||||
onPressed: () {
|
if (widget.onBack != null) {
|
||||||
if (widget.onBack != null) {
|
widget.onBack!.call();
|
||||||
widget.onBack!.call();
|
}
|
||||||
}
|
if (GoRouter.of(context).canPop()) {
|
||||||
if (GoRouter.of(context).canPop()) {
|
GoRouter.of(context).pop(context);
|
||||||
GoRouter.of(context).pop(context);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
GoRouter.of(context).replaceNamed('explore');
|
||||||
GoRouter.of(context).replaceNamed('explore');
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
title: _data?.body['title'] != null
|
|
||||||
? RichText(
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
text: TextSpan(children: [
|
|
||||||
TextSpan(
|
|
||||||
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
|
||||||
color:
|
|
||||||
Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const TextSpan(text: '\n'),
|
|
||||||
TextSpan(
|
|
||||||
text: 'postDetail'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
|
||||||
color:
|
|
||||||
Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
: Text('postDetail').tr(),
|
|
||||||
),
|
),
|
||||||
body: CustomScrollView(
|
title: _data?.body['title'] != null
|
||||||
slivers: [
|
? RichText(
|
||||||
SliverToBoxAdapter(
|
textAlign: TextAlign.center,
|
||||||
child: LoadingIndicator(isActive: _isBusy),
|
text: TextSpan(children: [
|
||||||
),
|
TextSpan(
|
||||||
if (_data != null)
|
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
||||||
SliverToBoxAdapter(
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
child: PostItem(
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
data: _data!,
|
|
||||||
maxWidth: maxWidth,
|
|
||||||
showComments: false,
|
|
||||||
showFullPost: true,
|
|
||||||
onChanged: (data) {
|
|
||||||
setState(() => _data = data);
|
|
||||||
},
|
|
||||||
onDeleted: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_data != null)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Divider(height: 1).padding(top: 8),
|
|
||||||
),
|
|
||||||
if (_data != null)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.comment, size: 24),
|
|
||||||
const Gap(16),
|
|
||||||
Text('postCommentsDetailed')
|
|
||||||
.plural(_data!.metric.replyCount)
|
|
||||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 20, vertical: 12).center(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_data != null && ua.isAuthorized)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: PostCommentQuickAction(
|
|
||||||
parentPost: _data!,
|
|
||||||
maxWidth: maxWidth,
|
|
||||||
onPosted: () {
|
|
||||||
setState(() {
|
|
||||||
_data = _data!.copyWith(
|
|
||||||
metric: _data!.metric.copyWith(
|
|
||||||
replyCount: _data!.metric.replyCount + 1,
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
});
|
const TextSpan(text: '\n'),
|
||||||
_childListKey.currentState!.refresh();
|
TextSpan(
|
||||||
},
|
text: 'postDetail'.tr(),
|
||||||
),
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text('postDetail').tr(),
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: LoadingIndicator(isActive: _isBusy),
|
||||||
|
),
|
||||||
|
if (_data != null)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: PostItem(
|
||||||
|
data: _data!,
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
showComments: false,
|
||||||
|
showFullPost: true,
|
||||||
|
onChanged: (data) {
|
||||||
|
setState(() => _data = data);
|
||||||
|
},
|
||||||
|
onDeleted: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (_data != null) SliverGap(8),
|
),
|
||||||
if (_data != null)
|
if (_data != null)
|
||||||
PostCommentSliverList(
|
SliverToBoxAdapter(
|
||||||
key: _childListKey,
|
child: Divider(height: 1).padding(top: 8),
|
||||||
|
),
|
||||||
|
if (_data != null)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.comment, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text('postCommentsDetailed')
|
||||||
|
.plural(_data!.metric.replyCount)
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, vertical: 12).center(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_data != null && ua.isAuthorized)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: PostCommentQuickAction(
|
||||||
parentPost: _data!,
|
parentPost: _data!,
|
||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
|
onPosted: () {
|
||||||
|
setState(() {
|
||||||
|
_data = _data!.copyWith(
|
||||||
|
metric: _data!.metric.copyWith(
|
||||||
|
replyCount: _data!.metric.replyCount + 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
_childListKey.currentState!.refresh();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (_data != null)
|
),
|
||||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
if (_data != null) SliverGap(8),
|
||||||
],
|
if (_data != null)
|
||||||
),
|
PostCommentSliverList(
|
||||||
|
key: _childListKey,
|
||||||
|
parentPost: _data!,
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
),
|
||||||
|
if (_data != null)
|
||||||
|
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -346,9 +346,15 @@ class _PostEditorScreenState extends State<PostEditorScreen>
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Icons.edit, size: 16),
|
const Icon(Icons.edit, size: 16),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Text('postEditingNotice').tr(args: [
|
Expanded(
|
||||||
'@${_writeController.editingPost!.publisher.name}'
|
child: Text(
|
||||||
]),
|
'postEditingNotice',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
).tr(args: [
|
||||||
|
'@${_writeController.editingPost!.publisher.name}'
|
||||||
|
]),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -28,11 +28,8 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
|
|||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
try {
|
try {
|
||||||
final pt = context.read<SnPostContentProvider>();
|
final pt = context.read<SnPostContentProvider>();
|
||||||
final result = await pt.listPosts(
|
final result =
|
||||||
take: 10,
|
await pt.listPosts(take: 10, offset: _posts.length, isShuffle: true);
|
||||||
offset: _posts.length,
|
|
||||||
isShuffle: true,
|
|
||||||
);
|
|
||||||
_posts.addAll(result.$1);
|
_posts.addAll(result.$1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -57,19 +54,14 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text('postShuffle').tr()),
|
||||||
title: Text('postShuffle').tr(),
|
|
||||||
),
|
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
if (_isBusy || _posts.isEmpty)
|
if (_isBusy || _posts.isEmpty)
|
||||||
const Expanded(
|
const Expanded(
|
||||||
child: Center(
|
child: Center(child: CircularProgressIndicator()))
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CardSwiper(
|
child: CardSwiper(
|
||||||
@@ -81,17 +73,21 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
|
|||||||
final ele = _posts[idx];
|
final ele = _posts[idx];
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: OpenablePostItem(
|
child: Card(
|
||||||
key: ValueKey(ele),
|
color: Theme.of(context).colorScheme.surface,
|
||||||
data: ele,
|
child: OpenablePostItem(
|
||||||
maxWidth: 640,
|
key: ValueKey(ele),
|
||||||
onChanged: (ele) {
|
data: ele,
|
||||||
_posts[idx] = ele;
|
maxWidth: 640,
|
||||||
setState(() {});
|
useReplace: true,
|
||||||
},
|
onChanged: (ele) {
|
||||||
onDeleted: () {
|
_posts[idx] = ele;
|
||||||
_fetchPosts();
|
setState(() {});
|
||||||
},
|
},
|
||||||
|
onDeleted: () {
|
||||||
|
_fetchPosts();
|
||||||
|
},
|
||||||
|
).padding(all: 8),
|
||||||
).padding(
|
).padding(
|
||||||
all: 24,
|
all: 24,
|
||||||
bottom:
|
bottom:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
|
|||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late final ScrollController _scrollController = ScrollController();
|
late final ScrollController _scrollController = ScrollController();
|
||||||
late final TabController _tabController =
|
late final TabController _tabController =
|
||||||
TabController(length: 3, vsync: this);
|
TabController(length: 5, vsync: this);
|
||||||
|
|
||||||
SnPublisher? _publisher;
|
SnPublisher? _publisher;
|
||||||
SnAccount? _account;
|
SnAccount? _account;
|
||||||
@@ -137,7 +137,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
|
|||||||
|
|
||||||
late final _appBarWidth = MediaQuery.of(context).size.width;
|
late final _appBarWidth = MediaQuery.of(context).size.width;
|
||||||
late final _appBarHeight =
|
late final _appBarHeight =
|
||||||
(_appBarWidth * kBannerAspectRatio).roundToDouble();
|
math.min((_appBarWidth * kBannerAspectRatio), 360).roundToDouble();
|
||||||
|
|
||||||
void _updateAppBarBlur() {
|
void _updateAppBarBlur() {
|
||||||
if (_scrollController.offset > _appBarHeight) return;
|
if (_scrollController.offset > _appBarHeight) return;
|
||||||
@@ -165,6 +165,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
|
|||||||
type: switch (_tabController.index) {
|
type: switch (_tabController.index) {
|
||||||
1 => 'story',
|
1 => 'story',
|
||||||
2 => 'article',
|
2 => 'article',
|
||||||
|
3 => 'question',
|
||||||
|
4 => 'video',
|
||||||
_ => null,
|
_ => null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -284,6 +286,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
@@ -300,6 +303,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
|
|||||||
),
|
),
|
||||||
child: SliverAppBar(
|
child: SliverAppBar(
|
||||||
expandedHeight: _appBarHeight,
|
expandedHeight: _appBarHeight,
|
||||||
|
leading: const PageBackButton(),
|
||||||
title: _publisher == null
|
title: _publisher == null
|
||||||
? Text('loading').tr()
|
? Text('loading').tr()
|
||||||
: RichText(
|
: RichText(
|
||||||
@@ -568,6 +572,18 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
|
|||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Tab(
|
||||||
|
icon: Icon(
|
||||||
|
Symbols.help,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
icon: Icon(
|
||||||
|
Symbols.video_call,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(child: const Divider(height: 1)),
|
SliverToBoxAdapter(child: const Divider(height: 1)),
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/config.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/sn_realm.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
@@ -57,7 +59,9 @@ class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
|
|||||||
title: Text('screenRealmDiscovery').tr(),
|
title: Text('screenRealmDiscovery').tr(),
|
||||||
actions: [
|
actions: [
|
||||||
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;
|
context.read<ConfigProvider>().realmCompactView = _isCompactView;
|
||||||
@@ -117,7 +121,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
|||||||
try {
|
try {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
|
final resp =
|
||||||
|
await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
|
||||||
final out = List<SnChannel>.from(
|
final out = List<SnChannel>.from(
|
||||||
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
||||||
);
|
);
|
||||||
@@ -135,10 +140,13 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
|||||||
setState(() => _isJoining = true);
|
setState(() => _isJoining = true);
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
await sn.client.post('/cgi/id/realms/${widget.realm.alias}/members', data: {
|
final rel = context.read<SnRealmProvider>();
|
||||||
|
await sn.client
|
||||||
|
.post('/cgi/id/realms/${widget.realm.alias}/members', data: {
|
||||||
'related': ua.user?.name,
|
'related': ua.user?.name,
|
||||||
});
|
});
|
||||||
await _joinSelectedChannels();
|
await _joinSelectedChannels();
|
||||||
|
rel.addAvailableRealm(widget.realm);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('realmJoined'.tr(args: [widget.realm.name]));
|
context.showSnackbar('realmJoined'.tr(args: [widget.realm.name]));
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -156,13 +164,20 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
|||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
await sn.client.post('/cgi/im/channels/${widget.realm.alias}/$channel/members', data: {
|
await sn.client.post(
|
||||||
'related': ua.user?.name,
|
'/cgi/im/channels/${widget.realm.alias}/$channel/members',
|
||||||
});
|
data: {
|
||||||
|
'related': ua.user?.name,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
}
|
}
|
||||||
|
final ct = context.read<ChatChannelProvider>();
|
||||||
|
for (final channel
|
||||||
|
in _channels!.where((ele) => _planJoinChannels.contains(ele.alias))) {
|
||||||
|
ct.addAvailableChannel(channel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +197,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.group_add, size: 24),
|
const Icon(Symbols.group_add, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('realmJoin', style: Theme.of(context).textTheme.titleLarge).tr(),
|
Text('realmJoin', style: Theme.of(context).textTheme.titleLarge)
|
||||||
|
.tr(),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
Row(
|
Row(
|
||||||
@@ -216,7 +232,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
|||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
|
child: Text('realmCommunityPublicChannelsHint'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium)
|
||||||
.padding(horizontal: 24, vertical: 8),
|
.padding(horizontal: 24, vertical: 8),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final dt = context.read<DatabaseProvider>();
|
final dt = context.read<DatabaseProvider>();
|
||||||
|
final cfg = context.watch<ConfigProvider>();
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -322,20 +325,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
CheckboxListTile(
|
|
||||||
secondary: const Icon(Symbols.left_panel_close),
|
|
||||||
title: Text('settingsDrawerPreferCollapse').tr(),
|
|
||||||
subtitle:
|
|
||||||
Text('settingsDrawerPreferCollapseDescription').tr(),
|
|
||||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
|
||||||
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
|
|
||||||
onChanged: (value) {
|
|
||||||
_prefs.setBool(kAppDrawerPreferCollapse, value ?? false);
|
|
||||||
final cfg = context.read<ConfigProvider>();
|
|
||||||
cfg.calcDrawerSize(context);
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
secondary: const Icon(Symbols.hide),
|
secondary: const Icon(Symbols.hide),
|
||||||
title: Text('settingsHideBottomNav').tr(),
|
title: Text('settingsHideBottomNav').tr(),
|
||||||
@@ -349,6 +338,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
value: cfg.soundEffects,
|
||||||
|
onChanged: (value) {
|
||||||
|
cfg.soundEffects = value ?? false;
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
title: Text('settingsSoundEffects').tr(),
|
||||||
|
subtitle: Text('settingsSoundEffectsDescription').tr(),
|
||||||
|
secondary: const Icon(Symbols.sound_sampler),
|
||||||
|
),
|
||||||
|
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS))
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.window),
|
||||||
|
title: Text('settingsResetMemorizedWindowSize').tr(),
|
||||||
|
subtitle:
|
||||||
|
Text('settingsResetMemorizedWindowSizeDescription')
|
||||||
|
.tr(),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 24),
|
||||||
|
onTap: () {
|
||||||
|
final prefs = context.read<ConfigProvider>().prefs;
|
||||||
|
prefs.remove(kAppWindowSize);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.font_download),
|
leading: const Icon(Symbols.font_download),
|
||||||
title: Text('settingsCustomFonts').tr(),
|
title: Text('settingsCustomFonts').tr(),
|
||||||
@@ -741,6 +755,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
GoRouter.of(context).pushNamed('about');
|
GoRouter.of(context).pushNamed('about');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (now.day == 1 && now.month == 4)
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text('settingsAprilFoolFeatures').tr(),
|
||||||
|
subtitle: Text('settingsAprilFoolFeaturesDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
secondary: const Icon(Symbols.new_releases),
|
||||||
|
value: cfg.aprilFoolFeatures,
|
||||||
|
onChanged: (value) {
|
||||||
|
cfg.aprilFoolFeatures = value ?? false;
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
Card(
|
Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(width: double.infinity),
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(horizontal: 24),
|
const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import 'package:surface/providers/sn_network.dart';
|
|||||||
import 'package:surface/providers/sn_sticker.dart';
|
import 'package:surface/providers/sn_sticker.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/attachment.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/attachment/attachment_item.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';
|
||||||
@@ -134,7 +133,7 @@ class _StickerScreenState extends State<StickerScreen>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: PageBackButton(),
|
||||||
title: Text('screenStickers').tr(),
|
title: Text('screenStickers').tr(),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
@@ -45,10 +45,9 @@ class _WalletScreenState extends State<WalletScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: PageBackButton(),
|
leading: PageBackButton(), title: Text('screenAccountWallet').tr()),
|
||||||
title: Text('screenAccountWallet').tr(),
|
|
||||||
),
|
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
LoadingIndicator(isActive: _isBusy),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
@@ -66,25 +65,36 @@ class _WalletScreenState extends State<WalletScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
|
||||||
radius: 28,
|
|
||||||
child: Icon(Symbols.wallet, size: 28),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
SizedBox(width: double.infinity),
|
SizedBox(width: double.infinity),
|
||||||
Text(
|
Text(
|
||||||
NumberFormat.compactCurrency(
|
NumberFormat.compactCurrency(
|
||||||
locale: EasyLocalization.of(context)!.currentLocale.toString(),
|
locale: EasyLocalization.of(context)!
|
||||||
|
.currentLocale
|
||||||
|
.toString(),
|
||||||
symbol: '${'walletCurrencyShort'.tr()} ',
|
symbol: '${'walletCurrencyShort'.tr()} ',
|
||||||
decimalDigits: 2,
|
decimalDigits: 2,
|
||||||
).format(double.parse(_wallet!.balance)),
|
).format(double.parse(_wallet!.balance)),
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
Text('walletCurrency'.plural(double.parse(_wallet!.balance))),
|
Text('walletCurrency'.plural(double.parse(_wallet!.balance))),
|
||||||
|
const Gap(16),
|
||||||
|
Text(
|
||||||
|
NumberFormat.compactCurrency(
|
||||||
|
locale: EasyLocalization.of(context)!
|
||||||
|
.currentLocale
|
||||||
|
.toString(),
|
||||||
|
symbol: '${'walletCurrencyGoldenShort'.tr()} ',
|
||||||
|
decimalDigits: 2,
|
||||||
|
).format(double.parse(_wallet!.goldenBalance)),
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
Text('walletCurrencyGolden'
|
||||||
|
.plural(double.parse(_wallet!.goldenBalance))),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, vertical: 24),
|
).padding(horizontal: 20, vertical: 24),
|
||||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||||
if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)),
|
if (_wallet != null)
|
||||||
|
Expanded(child: _WalletTransactionList(myself: _wallet!)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -109,14 +119,15 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
|
|||||||
try {
|
try {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/wa/transactions/me', queryParameters: {
|
final resp = await sn.client.get(
|
||||||
'take': 10,
|
'/cgi/wa/transactions/me',
|
||||||
'offset': _transactions.length,
|
queryParameters: {'take': 10, 'offset': _transactions.length},
|
||||||
});
|
|
||||||
_totalCount = resp.data['count'];
|
|
||||||
_transactions.addAll(
|
|
||||||
resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? [],
|
|
||||||
);
|
);
|
||||||
|
_totalCount = resp.data['count'];
|
||||||
|
_transactions.addAll(resp.data['data']
|
||||||
|
?.map((e) => SnTransaction.fromJson(e))
|
||||||
|
.cast<SnTransaction>() ??
|
||||||
|
[]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -141,7 +152,8 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
|
|||||||
child: InfiniteList(
|
child: InfiniteList(
|
||||||
itemCount: _transactions.length,
|
itemCount: _transactions.length,
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!,
|
hasReachedMax:
|
||||||
|
_totalCount != null && _transactions.length >= _totalCount!,
|
||||||
onFetchData: () {
|
onFetchData: () {
|
||||||
_fetchTransactions();
|
_fetchTransactions();
|
||||||
},
|
},
|
||||||
@@ -149,7 +161,9 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
|
|||||||
final ele = _transactions[idx];
|
final ele = _transactions[idx];
|
||||||
final isIncoming = ele.payeeId == widget.myself.id;
|
final isIncoming = ele.payeeId == widget.myself.id;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made),
|
leading: isIncoming
|
||||||
|
? const Icon(Symbols.call_received)
|
||||||
|
: const Icon(Symbols.call_made),
|
||||||
title: Text(
|
title: Text(
|
||||||
'${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
|
'${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
|
||||||
style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
|
style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
|
||||||
@@ -159,12 +173,26 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
|
|||||||
children: [
|
children: [
|
||||||
Text(ele.remark),
|
Text(ele.remark),
|
||||||
const Gap(2),
|
const Gap(2),
|
||||||
Text(
|
Row(
|
||||||
DateFormat(
|
children: [
|
||||||
null,
|
Text(
|
||||||
EasyLocalization.of(context)!.currentLocale.toString(),
|
'walletTransactionType${ele.currency.capitalize()}'
|
||||||
).format(ele.createdAt),
|
.tr(),
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
Text(' · ')
|
||||||
|
.textStyle(Theme.of(context).textTheme.labelSmall!)
|
||||||
|
.padding(right: 4),
|
||||||
|
Text(
|
||||||
|
DateFormat(
|
||||||
|
null,
|
||||||
|
EasyLocalization.of(context)!
|
||||||
|
.currentLocale
|
||||||
|
.toString())
|
||||||
|
.format(ele.createdAt),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -205,17 +233,14 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
controller: passwordController,
|
controller: passwordController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(labelText: 'fieldPassword'.tr()),
|
||||||
labelText: 'fieldPassword'.tr(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(ctx).pop(),
|
onPressed: () => Navigator.of(ctx).pop(),
|
||||||
child: Text('cancel').tr(),
|
child: Text('cancel').tr()),
|
||||||
),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(ctx).pop(passwordController.text);
|
Navigator.of(ctx).pop(passwordController.text);
|
||||||
@@ -234,9 +259,7 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
|
|||||||
try {
|
try {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.post('/cgi/wa/wallets/me', data: {
|
await sn.client.post('/cgi/wa/wallets/me', data: {'password': password});
|
||||||
'password': password,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -255,20 +278,20 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(radius: 28, child: Icon(Symbols.add, size: 28)),
|
||||||
radius: 28,
|
|
||||||
child: Icon(Symbols.add, size: 28),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(),
|
Text('walletCreate',
|
||||||
Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(),
|
style: Theme.of(context).textTheme.titleLarge)
|
||||||
|
.tr(),
|
||||||
|
Text('walletCreateSubtitle',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium)
|
||||||
|
.tr(),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: _isBusy ? null : () => _createWallet(),
|
onPressed: _isBusy ? null : () => _createWallet(),
|
||||||
child: Text('next').tr(),
|
child: Text('next').tr()),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, vertical: 24),
|
).padding(horizontal: 20, vertical: 24),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ abstract class SnAccount with _$SnAccount {
|
|||||||
required String language,
|
required String language,
|
||||||
required SnAccountProfile? profile,
|
required SnAccountProfile? profile,
|
||||||
@Default([]) List<SnAccountBadge> badges,
|
@Default([]) List<SnAccountBadge> badges,
|
||||||
|
@Default([]) List<SnPunishment> punishments,
|
||||||
required DateTime? suspendedAt,
|
required DateTime? suspendedAt,
|
||||||
required int? affiliatedId,
|
required int? affiliatedId,
|
||||||
required int? affiliatedTo,
|
required int? affiliatedTo,
|
||||||
@@ -184,3 +185,63 @@ abstract class SnActionEvent with _$SnActionEvent {
|
|||||||
factory SnActionEvent.fromJson(Map<String, Object?> json) =>
|
factory SnActionEvent.fromJson(Map<String, Object?> json) =>
|
||||||
_$SnActionEventFromJson(json);
|
_$SnActionEventFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class SnProgram with _$SnProgram {
|
||||||
|
const factory SnProgram({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required String name,
|
||||||
|
required String description,
|
||||||
|
required String alias,
|
||||||
|
required int expRequirement,
|
||||||
|
required Map<String, dynamic> price,
|
||||||
|
required Map<String, dynamic> badge,
|
||||||
|
required Map<String, dynamic> group,
|
||||||
|
required Map<String, dynamic> appearance,
|
||||||
|
}) = _SnProgram;
|
||||||
|
|
||||||
|
factory SnProgram.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnProgramFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class SnProgramMember with _$SnProgramMember {
|
||||||
|
const factory SnProgramMember({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required DateTime lastPaid,
|
||||||
|
required SnAccount account,
|
||||||
|
required int accountId,
|
||||||
|
required SnProgram program,
|
||||||
|
required int programId,
|
||||||
|
}) = _SnProgramMember;
|
||||||
|
|
||||||
|
factory SnProgramMember.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnProgramMemberFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class SnPunishment with _$SnPunishment {
|
||||||
|
const factory SnPunishment({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required String reason,
|
||||||
|
required int type,
|
||||||
|
@Default({}) Map<String, dynamic> permNodes,
|
||||||
|
required DateTime? expiredAt,
|
||||||
|
required SnAccount? account,
|
||||||
|
required int? accountId,
|
||||||
|
required SnAccount? moderator,
|
||||||
|
required int? moderatorId,
|
||||||
|
}) = _SnPunishment;
|
||||||
|
|
||||||
|
factory SnPunishment.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnPunishmentFromJson(json);
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,10 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
|
|||||||
?.map((e) => SnAccountBadge.fromJson(e as Map<String, dynamic>))
|
?.map((e) => SnAccountBadge.fromJson(e as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
const [],
|
const [],
|
||||||
|
punishments: (json['punishments'] as List<dynamic>?)
|
||||||
|
?.map((e) => SnPunishment.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
suspendedAt: json['suspended_at'] == null
|
suspendedAt: json['suspended_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['suspended_at'] as String),
|
: DateTime.parse(json['suspended_at'] as String),
|
||||||
@@ -57,6 +61,7 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
|
|||||||
'language': instance.language,
|
'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(),
|
||||||
|
'punishments': instance.punishments.map((e) => e.toJson()).toList(),
|
||||||
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
||||||
'affiliated_id': instance.affiliatedId,
|
'affiliated_id': instance.affiliatedId,
|
||||||
'affiliated_to': instance.affiliatedTo,
|
'affiliated_to': instance.affiliatedTo,
|
||||||
@@ -319,3 +324,104 @@ Map<String, dynamic> _$SnActionEventToJson(_SnActionEvent instance) =>
|
|||||||
'account': instance.account.toJson(),
|
'account': instance.account.toJson(),
|
||||||
'account_id': instance.accountId,
|
'account_id': instance.accountId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_SnProgram _$SnProgramFromJson(Map<String, dynamic> json) => _SnProgram(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
name: json['name'] as String,
|
||||||
|
description: json['description'] as String,
|
||||||
|
alias: json['alias'] as String,
|
||||||
|
expRequirement: (json['exp_requirement'] as num).toInt(),
|
||||||
|
price: json['price'] as Map<String, dynamic>,
|
||||||
|
badge: json['badge'] as Map<String, dynamic>,
|
||||||
|
group: json['group'] as Map<String, dynamic>,
|
||||||
|
appearance: json['appearance'] as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnProgramToJson(_SnProgram instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'name': instance.name,
|
||||||
|
'description': instance.description,
|
||||||
|
'alias': instance.alias,
|
||||||
|
'exp_requirement': instance.expRequirement,
|
||||||
|
'price': instance.price,
|
||||||
|
'badge': instance.badge,
|
||||||
|
'group': instance.group,
|
||||||
|
'appearance': instance.appearance,
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnProgramMember _$SnProgramMemberFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnProgramMember(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
lastPaid: DateTime.parse(json['last_paid'] as String),
|
||||||
|
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||||
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
|
program: SnProgram.fromJson(json['program'] as Map<String, dynamic>),
|
||||||
|
programId: (json['program_id'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnProgramMemberToJson(_SnProgramMember instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'last_paid': instance.lastPaid.toIso8601String(),
|
||||||
|
'account': instance.account.toJson(),
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'program': instance.program.toJson(),
|
||||||
|
'program_id': instance.programId,
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnPunishment _$SnPunishmentFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnPunishment(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
reason: json['reason'] as String,
|
||||||
|
type: (json['type'] as num).toInt(),
|
||||||
|
permNodes: json['perm_nodes'] as Map<String, dynamic>? ?? const {},
|
||||||
|
expiredAt: json['expired_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['expired_at'] as String),
|
||||||
|
account: json['account'] == null
|
||||||
|
? null
|
||||||
|
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||||
|
accountId: (json['account_id'] as num?)?.toInt(),
|
||||||
|
moderator: json['moderator'] == null
|
||||||
|
? null
|
||||||
|
: SnAccount.fromJson(json['moderator'] as Map<String, dynamic>),
|
||||||
|
moderatorId: (json['moderator_id'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnPunishmentToJson(_SnPunishment instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'reason': instance.reason,
|
||||||
|
'type': instance.type,
|
||||||
|
'perm_nodes': instance.permNodes,
|
||||||
|
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||||
|
'account': instance.account?.toJson(),
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'moderator': instance.moderator?.toJson(),
|
||||||
|
'moderator_id': instance.moderatorId,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:surface/types/account.dart';
|
||||||
|
|
||||||
part 'attachment.freezed.dart';
|
part 'attachment.freezed.dart';
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ abstract class SnAttachment with _$SnAttachment {
|
|||||||
required int? refId,
|
required int? refId,
|
||||||
required SnAttachmentPool? pool,
|
required SnAttachmentPool? pool,
|
||||||
required int? poolId,
|
required int? poolId,
|
||||||
|
required SnAccount? account,
|
||||||
required int accountId,
|
required int accountId,
|
||||||
int? thumbnailId,
|
int? thumbnailId,
|
||||||
SnAttachment? thumbnail,
|
SnAttachment? thumbnail,
|
||||||
@@ -49,7 +51,8 @@ abstract class SnAttachment with _$SnAttachment {
|
|||||||
@Default({}) Map<String, dynamic> metadata,
|
@Default({}) Map<String, dynamic> metadata,
|
||||||
}) = _SnAttachment;
|
}) = _SnAttachment;
|
||||||
|
|
||||||
factory SnAttachment.fromJson(Map<String, Object?> json) => _$SnAttachmentFromJson(json);
|
factory SnAttachment.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnAttachmentFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> get data => {
|
Map<String, dynamic> get data => {
|
||||||
...metadata,
|
...metadata,
|
||||||
@@ -85,7 +88,8 @@ abstract class SnAttachmentFragment with _$SnAttachmentFragment {
|
|||||||
@Default([]) List<String> fileChunksMissing,
|
@Default([]) List<String> fileChunksMissing,
|
||||||
}) = _SnAttachmentFragment;
|
}) = _SnAttachmentFragment;
|
||||||
|
|
||||||
factory SnAttachmentFragment.fromJson(Map<String, Object?> json) => _$SnAttachmentFragmentFromJson(json);
|
factory SnAttachmentFragment.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnAttachmentFragmentFromJson(json);
|
||||||
|
|
||||||
SnMediaType get mediaType => switch (mimetype.split('/').firstOrNull) {
|
SnMediaType get mediaType => switch (mimetype.split('/').firstOrNull) {
|
||||||
'image' => SnMediaType.image,
|
'image' => SnMediaType.image,
|
||||||
@@ -109,7 +113,8 @@ abstract class SnAttachmentPool with _$SnAttachmentPool {
|
|||||||
required int? accountId,
|
required int? accountId,
|
||||||
}) = _SnAttachmentPool;
|
}) = _SnAttachmentPool;
|
||||||
|
|
||||||
factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json);
|
factory SnAttachmentPool.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnAttachmentPoolFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@@ -122,7 +127,8 @@ abstract class SnAttachmentDestination with _$SnAttachmentDestination {
|
|||||||
required bool isBoost,
|
required bool isBoost,
|
||||||
}) = _SnAttachmentDestination;
|
}) = _SnAttachmentDestination;
|
||||||
|
|
||||||
factory SnAttachmentDestination.fromJson(Map<String, Object?> json) => _$SnAttachmentDestinationFromJson(json);
|
factory SnAttachmentDestination.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnAttachmentDestinationFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@@ -139,7 +145,8 @@ abstract class SnAttachmentBoost with _$SnAttachmentBoost {
|
|||||||
required int account,
|
required int account,
|
||||||
}) = _SnAttachmentBoost;
|
}) = _SnAttachmentBoost;
|
||||||
|
|
||||||
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json);
|
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnAttachmentBoostFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@@ -158,7 +165,8 @@ abstract class SnSticker with _$SnSticker {
|
|||||||
required int accountId,
|
required int accountId,
|
||||||
}) = _SnSticker;
|
}) = _SnSticker;
|
||||||
|
|
||||||
factory SnSticker.fromJson(Map<String, Object?> json) => _$SnStickerFromJson(json);
|
factory SnSticker.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnStickerFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@@ -175,7 +183,8 @@ abstract class SnStickerPack with _$SnStickerPack {
|
|||||||
required int accountId,
|
required int accountId,
|
||||||
}) = _SnStickerPack;
|
}) = _SnStickerPack;
|
||||||
|
|
||||||
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json);
|
factory SnStickerPack.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnStickerPackFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@@ -186,5 +195,6 @@ abstract class SnAttachmentBilling with _$SnAttachmentBilling {
|
|||||||
required double includedRatio,
|
required double includedRatio,
|
||||||
}) = _SnAttachmentBilling;
|
}) = _SnAttachmentBilling;
|
||||||
|
|
||||||
factory SnAttachmentBilling.fromJson(Map<String, Object?> json) => _$SnAttachmentBillingFromJson(json);
|
factory SnAttachmentBilling.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnAttachmentBillingFromJson(json);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ mixin _$SnAttachment {
|
|||||||
int? get refId;
|
int? get refId;
|
||||||
SnAttachmentPool? get pool;
|
SnAttachmentPool? get pool;
|
||||||
int? get poolId;
|
int? get poolId;
|
||||||
|
SnAccount? get account;
|
||||||
int get accountId;
|
int get accountId;
|
||||||
int? get thumbnailId;
|
int? get thumbnailId;
|
||||||
SnAttachment? get thumbnail;
|
SnAttachment? get thumbnail;
|
||||||
@@ -98,6 +99,7 @@ mixin _$SnAttachment {
|
|||||||
(identical(other.refId, refId) || other.refId == refId) &&
|
(identical(other.refId, refId) || other.refId == refId) &&
|
||||||
(identical(other.pool, pool) || other.pool == pool) &&
|
(identical(other.pool, pool) || other.pool == pool) &&
|
||||||
(identical(other.poolId, poolId) || other.poolId == poolId) &&
|
(identical(other.poolId, poolId) || other.poolId == poolId) &&
|
||||||
|
(identical(other.account, account) || other.account == account) &&
|
||||||
(identical(other.accountId, accountId) ||
|
(identical(other.accountId, accountId) ||
|
||||||
other.accountId == accountId) &&
|
other.accountId == accountId) &&
|
||||||
(identical(other.thumbnailId, thumbnailId) ||
|
(identical(other.thumbnailId, thumbnailId) ||
|
||||||
@@ -140,6 +142,7 @@ mixin _$SnAttachment {
|
|||||||
refId,
|
refId,
|
||||||
pool,
|
pool,
|
||||||
poolId,
|
poolId,
|
||||||
|
account,
|
||||||
accountId,
|
accountId,
|
||||||
thumbnailId,
|
thumbnailId,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
@@ -152,7 +155,7 @@ mixin _$SnAttachment {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +189,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
|
|||||||
int? refId,
|
int? refId,
|
||||||
SnAttachmentPool? pool,
|
SnAttachmentPool? pool,
|
||||||
int? poolId,
|
int? poolId,
|
||||||
|
SnAccount? account,
|
||||||
int accountId,
|
int accountId,
|
||||||
int? thumbnailId,
|
int? thumbnailId,
|
||||||
SnAttachment? thumbnail,
|
SnAttachment? thumbnail,
|
||||||
@@ -197,6 +201,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
|
|||||||
|
|
||||||
$SnAttachmentCopyWith<$Res>? get ref;
|
$SnAttachmentCopyWith<$Res>? get ref;
|
||||||
$SnAttachmentPoolCopyWith<$Res>? get pool;
|
$SnAttachmentPoolCopyWith<$Res>? get pool;
|
||||||
|
$SnAccountCopyWith<$Res>? get account;
|
||||||
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
||||||
$SnAttachmentCopyWith<$Res>? get compressed;
|
$SnAttachmentCopyWith<$Res>? get compressed;
|
||||||
}
|
}
|
||||||
@@ -236,6 +241,7 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
|||||||
Object? refId = freezed,
|
Object? refId = freezed,
|
||||||
Object? pool = freezed,
|
Object? pool = freezed,
|
||||||
Object? poolId = freezed,
|
Object? poolId = freezed,
|
||||||
|
Object? account = freezed,
|
||||||
Object? accountId = null,
|
Object? accountId = null,
|
||||||
Object? thumbnailId = freezed,
|
Object? thumbnailId = freezed,
|
||||||
Object? thumbnail = freezed,
|
Object? thumbnail = freezed,
|
||||||
@@ -338,6 +344,10 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
|||||||
? _self.poolId
|
? _self.poolId
|
||||||
: poolId // ignore: cast_nullable_to_non_nullable
|
: poolId // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,
|
as int?,
|
||||||
|
account: freezed == account
|
||||||
|
? _self.account
|
||||||
|
: account // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccount?,
|
||||||
accountId: null == accountId
|
accountId: null == accountId
|
||||||
? _self.accountId
|
? _self.accountId
|
||||||
: accountId // ignore: cast_nullable_to_non_nullable
|
: accountId // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -401,6 +411,20 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnAttachment
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnAccountCopyWith<$Res>? get account {
|
||||||
|
if (_self.account == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||||
|
return _then(_self.copyWith(account: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a copy of SnAttachment
|
/// Create a copy of SnAttachment
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@@ -457,6 +481,7 @@ class _SnAttachment extends SnAttachment {
|
|||||||
required this.refId,
|
required this.refId,
|
||||||
required this.pool,
|
required this.pool,
|
||||||
required this.poolId,
|
required this.poolId,
|
||||||
|
required this.account,
|
||||||
required this.accountId,
|
required this.accountId,
|
||||||
this.thumbnailId,
|
this.thumbnailId,
|
||||||
this.thumbnail,
|
this.thumbnail,
|
||||||
@@ -521,6 +546,8 @@ class _SnAttachment extends SnAttachment {
|
|||||||
@override
|
@override
|
||||||
final int? poolId;
|
final int? poolId;
|
||||||
@override
|
@override
|
||||||
|
final SnAccount? account;
|
||||||
|
@override
|
||||||
final int accountId;
|
final int accountId;
|
||||||
@override
|
@override
|
||||||
final int? thumbnailId;
|
final int? thumbnailId;
|
||||||
@@ -612,6 +639,7 @@ class _SnAttachment extends SnAttachment {
|
|||||||
(identical(other.refId, refId) || other.refId == refId) &&
|
(identical(other.refId, refId) || other.refId == refId) &&
|
||||||
(identical(other.pool, pool) || other.pool == pool) &&
|
(identical(other.pool, pool) || other.pool == pool) &&
|
||||||
(identical(other.poolId, poolId) || other.poolId == poolId) &&
|
(identical(other.poolId, poolId) || other.poolId == poolId) &&
|
||||||
|
(identical(other.account, account) || other.account == account) &&
|
||||||
(identical(other.accountId, accountId) ||
|
(identical(other.accountId, accountId) ||
|
||||||
other.accountId == accountId) &&
|
other.accountId == accountId) &&
|
||||||
(identical(other.thumbnailId, thumbnailId) ||
|
(identical(other.thumbnailId, thumbnailId) ||
|
||||||
@@ -654,6 +682,7 @@ class _SnAttachment extends SnAttachment {
|
|||||||
refId,
|
refId,
|
||||||
pool,
|
pool,
|
||||||
poolId,
|
poolId,
|
||||||
|
account,
|
||||||
accountId,
|
accountId,
|
||||||
thumbnailId,
|
thumbnailId,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
@@ -666,7 +695,7 @@ class _SnAttachment extends SnAttachment {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,6 +731,7 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
|
|||||||
int? refId,
|
int? refId,
|
||||||
SnAttachmentPool? pool,
|
SnAttachmentPool? pool,
|
||||||
int? poolId,
|
int? poolId,
|
||||||
|
SnAccount? account,
|
||||||
int accountId,
|
int accountId,
|
||||||
int? thumbnailId,
|
int? thumbnailId,
|
||||||
SnAttachment? thumbnail,
|
SnAttachment? thumbnail,
|
||||||
@@ -716,6 +746,8 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
|
|||||||
@override
|
@override
|
||||||
$SnAttachmentPoolCopyWith<$Res>? get pool;
|
$SnAttachmentPoolCopyWith<$Res>? get pool;
|
||||||
@override
|
@override
|
||||||
|
$SnAccountCopyWith<$Res>? get account;
|
||||||
|
@override
|
||||||
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
||||||
@override
|
@override
|
||||||
$SnAttachmentCopyWith<$Res>? get compressed;
|
$SnAttachmentCopyWith<$Res>? get compressed;
|
||||||
@@ -757,6 +789,7 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
|||||||
Object? refId = freezed,
|
Object? refId = freezed,
|
||||||
Object? pool = freezed,
|
Object? pool = freezed,
|
||||||
Object? poolId = freezed,
|
Object? poolId = freezed,
|
||||||
|
Object? account = freezed,
|
||||||
Object? accountId = null,
|
Object? accountId = null,
|
||||||
Object? thumbnailId = freezed,
|
Object? thumbnailId = freezed,
|
||||||
Object? thumbnail = freezed,
|
Object? thumbnail = freezed,
|
||||||
@@ -859,6 +892,10 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
|||||||
? _self.poolId
|
? _self.poolId
|
||||||
: poolId // ignore: cast_nullable_to_non_nullable
|
: poolId // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,
|
as int?,
|
||||||
|
account: freezed == account
|
||||||
|
? _self.account
|
||||||
|
: account // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccount?,
|
||||||
accountId: null == accountId
|
accountId: null == accountId
|
||||||
? _self.accountId
|
? _self.accountId
|
||||||
: accountId // ignore: cast_nullable_to_non_nullable
|
: accountId // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -922,6 +959,20 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnAttachment
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnAccountCopyWith<$Res>? get account {
|
||||||
|
if (_self.account == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||||
|
return _then(_self.copyWith(account: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a copy of SnAttachment
|
/// Create a copy of SnAttachment
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ _SnAttachment _$SnAttachmentFromJson(Map<String, dynamic> json) =>
|
|||||||
? null
|
? null
|
||||||
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
|
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
|
||||||
poolId: (json['pool_id'] as num?)?.toInt(),
|
poolId: (json['pool_id'] as num?)?.toInt(),
|
||||||
|
account: json['account'] == null
|
||||||
|
? null
|
||||||
|
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||||
accountId: (json['account_id'] as num).toInt(),
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
|
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
|
||||||
thumbnail: json['thumbnail'] == null
|
thumbnail: json['thumbnail'] == null
|
||||||
@@ -82,6 +85,7 @@ Map<String, dynamic> _$SnAttachmentToJson(_SnAttachment instance) =>
|
|||||||
'ref_id': instance.refId,
|
'ref_id': instance.refId,
|
||||||
'pool': instance.pool?.toJson(),
|
'pool': instance.pool?.toJson(),
|
||||||
'pool_id': instance.poolId,
|
'pool_id': instance.poolId,
|
||||||
|
'account': instance.account?.toJson(),
|
||||||
'account_id': instance.accountId,
|
'account_id': instance.accountId,
|
||||||
'thumbnail_id': instance.thumbnailId,
|
'thumbnail_id': instance.thumbnailId,
|
||||||
'thumbnail': instance.thumbnail?.toJson(),
|
'thumbnail': instance.thumbnail?.toJson(),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:surface/types/account.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/poll.dart';
|
import 'package:surface/types/poll.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
@@ -26,6 +27,7 @@ abstract class SnPost with _$SnPost {
|
|||||||
required int? replyId,
|
required int? replyId,
|
||||||
required int? repostId,
|
required int? repostId,
|
||||||
required int? realmId,
|
required int? realmId,
|
||||||
|
required SnRealm? realm,
|
||||||
required SnPost? replyTo,
|
required SnPost? replyTo,
|
||||||
required SnPost? repostTo,
|
required SnPost? repostTo,
|
||||||
required List<int>? visibleUsersList,
|
required List<int>? visibleUsersList,
|
||||||
@@ -43,9 +45,9 @@ abstract class SnPost with _$SnPost {
|
|||||||
@Default(0) int totalAggregatedViews,
|
@Default(0) int totalAggregatedViews,
|
||||||
required int publisherId,
|
required int publisherId,
|
||||||
required int? pollId,
|
required int? pollId,
|
||||||
|
required SnPoll? poll,
|
||||||
required SnPublisher publisher,
|
required SnPublisher publisher,
|
||||||
required SnMetric metric,
|
required SnMetric metric,
|
||||||
SnPostPreload? preload,
|
|
||||||
}) = _SnPost;
|
}) = _SnPost;
|
||||||
|
|
||||||
factory SnPost.fromJson(Map<String, Object?> json) => _$SnPostFromJson(json);
|
factory SnPost.fromJson(Map<String, Object?> json) => _$SnPostFromJson(json);
|
||||||
@@ -146,6 +148,7 @@ abstract class SnPublisher with _$SnPublisher {
|
|||||||
required int totalDownvote,
|
required int totalDownvote,
|
||||||
required int? realmId,
|
required int? realmId,
|
||||||
required int accountId,
|
required int accountId,
|
||||||
|
required SnAccount? account,
|
||||||
}) = _SnPublisher;
|
}) = _SnPublisher;
|
||||||
|
|
||||||
factory SnPublisher.fromJson(Map<String, Object?> json) =>
|
factory SnPublisher.fromJson(Map<String, Object?> json) =>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ mixin _$SnPost {
|
|||||||
int? get replyId;
|
int? get replyId;
|
||||||
int? get repostId;
|
int? get repostId;
|
||||||
int? get realmId;
|
int? get realmId;
|
||||||
|
SnRealm? get realm;
|
||||||
SnPost? get replyTo;
|
SnPost? get replyTo;
|
||||||
SnPost? get repostTo;
|
SnPost? get repostTo;
|
||||||
List<int>? get visibleUsersList;
|
List<int>? get visibleUsersList;
|
||||||
@@ -47,9 +48,9 @@ mixin _$SnPost {
|
|||||||
int get totalAggregatedViews;
|
int get totalAggregatedViews;
|
||||||
int get publisherId;
|
int get publisherId;
|
||||||
int? get pollId;
|
int? get pollId;
|
||||||
|
SnPoll? get poll;
|
||||||
SnPublisher get publisher;
|
SnPublisher get publisher;
|
||||||
SnMetric get metric;
|
SnMetric get metric;
|
||||||
SnPostPreload? get preload;
|
|
||||||
|
|
||||||
/// Create a copy of SnPost
|
/// Create a copy of SnPost
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -88,6 +89,7 @@ mixin _$SnPost {
|
|||||||
(identical(other.repostId, repostId) ||
|
(identical(other.repostId, repostId) ||
|
||||||
other.repostId == repostId) &&
|
other.repostId == repostId) &&
|
||||||
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
||||||
|
(identical(other.realm, realm) || other.realm == realm) &&
|
||||||
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
|
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
|
||||||
(identical(other.repostTo, repostTo) ||
|
(identical(other.repostTo, repostTo) ||
|
||||||
other.repostTo == repostTo) &&
|
other.repostTo == repostTo) &&
|
||||||
@@ -119,10 +121,10 @@ mixin _$SnPost {
|
|||||||
(identical(other.publisherId, publisherId) ||
|
(identical(other.publisherId, publisherId) ||
|
||||||
other.publisherId == publisherId) &&
|
other.publisherId == publisherId) &&
|
||||||
(identical(other.pollId, pollId) || other.pollId == pollId) &&
|
(identical(other.pollId, pollId) || other.pollId == pollId) &&
|
||||||
|
(identical(other.poll, poll) || other.poll == poll) &&
|
||||||
(identical(other.publisher, publisher) ||
|
(identical(other.publisher, publisher) ||
|
||||||
other.publisher == publisher) &&
|
other.publisher == publisher) &&
|
||||||
(identical(other.metric, metric) || other.metric == metric) &&
|
(identical(other.metric, metric) || other.metric == metric));
|
||||||
(identical(other.preload, preload) || other.preload == preload));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -144,6 +146,7 @@ mixin _$SnPost {
|
|||||||
replyId,
|
replyId,
|
||||||
repostId,
|
repostId,
|
||||||
realmId,
|
realmId,
|
||||||
|
realm,
|
||||||
replyTo,
|
replyTo,
|
||||||
repostTo,
|
repostTo,
|
||||||
const DeepCollectionEquality().hash(visibleUsersList),
|
const DeepCollectionEquality().hash(visibleUsersList),
|
||||||
@@ -161,14 +164,14 @@ mixin _$SnPost {
|
|||||||
totalAggregatedViews,
|
totalAggregatedViews,
|
||||||
publisherId,
|
publisherId,
|
||||||
pollId,
|
pollId,
|
||||||
|
poll,
|
||||||
publisher,
|
publisher,
|
||||||
metric,
|
metric
|
||||||
preload
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)';
|
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, realm: $realm, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, poll: $poll, publisher: $publisher, metric: $metric)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +196,7 @@ abstract mixin class $SnPostCopyWith<$Res> {
|
|||||||
int? replyId,
|
int? replyId,
|
||||||
int? repostId,
|
int? repostId,
|
||||||
int? realmId,
|
int? realmId,
|
||||||
|
SnRealm? realm,
|
||||||
SnPost? replyTo,
|
SnPost? replyTo,
|
||||||
SnPost? repostTo,
|
SnPost? repostTo,
|
||||||
List<int>? visibleUsersList,
|
List<int>? visibleUsersList,
|
||||||
@@ -210,15 +214,16 @@ abstract mixin class $SnPostCopyWith<$Res> {
|
|||||||
int totalAggregatedViews,
|
int totalAggregatedViews,
|
||||||
int publisherId,
|
int publisherId,
|
||||||
int? pollId,
|
int? pollId,
|
||||||
|
SnPoll? poll,
|
||||||
SnPublisher publisher,
|
SnPublisher publisher,
|
||||||
SnMetric metric,
|
SnMetric metric});
|
||||||
SnPostPreload? preload});
|
|
||||||
|
|
||||||
|
$SnRealmCopyWith<$Res>? get realm;
|
||||||
$SnPostCopyWith<$Res>? get replyTo;
|
$SnPostCopyWith<$Res>? get replyTo;
|
||||||
$SnPostCopyWith<$Res>? get repostTo;
|
$SnPostCopyWith<$Res>? get repostTo;
|
||||||
|
$SnPollCopyWith<$Res>? get poll;
|
||||||
$SnPublisherCopyWith<$Res> get publisher;
|
$SnPublisherCopyWith<$Res> get publisher;
|
||||||
$SnMetricCopyWith<$Res> get metric;
|
$SnMetricCopyWith<$Res> get metric;
|
||||||
$SnPostPreloadCopyWith<$Res>? get preload;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -248,6 +253,7 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
|||||||
Object? replyId = freezed,
|
Object? replyId = freezed,
|
||||||
Object? repostId = freezed,
|
Object? repostId = freezed,
|
||||||
Object? realmId = freezed,
|
Object? realmId = freezed,
|
||||||
|
Object? realm = freezed,
|
||||||
Object? replyTo = freezed,
|
Object? replyTo = freezed,
|
||||||
Object? repostTo = freezed,
|
Object? repostTo = freezed,
|
||||||
Object? visibleUsersList = freezed,
|
Object? visibleUsersList = freezed,
|
||||||
@@ -265,9 +271,9 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
|||||||
Object? totalAggregatedViews = null,
|
Object? totalAggregatedViews = null,
|
||||||
Object? publisherId = null,
|
Object? publisherId = null,
|
||||||
Object? pollId = freezed,
|
Object? pollId = freezed,
|
||||||
|
Object? poll = freezed,
|
||||||
Object? publisher = null,
|
Object? publisher = null,
|
||||||
Object? metric = null,
|
Object? metric = null,
|
||||||
Object? preload = freezed,
|
|
||||||
}) {
|
}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
id: null == id
|
id: null == id
|
||||||
@@ -330,6 +336,10 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
|||||||
? _self.realmId
|
? _self.realmId
|
||||||
: realmId // ignore: cast_nullable_to_non_nullable
|
: realmId // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,
|
as int?,
|
||||||
|
realm: freezed == realm
|
||||||
|
? _self.realm
|
||||||
|
: realm // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnRealm?,
|
||||||
replyTo: freezed == replyTo
|
replyTo: freezed == replyTo
|
||||||
? _self.replyTo
|
? _self.replyTo
|
||||||
: replyTo // ignore: cast_nullable_to_non_nullable
|
: replyTo // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -398,6 +408,10 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
|||||||
? _self.pollId
|
? _self.pollId
|
||||||
: pollId // ignore: cast_nullable_to_non_nullable
|
: pollId // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,
|
as int?,
|
||||||
|
poll: freezed == poll
|
||||||
|
? _self.poll
|
||||||
|
: poll // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnPoll?,
|
||||||
publisher: null == publisher
|
publisher: null == publisher
|
||||||
? _self.publisher
|
? _self.publisher
|
||||||
: publisher // ignore: cast_nullable_to_non_nullable
|
: publisher // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -406,13 +420,23 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
|||||||
? _self.metric
|
? _self.metric
|
||||||
: metric // ignore: cast_nullable_to_non_nullable
|
: metric // ignore: cast_nullable_to_non_nullable
|
||||||
as SnMetric,
|
as SnMetric,
|
||||||
preload: freezed == preload
|
|
||||||
? _self.preload
|
|
||||||
: preload // ignore: cast_nullable_to_non_nullable
|
|
||||||
as SnPostPreload?,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnPost
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnRealmCopyWith<$Res>? get realm {
|
||||||
|
if (_self.realm == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnRealmCopyWith<$Res>(_self.realm!, (value) {
|
||||||
|
return _then(_self.copyWith(realm: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a copy of SnPost
|
/// Create a copy of SnPost
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@@ -441,6 +465,20 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnPost
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPollCopyWith<$Res>? get poll {
|
||||||
|
if (_self.poll == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnPollCopyWith<$Res>(_self.poll!, (value) {
|
||||||
|
return _then(_self.copyWith(poll: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a copy of SnPost
|
/// Create a copy of SnPost
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@@ -460,20 +498,6 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
|||||||
return _then(_self.copyWith(metric: value));
|
return _then(_self.copyWith(metric: value));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a copy of SnPost
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@override
|
|
||||||
@pragma('vm:prefer-inline')
|
|
||||||
$SnPostPreloadCopyWith<$Res>? get preload {
|
|
||||||
if (_self.preload == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $SnPostPreloadCopyWith<$Res>(_self.preload!, (value) {
|
|
||||||
return _then(_self.copyWith(preload: value));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -495,6 +519,7 @@ class _SnPost extends SnPost {
|
|||||||
required this.replyId,
|
required this.replyId,
|
||||||
required this.repostId,
|
required this.repostId,
|
||||||
required this.realmId,
|
required this.realmId,
|
||||||
|
required this.realm,
|
||||||
required this.replyTo,
|
required this.replyTo,
|
||||||
required this.repostTo,
|
required this.repostTo,
|
||||||
required final List<int>? visibleUsersList,
|
required final List<int>? visibleUsersList,
|
||||||
@@ -512,9 +537,9 @@ class _SnPost extends SnPost {
|
|||||||
this.totalAggregatedViews = 0,
|
this.totalAggregatedViews = 0,
|
||||||
required this.publisherId,
|
required this.publisherId,
|
||||||
required this.pollId,
|
required this.pollId,
|
||||||
|
required this.poll,
|
||||||
required this.publisher,
|
required this.publisher,
|
||||||
required this.metric,
|
required this.metric})
|
||||||
this.preload})
|
|
||||||
: _body = body,
|
: _body = body,
|
||||||
_tags = tags,
|
_tags = tags,
|
||||||
_categories = categories,
|
_categories = categories,
|
||||||
@@ -583,6 +608,8 @@ class _SnPost extends SnPost {
|
|||||||
@override
|
@override
|
||||||
final int? realmId;
|
final int? realmId;
|
||||||
@override
|
@override
|
||||||
|
final SnRealm? realm;
|
||||||
|
@override
|
||||||
final SnPost? replyTo;
|
final SnPost? replyTo;
|
||||||
@override
|
@override
|
||||||
final SnPost? repostTo;
|
final SnPost? repostTo;
|
||||||
@@ -637,11 +664,11 @@ class _SnPost extends SnPost {
|
|||||||
@override
|
@override
|
||||||
final int? pollId;
|
final int? pollId;
|
||||||
@override
|
@override
|
||||||
|
final SnPoll? poll;
|
||||||
|
@override
|
||||||
final SnPublisher publisher;
|
final SnPublisher publisher;
|
||||||
@override
|
@override
|
||||||
final SnMetric metric;
|
final SnMetric metric;
|
||||||
@override
|
|
||||||
final SnPostPreload? preload;
|
|
||||||
|
|
||||||
/// Create a copy of SnPost
|
/// Create a copy of SnPost
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -685,6 +712,7 @@ class _SnPost extends SnPost {
|
|||||||
(identical(other.repostId, repostId) ||
|
(identical(other.repostId, repostId) ||
|
||||||
other.repostId == repostId) &&
|
other.repostId == repostId) &&
|
||||||
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
||||||
|
(identical(other.realm, realm) || other.realm == realm) &&
|
||||||
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
|
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
|
||||||
(identical(other.repostTo, repostTo) ||
|
(identical(other.repostTo, repostTo) ||
|
||||||
other.repostTo == repostTo) &&
|
other.repostTo == repostTo) &&
|
||||||
@@ -716,10 +744,10 @@ class _SnPost extends SnPost {
|
|||||||
(identical(other.publisherId, publisherId) ||
|
(identical(other.publisherId, publisherId) ||
|
||||||
other.publisherId == publisherId) &&
|
other.publisherId == publisherId) &&
|
||||||
(identical(other.pollId, pollId) || other.pollId == pollId) &&
|
(identical(other.pollId, pollId) || other.pollId == pollId) &&
|
||||||
|
(identical(other.poll, poll) || other.poll == poll) &&
|
||||||
(identical(other.publisher, publisher) ||
|
(identical(other.publisher, publisher) ||
|
||||||
other.publisher == publisher) &&
|
other.publisher == publisher) &&
|
||||||
(identical(other.metric, metric) || other.metric == metric) &&
|
(identical(other.metric, metric) || other.metric == metric));
|
||||||
(identical(other.preload, preload) || other.preload == preload));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -741,6 +769,7 @@ class _SnPost extends SnPost {
|
|||||||
replyId,
|
replyId,
|
||||||
repostId,
|
repostId,
|
||||||
realmId,
|
realmId,
|
||||||
|
realm,
|
||||||
replyTo,
|
replyTo,
|
||||||
repostTo,
|
repostTo,
|
||||||
const DeepCollectionEquality().hash(_visibleUsersList),
|
const DeepCollectionEquality().hash(_visibleUsersList),
|
||||||
@@ -758,14 +787,14 @@ class _SnPost extends SnPost {
|
|||||||
totalAggregatedViews,
|
totalAggregatedViews,
|
||||||
publisherId,
|
publisherId,
|
||||||
pollId,
|
pollId,
|
||||||
|
poll,
|
||||||
publisher,
|
publisher,
|
||||||
metric,
|
metric
|
||||||
preload
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)';
|
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, realm: $realm, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, poll: $poll, publisher: $publisher, metric: $metric)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -791,6 +820,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
|||||||
int? replyId,
|
int? replyId,
|
||||||
int? repostId,
|
int? repostId,
|
||||||
int? realmId,
|
int? realmId,
|
||||||
|
SnRealm? realm,
|
||||||
SnPost? replyTo,
|
SnPost? replyTo,
|
||||||
SnPost? repostTo,
|
SnPost? repostTo,
|
||||||
List<int>? visibleUsersList,
|
List<int>? visibleUsersList,
|
||||||
@@ -808,20 +838,22 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
|||||||
int totalAggregatedViews,
|
int totalAggregatedViews,
|
||||||
int publisherId,
|
int publisherId,
|
||||||
int? pollId,
|
int? pollId,
|
||||||
|
SnPoll? poll,
|
||||||
SnPublisher publisher,
|
SnPublisher publisher,
|
||||||
SnMetric metric,
|
SnMetric metric});
|
||||||
SnPostPreload? preload});
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
$SnRealmCopyWith<$Res>? get realm;
|
||||||
@override
|
@override
|
||||||
$SnPostCopyWith<$Res>? get replyTo;
|
$SnPostCopyWith<$Res>? get replyTo;
|
||||||
@override
|
@override
|
||||||
$SnPostCopyWith<$Res>? get repostTo;
|
$SnPostCopyWith<$Res>? get repostTo;
|
||||||
@override
|
@override
|
||||||
|
$SnPollCopyWith<$Res>? get poll;
|
||||||
|
@override
|
||||||
$SnPublisherCopyWith<$Res> get publisher;
|
$SnPublisherCopyWith<$Res> get publisher;
|
||||||
@override
|
@override
|
||||||
$SnMetricCopyWith<$Res> get metric;
|
$SnMetricCopyWith<$Res> get metric;
|
||||||
@override
|
|
||||||
$SnPostPreloadCopyWith<$Res>? get preload;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -851,6 +883,7 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
|||||||
Object? replyId = freezed,
|
Object? replyId = freezed,
|
||||||
Object? repostId = freezed,
|
Object? repostId = freezed,
|
||||||
Object? realmId = freezed,
|
Object? realmId = freezed,
|
||||||
|
Object? realm = freezed,
|
||||||
Object? replyTo = freezed,
|
Object? replyTo = freezed,
|
||||||
Object? repostTo = freezed,
|
Object? repostTo = freezed,
|
||||||
Object? visibleUsersList = freezed,
|
Object? visibleUsersList = freezed,
|
||||||
@@ -868,9 +901,9 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
|||||||
Object? totalAggregatedViews = null,
|
Object? totalAggregatedViews = null,
|
||||||
Object? publisherId = null,
|
Object? publisherId = null,
|
||||||
Object? pollId = freezed,
|
Object? pollId = freezed,
|
||||||
|
Object? poll = freezed,
|
||||||
Object? publisher = null,
|
Object? publisher = null,
|
||||||
Object? metric = null,
|
Object? metric = null,
|
||||||
Object? preload = freezed,
|
|
||||||
}) {
|
}) {
|
||||||
return _then(_SnPost(
|
return _then(_SnPost(
|
||||||
id: null == id
|
id: null == id
|
||||||
@@ -933,6 +966,10 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
|||||||
? _self.realmId
|
? _self.realmId
|
||||||
: realmId // ignore: cast_nullable_to_non_nullable
|
: realmId // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,
|
as int?,
|
||||||
|
realm: freezed == realm
|
||||||
|
? _self.realm
|
||||||
|
: realm // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnRealm?,
|
||||||
replyTo: freezed == replyTo
|
replyTo: freezed == replyTo
|
||||||
? _self.replyTo
|
? _self.replyTo
|
||||||
: replyTo // ignore: cast_nullable_to_non_nullable
|
: replyTo // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -1001,6 +1038,10 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
|||||||
? _self.pollId
|
? _self.pollId
|
||||||
: pollId // ignore: cast_nullable_to_non_nullable
|
: pollId // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,
|
as int?,
|
||||||
|
poll: freezed == poll
|
||||||
|
? _self.poll
|
||||||
|
: poll // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnPoll?,
|
||||||
publisher: null == publisher
|
publisher: null == publisher
|
||||||
? _self.publisher
|
? _self.publisher
|
||||||
: publisher // ignore: cast_nullable_to_non_nullable
|
: publisher // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -1009,13 +1050,23 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
|||||||
? _self.metric
|
? _self.metric
|
||||||
: metric // ignore: cast_nullable_to_non_nullable
|
: metric // ignore: cast_nullable_to_non_nullable
|
||||||
as SnMetric,
|
as SnMetric,
|
||||||
preload: freezed == preload
|
|
||||||
? _self.preload
|
|
||||||
: preload // ignore: cast_nullable_to_non_nullable
|
|
||||||
as SnPostPreload?,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnPost
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnRealmCopyWith<$Res>? get realm {
|
||||||
|
if (_self.realm == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnRealmCopyWith<$Res>(_self.realm!, (value) {
|
||||||
|
return _then(_self.copyWith(realm: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a copy of SnPost
|
/// Create a copy of SnPost
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@@ -1044,6 +1095,20 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnPost
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPollCopyWith<$Res>? get poll {
|
||||||
|
if (_self.poll == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnPollCopyWith<$Res>(_self.poll!, (value) {
|
||||||
|
return _then(_self.copyWith(poll: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a copy of SnPost
|
/// Create a copy of SnPost
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@@ -1063,20 +1128,6 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
|||||||
return _then(_self.copyWith(metric: value));
|
return _then(_self.copyWith(metric: value));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a copy of SnPost
|
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
|
||||||
@override
|
|
||||||
@pragma('vm:prefer-inline')
|
|
||||||
$SnPostPreloadCopyWith<$Res>? get preload {
|
|
||||||
if (_self.preload == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $SnPostPreloadCopyWith<$Res>(_self.preload!, (value) {
|
|
||||||
return _then(_self.copyWith(preload: value));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -2465,6 +2516,7 @@ mixin _$SnPublisher {
|
|||||||
int get totalDownvote;
|
int get totalDownvote;
|
||||||
int? get realmId;
|
int? get realmId;
|
||||||
int get accountId;
|
int get accountId;
|
||||||
|
SnAccount? get account;
|
||||||
|
|
||||||
/// Create a copy of SnPublisher
|
/// Create a copy of SnPublisher
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -2501,7 +2553,8 @@ mixin _$SnPublisher {
|
|||||||
other.totalDownvote == totalDownvote) &&
|
other.totalDownvote == totalDownvote) &&
|
||||||
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
||||||
(identical(other.accountId, accountId) ||
|
(identical(other.accountId, accountId) ||
|
||||||
other.accountId == accountId));
|
other.accountId == accountId) &&
|
||||||
|
(identical(other.account, account) || other.account == account));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -2521,11 +2574,12 @@ mixin _$SnPublisher {
|
|||||||
totalUpvote,
|
totalUpvote,
|
||||||
totalDownvote,
|
totalDownvote,
|
||||||
realmId,
|
realmId,
|
||||||
accountId);
|
accountId,
|
||||||
|
account);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId)';
|
return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId, account: $account)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2549,7 +2603,10 @@ abstract mixin class $SnPublisherCopyWith<$Res> {
|
|||||||
int totalUpvote,
|
int totalUpvote,
|
||||||
int totalDownvote,
|
int totalDownvote,
|
||||||
int? realmId,
|
int? realmId,
|
||||||
int accountId});
|
int accountId,
|
||||||
|
SnAccount? account});
|
||||||
|
|
||||||
|
$SnAccountCopyWith<$Res>? get account;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -2578,6 +2635,7 @@ class _$SnPublisherCopyWithImpl<$Res> implements $SnPublisherCopyWith<$Res> {
|
|||||||
Object? totalDownvote = null,
|
Object? totalDownvote = null,
|
||||||
Object? realmId = freezed,
|
Object? realmId = freezed,
|
||||||
Object? accountId = null,
|
Object? accountId = null,
|
||||||
|
Object? account = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
id: null == id
|
id: null == id
|
||||||
@@ -2636,8 +2694,26 @@ class _$SnPublisherCopyWithImpl<$Res> implements $SnPublisherCopyWith<$Res> {
|
|||||||
? _self.accountId
|
? _self.accountId
|
||||||
: accountId // ignore: cast_nullable_to_non_nullable
|
: accountId // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
account: freezed == account
|
||||||
|
? _self.account
|
||||||
|
: account // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccount?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnPublisher
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnAccountCopyWith<$Res>? get account {
|
||||||
|
if (_self.account == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||||
|
return _then(_self.copyWith(account: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -2657,7 +2733,8 @@ class _SnPublisher implements SnPublisher {
|
|||||||
required this.totalUpvote,
|
required this.totalUpvote,
|
||||||
required this.totalDownvote,
|
required this.totalDownvote,
|
||||||
required this.realmId,
|
required this.realmId,
|
||||||
required this.accountId});
|
required this.accountId,
|
||||||
|
required this.account});
|
||||||
factory _SnPublisher.fromJson(Map<String, dynamic> json) =>
|
factory _SnPublisher.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnPublisherFromJson(json);
|
_$SnPublisherFromJson(json);
|
||||||
|
|
||||||
@@ -2689,6 +2766,8 @@ class _SnPublisher implements SnPublisher {
|
|||||||
final int? realmId;
|
final int? realmId;
|
||||||
@override
|
@override
|
||||||
final int accountId;
|
final int accountId;
|
||||||
|
@override
|
||||||
|
final SnAccount? account;
|
||||||
|
|
||||||
/// Create a copy of SnPublisher
|
/// Create a copy of SnPublisher
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -2730,7 +2809,8 @@ class _SnPublisher implements SnPublisher {
|
|||||||
other.totalDownvote == totalDownvote) &&
|
other.totalDownvote == totalDownvote) &&
|
||||||
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
||||||
(identical(other.accountId, accountId) ||
|
(identical(other.accountId, accountId) ||
|
||||||
other.accountId == accountId));
|
other.accountId == accountId) &&
|
||||||
|
(identical(other.account, account) || other.account == account));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -2750,11 +2830,12 @@ class _SnPublisher implements SnPublisher {
|
|||||||
totalUpvote,
|
totalUpvote,
|
||||||
totalDownvote,
|
totalDownvote,
|
||||||
realmId,
|
realmId,
|
||||||
accountId);
|
accountId,
|
||||||
|
account);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId)';
|
return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId, account: $account)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2780,7 +2861,11 @@ abstract mixin class _$SnPublisherCopyWith<$Res>
|
|||||||
int totalUpvote,
|
int totalUpvote,
|
||||||
int totalDownvote,
|
int totalDownvote,
|
||||||
int? realmId,
|
int? realmId,
|
||||||
int accountId});
|
int accountId,
|
||||||
|
SnAccount? account});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$SnAccountCopyWith<$Res>? get account;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -2809,6 +2894,7 @@ class __$SnPublisherCopyWithImpl<$Res> implements _$SnPublisherCopyWith<$Res> {
|
|||||||
Object? totalDownvote = null,
|
Object? totalDownvote = null,
|
||||||
Object? realmId = freezed,
|
Object? realmId = freezed,
|
||||||
Object? accountId = null,
|
Object? accountId = null,
|
||||||
|
Object? account = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_SnPublisher(
|
return _then(_SnPublisher(
|
||||||
id: null == id
|
id: null == id
|
||||||
@@ -2867,8 +2953,26 @@ class __$SnPublisherCopyWithImpl<$Res> implements _$SnPublisherCopyWith<$Res> {
|
|||||||
? _self.accountId
|
? _self.accountId
|
||||||
: accountId // ignore: cast_nullable_to_non_nullable
|
: accountId // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
account: freezed == account
|
||||||
|
? _self.account
|
||||||
|
: account // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnAccount?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnPublisher
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnAccountCopyWith<$Res>? get account {
|
||||||
|
if (_self.account == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||||
|
return _then(_self.copyWith(account: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
|
|||||||
replyId: (json['reply_id'] as num?)?.toInt(),
|
replyId: (json['reply_id'] as num?)?.toInt(),
|
||||||
repostId: (json['repost_id'] as num?)?.toInt(),
|
repostId: (json['repost_id'] as num?)?.toInt(),
|
||||||
realmId: (json['realm_id'] as num?)?.toInt(),
|
realmId: (json['realm_id'] as num?)?.toInt(),
|
||||||
|
realm: json['realm'] == null
|
||||||
|
? null
|
||||||
|
: SnRealm.fromJson(json['realm'] as Map<String, dynamic>),
|
||||||
replyTo: json['reply_to'] == null
|
replyTo: json['reply_to'] == null
|
||||||
? null
|
? null
|
||||||
: SnPost.fromJson(json['reply_to'] as Map<String, dynamic>),
|
: SnPost.fromJson(json['reply_to'] as Map<String, dynamic>),
|
||||||
@@ -68,12 +71,12 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
|
|||||||
(json['total_aggregated_views'] as num?)?.toInt() ?? 0,
|
(json['total_aggregated_views'] as num?)?.toInt() ?? 0,
|
||||||
publisherId: (json['publisher_id'] as num).toInt(),
|
publisherId: (json['publisher_id'] as num).toInt(),
|
||||||
pollId: (json['poll_id'] as num?)?.toInt(),
|
pollId: (json['poll_id'] as num?)?.toInt(),
|
||||||
|
poll: json['poll'] == null
|
||||||
|
? null
|
||||||
|
: SnPoll.fromJson(json['poll'] as Map<String, dynamic>),
|
||||||
publisher:
|
publisher:
|
||||||
SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
||||||
metric: SnMetric.fromJson(json['metric'] as Map<String, dynamic>),
|
metric: SnMetric.fromJson(json['metric'] as Map<String, dynamic>),
|
||||||
preload: json['preload'] == null
|
|
||||||
? null
|
|
||||||
: SnPostPreload.fromJson(json['preload'] as Map<String, dynamic>),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
|
Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
|
||||||
@@ -92,6 +95,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
|
|||||||
'reply_id': instance.replyId,
|
'reply_id': instance.replyId,
|
||||||
'repost_id': instance.repostId,
|
'repost_id': instance.repostId,
|
||||||
'realm_id': instance.realmId,
|
'realm_id': instance.realmId,
|
||||||
|
'realm': instance.realm?.toJson(),
|
||||||
'reply_to': instance.replyTo?.toJson(),
|
'reply_to': instance.replyTo?.toJson(),
|
||||||
'repost_to': instance.repostTo?.toJson(),
|
'repost_to': instance.repostTo?.toJson(),
|
||||||
'visible_users_list': instance.visibleUsersList,
|
'visible_users_list': instance.visibleUsersList,
|
||||||
@@ -109,9 +113,9 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
|
|||||||
'total_aggregated_views': instance.totalAggregatedViews,
|
'total_aggregated_views': instance.totalAggregatedViews,
|
||||||
'publisher_id': instance.publisherId,
|
'publisher_id': instance.publisherId,
|
||||||
'poll_id': instance.pollId,
|
'poll_id': instance.pollId,
|
||||||
|
'poll': instance.poll?.toJson(),
|
||||||
'publisher': instance.publisher.toJson(),
|
'publisher': instance.publisher.toJson(),
|
||||||
'metric': instance.metric.toJson(),
|
'metric': instance.metric.toJson(),
|
||||||
'preload': instance.preload?.toJson(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag(
|
_SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag(
|
||||||
@@ -241,6 +245,9 @@ _SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
|
|||||||
totalDownvote: (json['total_downvote'] as num).toInt(),
|
totalDownvote: (json['total_downvote'] as num).toInt(),
|
||||||
realmId: (json['realm_id'] as num?)?.toInt(),
|
realmId: (json['realm_id'] as num?)?.toInt(),
|
||||||
accountId: (json['account_id'] as num).toInt(),
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
|
account: json['account'] == null
|
||||||
|
? null
|
||||||
|
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
|
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
|
||||||
@@ -259,6 +266,7 @@ Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
|
|||||||
'total_downvote': instance.totalDownvote,
|
'total_downvote': instance.totalDownvote,
|
||||||
'realm_id': instance.realmId,
|
'realm_id': instance.realmId,
|
||||||
'account_id': instance.accountId,
|
'account_id': instance.accountId,
|
||||||
|
'account': instance.account?.toJson(),
|
||||||
};
|
};
|
||||||
|
|
||||||
_SnSubscription _$SnSubscriptionFromJson(Map<String, dynamic> json) =>
|
_SnSubscription _$SnSubscriptionFromJson(Map<String, dynamic> json) =>
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ const Map<String, ReactInfo> kTemplateReactions = {
|
|||||||
'party': ReactInfo(icon: '🎉', attitude: 1),
|
'party': ReactInfo(icon: '🎉', attitude: 1),
|
||||||
'joy': ReactInfo(icon: '🤣', attitude: 1),
|
'joy': ReactInfo(icon: '🤣', attitude: 1),
|
||||||
'pray': ReactInfo(icon: '🙏', attitude: 1),
|
'pray': ReactInfo(icon: '🙏', attitude: 1),
|
||||||
|
'heart': ReactInfo(icon: '❤️', attitude: 1),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ abstract class SnWallet with _$SnWallet {
|
|||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
required DateTime? deletedAt,
|
required DateTime? deletedAt,
|
||||||
required String balance,
|
required String balance,
|
||||||
|
required String goldenBalance,
|
||||||
required String password,
|
required String password,
|
||||||
required int accountId,
|
required int accountId,
|
||||||
}) = _SnWallet;
|
}) = _SnWallet;
|
||||||
@@ -27,6 +28,7 @@ abstract class SnTransaction with _$SnTransaction {
|
|||||||
required DateTime? deletedAt,
|
required DateTime? deletedAt,
|
||||||
required String remark,
|
required String remark,
|
||||||
required String amount,
|
required String amount,
|
||||||
|
required String currency,
|
||||||
required SnWallet? payer,
|
required SnWallet? payer,
|
||||||
required SnWallet? payee,
|
required SnWallet? payee,
|
||||||
required int? payerId,
|
required int? payerId,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ mixin _$SnWallet {
|
|||||||
DateTime get updatedAt;
|
DateTime get updatedAt;
|
||||||
DateTime? get deletedAt;
|
DateTime? get deletedAt;
|
||||||
String get balance;
|
String get balance;
|
||||||
|
String get goldenBalance;
|
||||||
String get password;
|
String get password;
|
||||||
int get accountId;
|
int get accountId;
|
||||||
|
|
||||||
@@ -46,6 +47,8 @@ mixin _$SnWallet {
|
|||||||
(identical(other.deletedAt, deletedAt) ||
|
(identical(other.deletedAt, deletedAt) ||
|
||||||
other.deletedAt == deletedAt) &&
|
other.deletedAt == deletedAt) &&
|
||||||
(identical(other.balance, balance) || other.balance == balance) &&
|
(identical(other.balance, balance) || other.balance == balance) &&
|
||||||
|
(identical(other.goldenBalance, goldenBalance) ||
|
||||||
|
other.goldenBalance == goldenBalance) &&
|
||||||
(identical(other.password, password) ||
|
(identical(other.password, password) ||
|
||||||
other.password == password) &&
|
other.password == password) &&
|
||||||
(identical(other.accountId, accountId) ||
|
(identical(other.accountId, accountId) ||
|
||||||
@@ -55,11 +58,11 @@ mixin _$SnWallet {
|
|||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||||
deletedAt, balance, password, accountId);
|
deletedAt, balance, goldenBalance, password, accountId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, password: $password, accountId: $accountId)';
|
return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, goldenBalance: $goldenBalance, password: $password, accountId: $accountId)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +77,7 @@ abstract mixin class $SnWalletCopyWith<$Res> {
|
|||||||
DateTime updatedAt,
|
DateTime updatedAt,
|
||||||
DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
String balance,
|
String balance,
|
||||||
|
String goldenBalance,
|
||||||
String password,
|
String password,
|
||||||
int accountId});
|
int accountId});
|
||||||
}
|
}
|
||||||
@@ -95,6 +99,7 @@ class _$SnWalletCopyWithImpl<$Res> implements $SnWalletCopyWith<$Res> {
|
|||||||
Object? updatedAt = null,
|
Object? updatedAt = null,
|
||||||
Object? deletedAt = freezed,
|
Object? deletedAt = freezed,
|
||||||
Object? balance = null,
|
Object? balance = null,
|
||||||
|
Object? goldenBalance = null,
|
||||||
Object? password = null,
|
Object? password = null,
|
||||||
Object? accountId = null,
|
Object? accountId = null,
|
||||||
}) {
|
}) {
|
||||||
@@ -119,6 +124,10 @@ class _$SnWalletCopyWithImpl<$Res> implements $SnWalletCopyWith<$Res> {
|
|||||||
? _self.balance
|
? _self.balance
|
||||||
: balance // ignore: cast_nullable_to_non_nullable
|
: balance // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
goldenBalance: null == goldenBalance
|
||||||
|
? _self.goldenBalance
|
||||||
|
: goldenBalance // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
password: null == password
|
password: null == password
|
||||||
? _self.password
|
? _self.password
|
||||||
: password // ignore: cast_nullable_to_non_nullable
|
: password // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -140,6 +149,7 @@ class _SnWallet implements SnWallet {
|
|||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
required this.deletedAt,
|
required this.deletedAt,
|
||||||
required this.balance,
|
required this.balance,
|
||||||
|
required this.goldenBalance,
|
||||||
required this.password,
|
required this.password,
|
||||||
required this.accountId});
|
required this.accountId});
|
||||||
factory _SnWallet.fromJson(Map<String, dynamic> json) =>
|
factory _SnWallet.fromJson(Map<String, dynamic> json) =>
|
||||||
@@ -156,6 +166,8 @@ class _SnWallet implements SnWallet {
|
|||||||
@override
|
@override
|
||||||
final String balance;
|
final String balance;
|
||||||
@override
|
@override
|
||||||
|
final String goldenBalance;
|
||||||
|
@override
|
||||||
final String password;
|
final String password;
|
||||||
@override
|
@override
|
||||||
final int accountId;
|
final int accountId;
|
||||||
@@ -188,6 +200,8 @@ class _SnWallet implements SnWallet {
|
|||||||
(identical(other.deletedAt, deletedAt) ||
|
(identical(other.deletedAt, deletedAt) ||
|
||||||
other.deletedAt == deletedAt) &&
|
other.deletedAt == deletedAt) &&
|
||||||
(identical(other.balance, balance) || other.balance == balance) &&
|
(identical(other.balance, balance) || other.balance == balance) &&
|
||||||
|
(identical(other.goldenBalance, goldenBalance) ||
|
||||||
|
other.goldenBalance == goldenBalance) &&
|
||||||
(identical(other.password, password) ||
|
(identical(other.password, password) ||
|
||||||
other.password == password) &&
|
other.password == password) &&
|
||||||
(identical(other.accountId, accountId) ||
|
(identical(other.accountId, accountId) ||
|
||||||
@@ -197,11 +211,11 @@ class _SnWallet implements SnWallet {
|
|||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||||
deletedAt, balance, password, accountId);
|
deletedAt, balance, goldenBalance, password, accountId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, password: $password, accountId: $accountId)';
|
return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, goldenBalance: $goldenBalance, password: $password, accountId: $accountId)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +232,7 @@ abstract mixin class _$SnWalletCopyWith<$Res>
|
|||||||
DateTime updatedAt,
|
DateTime updatedAt,
|
||||||
DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
String balance,
|
String balance,
|
||||||
|
String goldenBalance,
|
||||||
String password,
|
String password,
|
||||||
int accountId});
|
int accountId});
|
||||||
}
|
}
|
||||||
@@ -239,6 +254,7 @@ class __$SnWalletCopyWithImpl<$Res> implements _$SnWalletCopyWith<$Res> {
|
|||||||
Object? updatedAt = null,
|
Object? updatedAt = null,
|
||||||
Object? deletedAt = freezed,
|
Object? deletedAt = freezed,
|
||||||
Object? balance = null,
|
Object? balance = null,
|
||||||
|
Object? goldenBalance = null,
|
||||||
Object? password = null,
|
Object? password = null,
|
||||||
Object? accountId = null,
|
Object? accountId = null,
|
||||||
}) {
|
}) {
|
||||||
@@ -263,6 +279,10 @@ class __$SnWalletCopyWithImpl<$Res> implements _$SnWalletCopyWith<$Res> {
|
|||||||
? _self.balance
|
? _self.balance
|
||||||
: balance // ignore: cast_nullable_to_non_nullable
|
: balance // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
goldenBalance: null == goldenBalance
|
||||||
|
? _self.goldenBalance
|
||||||
|
: goldenBalance // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
password: null == password
|
password: null == password
|
||||||
? _self.password
|
? _self.password
|
||||||
: password // ignore: cast_nullable_to_non_nullable
|
: password // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -283,6 +303,7 @@ mixin _$SnTransaction {
|
|||||||
DateTime? get deletedAt;
|
DateTime? get deletedAt;
|
||||||
String get remark;
|
String get remark;
|
||||||
String get amount;
|
String get amount;
|
||||||
|
String get currency;
|
||||||
SnWallet? get payer;
|
SnWallet? get payer;
|
||||||
SnWallet? get payee;
|
SnWallet? get payee;
|
||||||
int? get payerId;
|
int? get payerId;
|
||||||
@@ -313,6 +334,8 @@ mixin _$SnTransaction {
|
|||||||
other.deletedAt == deletedAt) &&
|
other.deletedAt == deletedAt) &&
|
||||||
(identical(other.remark, remark) || other.remark == remark) &&
|
(identical(other.remark, remark) || other.remark == remark) &&
|
||||||
(identical(other.amount, amount) || other.amount == amount) &&
|
(identical(other.amount, amount) || other.amount == amount) &&
|
||||||
|
(identical(other.currency, currency) ||
|
||||||
|
other.currency == currency) &&
|
||||||
(identical(other.payer, payer) || other.payer == payer) &&
|
(identical(other.payer, payer) || other.payer == payer) &&
|
||||||
(identical(other.payee, payee) || other.payee == payee) &&
|
(identical(other.payee, payee) || other.payee == payee) &&
|
||||||
(identical(other.payerId, payerId) || other.payerId == payerId) &&
|
(identical(other.payerId, payerId) || other.payerId == payerId) &&
|
||||||
@@ -322,11 +345,11 @@ mixin _$SnTransaction {
|
|||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||||
deletedAt, remark, amount, payer, payee, payerId, payeeId);
|
deletedAt, remark, amount, currency, payer, payee, payerId, payeeId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)';
|
return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, currency: $currency, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,6 +366,7 @@ abstract mixin class $SnTransactionCopyWith<$Res> {
|
|||||||
DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
String remark,
|
String remark,
|
||||||
String amount,
|
String amount,
|
||||||
|
String currency,
|
||||||
SnWallet? payer,
|
SnWallet? payer,
|
||||||
SnWallet? payee,
|
SnWallet? payee,
|
||||||
int? payerId,
|
int? payerId,
|
||||||
@@ -371,6 +395,7 @@ class _$SnTransactionCopyWithImpl<$Res>
|
|||||||
Object? deletedAt = freezed,
|
Object? deletedAt = freezed,
|
||||||
Object? remark = null,
|
Object? remark = null,
|
||||||
Object? amount = null,
|
Object? amount = null,
|
||||||
|
Object? currency = null,
|
||||||
Object? payer = freezed,
|
Object? payer = freezed,
|
||||||
Object? payee = freezed,
|
Object? payee = freezed,
|
||||||
Object? payerId = freezed,
|
Object? payerId = freezed,
|
||||||
@@ -401,6 +426,10 @@ class _$SnTransactionCopyWithImpl<$Res>
|
|||||||
? _self.amount
|
? _self.amount
|
||||||
: amount // ignore: cast_nullable_to_non_nullable
|
: amount // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
currency: null == currency
|
||||||
|
? _self.currency
|
||||||
|
: currency // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
payer: freezed == payer
|
payer: freezed == payer
|
||||||
? _self.payer
|
? _self.payer
|
||||||
: payer // ignore: cast_nullable_to_non_nullable
|
: payer // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -459,6 +488,7 @@ class _SnTransaction implements SnTransaction {
|
|||||||
required this.deletedAt,
|
required this.deletedAt,
|
||||||
required this.remark,
|
required this.remark,
|
||||||
required this.amount,
|
required this.amount,
|
||||||
|
required this.currency,
|
||||||
required this.payer,
|
required this.payer,
|
||||||
required this.payee,
|
required this.payee,
|
||||||
required this.payerId,
|
required this.payerId,
|
||||||
@@ -479,6 +509,8 @@ class _SnTransaction implements SnTransaction {
|
|||||||
@override
|
@override
|
||||||
final String amount;
|
final String amount;
|
||||||
@override
|
@override
|
||||||
|
final String currency;
|
||||||
|
@override
|
||||||
final SnWallet? payer;
|
final SnWallet? payer;
|
||||||
@override
|
@override
|
||||||
final SnWallet? payee;
|
final SnWallet? payee;
|
||||||
@@ -516,6 +548,8 @@ class _SnTransaction implements SnTransaction {
|
|||||||
other.deletedAt == deletedAt) &&
|
other.deletedAt == deletedAt) &&
|
||||||
(identical(other.remark, remark) || other.remark == remark) &&
|
(identical(other.remark, remark) || other.remark == remark) &&
|
||||||
(identical(other.amount, amount) || other.amount == amount) &&
|
(identical(other.amount, amount) || other.amount == amount) &&
|
||||||
|
(identical(other.currency, currency) ||
|
||||||
|
other.currency == currency) &&
|
||||||
(identical(other.payer, payer) || other.payer == payer) &&
|
(identical(other.payer, payer) || other.payer == payer) &&
|
||||||
(identical(other.payee, payee) || other.payee == payee) &&
|
(identical(other.payee, payee) || other.payee == payee) &&
|
||||||
(identical(other.payerId, payerId) || other.payerId == payerId) &&
|
(identical(other.payerId, payerId) || other.payerId == payerId) &&
|
||||||
@@ -525,11 +559,11 @@ class _SnTransaction implements SnTransaction {
|
|||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||||
deletedAt, remark, amount, payer, payee, payerId, payeeId);
|
deletedAt, remark, amount, currency, payer, payee, payerId, payeeId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)';
|
return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, currency: $currency, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,6 +582,7 @@ abstract mixin class _$SnTransactionCopyWith<$Res>
|
|||||||
DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
String remark,
|
String remark,
|
||||||
String amount,
|
String amount,
|
||||||
|
String currency,
|
||||||
SnWallet? payer,
|
SnWallet? payer,
|
||||||
SnWallet? payee,
|
SnWallet? payee,
|
||||||
int? payerId,
|
int? payerId,
|
||||||
@@ -578,6 +613,7 @@ class __$SnTransactionCopyWithImpl<$Res>
|
|||||||
Object? deletedAt = freezed,
|
Object? deletedAt = freezed,
|
||||||
Object? remark = null,
|
Object? remark = null,
|
||||||
Object? amount = null,
|
Object? amount = null,
|
||||||
|
Object? currency = null,
|
||||||
Object? payer = freezed,
|
Object? payer = freezed,
|
||||||
Object? payee = freezed,
|
Object? payee = freezed,
|
||||||
Object? payerId = freezed,
|
Object? payerId = freezed,
|
||||||
@@ -608,6 +644,10 @@ class __$SnTransactionCopyWithImpl<$Res>
|
|||||||
? _self.amount
|
? _self.amount
|
||||||
: amount // ignore: cast_nullable_to_non_nullable
|
: amount // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
currency: null == currency
|
||||||
|
? _self.currency
|
||||||
|
: currency // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
payer: freezed == payer
|
payer: freezed == payer
|
||||||
? _self.payer
|
? _self.payer
|
||||||
: payer // ignore: cast_nullable_to_non_nullable
|
: payer // ignore: cast_nullable_to_non_nullable
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ _SnWallet _$SnWalletFromJson(Map<String, dynamic> json) => _SnWallet(
|
|||||||
? null
|
? null
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
balance: json['balance'] as String,
|
balance: json['balance'] as String,
|
||||||
|
goldenBalance: json['golden_balance'] as String,
|
||||||
password: json['password'] as String,
|
password: json['password'] as String,
|
||||||
accountId: (json['account_id'] as num).toInt(),
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
);
|
);
|
||||||
@@ -24,6 +25,7 @@ Map<String, dynamic> _$SnWalletToJson(_SnWallet instance) => <String, dynamic>{
|
|||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
'balance': instance.balance,
|
'balance': instance.balance,
|
||||||
|
'golden_balance': instance.goldenBalance,
|
||||||
'password': instance.password,
|
'password': instance.password,
|
||||||
'account_id': instance.accountId,
|
'account_id': instance.accountId,
|
||||||
};
|
};
|
||||||
@@ -38,6 +40,7 @@ _SnTransaction _$SnTransactionFromJson(Map<String, dynamic> json) =>
|
|||||||
: DateTime.parse(json['deleted_at'] as String),
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
remark: json['remark'] as String,
|
remark: json['remark'] as String,
|
||||||
amount: json['amount'] as String,
|
amount: json['amount'] as String,
|
||||||
|
currency: json['currency'] as String,
|
||||||
payer: json['payer'] == null
|
payer: json['payer'] == null
|
||||||
? null
|
? null
|
||||||
: SnWallet.fromJson(json['payer'] as Map<String, dynamic>),
|
: SnWallet.fromJson(json['payer'] as Map<String, dynamic>),
|
||||||
@@ -56,6 +59,7 @@ Map<String, dynamic> _$SnTransactionToJson(_SnTransaction instance) =>
|
|||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
'remark': instance.remark,
|
'remark': instance.remark,
|
||||||
'amount': instance.amount,
|
'amount': instance.amount,
|
||||||
|
'currency': instance.currency,
|
||||||
'payer': instance.payer?.toJson(),
|
'payer': instance.payer?.toJson(),
|
||||||
'payee': instance.payee?.toJson(),
|
'payee': instance.payee?.toJson(),
|
||||||
'payer_id': instance.payerId,
|
'payer_id': instance.payerId,
|
||||||
|
|||||||
@@ -54,11 +54,15 @@ class AccountImage extends StatelessWidget {
|
|||||||
))
|
))
|
||||||
.center(),
|
.center(),
|
||||||
)
|
)
|
||||||
: AutoResizeUniversalImage(
|
: UniversalImage(
|
||||||
sn.getAttachmentUrl(url),
|
sn.getAttachmentUrl(url),
|
||||||
filterQuality: filterQuality,
|
filterQuality: filterQuality,
|
||||||
key: Key('attachment-${content.hashCode}'),
|
key: Key('attachment-${content.hashCode}'),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
width: (radius != null ? radius! : 20) * 2,
|
||||||
|
height: (radius != null ? radius! : 20) * 2,
|
||||||
|
cacheWidth: (radius != null ? radius! : 20) * 2,
|
||||||
|
cacheHeight: (radius != null ? radius! : 20) * 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -22,142 +22,151 @@ class AccountPopoverCard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
return Column(
|
return SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (data.banner.isNotEmpty)
|
if (data.banner.isNotEmpty)
|
||||||
Container(
|
ClipRRect(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: AspectRatio(
|
child: Container(
|
||||||
aspectRatio: 16 / 7,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: AutoResizeUniversalImage(
|
child: AspectRatio(
|
||||||
sn.getAttachmentUrl(data.banner),
|
aspectRatio: 16 / 7,
|
||||||
fit: BoxFit.cover,
|
child: AutoResizeUniversalImage(
|
||||||
),
|
sn.getAttachmentUrl(data.banner),
|
||||||
),
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
// Top padding
|
|
||||||
Gap(16),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
AccountImage(
|
|
||||||
content: data.avatar,
|
|
||||||
radius: 20,
|
|
||||||
),
|
|
||||||
Gap(16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(data.nick).bold(),
|
|
||||||
Text('@${data.name}').fontSize(13).opacity(0.75),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'accountProfilePage',
|
|
||||||
pathParameters: {'name': data.name},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Symbols.chevron_right),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
|
||||||
),
|
|
||||||
const Gap(8)
|
|
||||||
],
|
|
||||||
).padding(horizontal: 16),
|
|
||||||
if (data.badges.isNotEmpty)
|
|
||||||
Wrap(
|
|
||||||
spacing: 4,
|
|
||||||
children: data.badges
|
|
||||||
.map(
|
|
||||||
(ele) => AccountBadge(badge: ele),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
).padding(horizontal: 24, bottom: 12, top: 12),
|
|
||||||
if (data.profile?.description.isNotEmpty ?? false)
|
|
||||||
Text(
|
|
||||||
data.profile?.description ?? '',
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
).padding(horizontal: 26, bottom: 8),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.star),
|
|
||||||
const Gap(8),
|
|
||||||
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
|
|
||||||
const Gap(8),
|
|
||||||
Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0))
|
|
||||||
.fontSize(11)
|
|
||||||
.opacity(0.5),
|
|
||||||
const Gap(8),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
constraints: const BoxConstraints(maxWidth: 160),
|
|
||||||
child: LinearProgressIndicator(
|
|
||||||
value: calcLevelUpProgress(data.profile?.experience ?? 0),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
).alignment(Alignment.centerLeft),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 24),
|
|
||||||
FutureBuilder(
|
|
||||||
future: sn.client.get('/cgi/id/users/${data.name}/status'),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final SnAccountStatusInfo? status = snapshot.hasData
|
|
||||||
? SnAccountStatusInfo.fromJson(snapshot.data!.data)
|
|
||||||
: null;
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
(status?.isDisturbable ?? true)
|
|
||||||
? Symbols.circle
|
|
||||||
: Symbols.do_not_disturb_on,
|
|
||||||
fill: (status?.isOnline ?? false) ? 1 : 0,
|
|
||||||
size: 16,
|
|
||||||
color: (status?.isOnline ?? false)
|
|
||||||
? (status?.isDisturbable ?? true)
|
|
||||||
? Colors.green
|
|
||||||
: Colors.red
|
|
||||||
: Colors.grey,
|
|
||||||
).padding(all: 4),
|
|
||||||
const Gap(8),
|
|
||||||
Text(
|
|
||||||
status != null
|
|
||||||
? (status.status?.label.isNotEmpty ?? false)
|
|
||||||
? status.status!.label
|
|
||||||
: status.isOnline
|
|
||||||
? 'accountStatusOnline'.tr()
|
|
||||||
: 'accountStatusOffline'.tr()
|
|
||||||
: 'loading'.tr(),
|
|
||||||
),
|
),
|
||||||
if (status != null &&
|
),
|
||||||
!status.isOnline &&
|
).padding(all: 16)
|
||||||
status.lastSeenAt != null)
|
else
|
||||||
|
const Gap(16),
|
||||||
|
// Top padding
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AccountImage(
|
||||||
|
content: data.avatar,
|
||||||
|
radius: 20,
|
||||||
|
),
|
||||||
|
Gap(16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(data.nick).bold(),
|
||||||
|
Text('@${data.name}').fontSize(13).opacity(0.75),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
GoRouter.of(context).pushReplacementNamed(
|
||||||
|
'accountProfilePage',
|
||||||
|
pathParameters: {'name': data.name},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.chevron_right),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
visualDensity:
|
||||||
|
const VisualDensity(horizontal: -4, vertical: -4),
|
||||||
|
),
|
||||||
|
const Gap(8)
|
||||||
|
],
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
if (data.badges.isNotEmpty)
|
||||||
|
Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
children: data.badges
|
||||||
|
.map(
|
||||||
|
(ele) => AccountBadge(badge: ele),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
).padding(horizontal: 24, bottom: 12, top: 12),
|
||||||
|
if (data.profile?.description.isNotEmpty ?? false)
|
||||||
|
Text(
|
||||||
|
data.profile?.description ?? '',
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
).padding(horizontal: 26, bottom: 8)
|
||||||
|
else
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.star),
|
||||||
|
const Gap(8),
|
||||||
|
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
|
||||||
|
const Gap(8),
|
||||||
|
Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0))
|
||||||
|
.fontSize(11)
|
||||||
|
.opacity(0.5),
|
||||||
|
const Gap(8),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
constraints: const BoxConstraints(maxWidth: 160),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: calcLevelUpProgress(data.profile?.experience ?? 0),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
).alignment(Alignment.centerLeft),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24),
|
||||||
|
FutureBuilder(
|
||||||
|
future: sn.client.get('/cgi/id/users/${data.name}/status'),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final SnAccountStatusInfo? status = snapshot.hasData
|
||||||
|
? SnAccountStatusInfo.fromJson(snapshot.data!.data)
|
||||||
|
: null;
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
(status?.isDisturbable ?? true)
|
||||||
|
? Symbols.circle
|
||||||
|
: Symbols.do_not_disturb_on,
|
||||||
|
fill: (status?.isOnline ?? false) ? 1 : 0,
|
||||||
|
size: 16,
|
||||||
|
color: (status?.isOnline ?? false)
|
||||||
|
? (status?.isDisturbable ?? true)
|
||||||
|
? Colors.green
|
||||||
|
: Colors.red
|
||||||
|
: Colors.grey,
|
||||||
|
).padding(all: 4),
|
||||||
|
const Gap(8),
|
||||||
Text(
|
Text(
|
||||||
'accountStatusLastSeen'.tr(args: [
|
status != null
|
||||||
status.lastSeenAt != null
|
? (status.status?.label.isNotEmpty ?? false)
|
||||||
? RelativeTime(context).format(
|
? status.status!.label
|
||||||
status.lastSeenAt!.toLocal(),
|
: status.isOnline
|
||||||
)
|
? 'accountStatusOnline'.tr()
|
||||||
: 'unknown',
|
: 'accountStatusOffline'.tr()
|
||||||
]),
|
: 'loading'.tr(),
|
||||||
).padding(left: 6).opacity(0.75),
|
),
|
||||||
],
|
if (status != null &&
|
||||||
).padding(horizontal: 24);
|
!status.isOnline &&
|
||||||
},
|
status.lastSeenAt != null)
|
||||||
),
|
Text(
|
||||||
// Bottom padding
|
'accountStatusLastSeen'.tr(args: [
|
||||||
const Gap(16),
|
status.lastSeenAt != null
|
||||||
],
|
? RelativeTime(context).format(
|
||||||
|
status.lastSeenAt!.toLocal(),
|
||||||
|
)
|
||||||
|
: 'unknown',
|
||||||
|
]),
|
||||||
|
).padding(left: 6).opacity(0.75),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Bottom padding
|
||||||
|
const Gap(64),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'dart:math' as math;
|
|||||||
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_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@@ -12,6 +13,7 @@ import 'package:media_kit/media_kit.dart';
|
|||||||
import 'package:media_kit_video/media_kit_video.dart';
|
import 'package:media_kit_video/media_kit_video.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/logger.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/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
@@ -23,6 +25,7 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
final String? heroTag;
|
final String? heroTag;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
final FilterQuality? filterQuality;
|
final FilterQuality? filterQuality;
|
||||||
|
final Function? onZoom;
|
||||||
|
|
||||||
const AttachmentItem({
|
const AttachmentItem({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -30,6 +33,7 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
required this.data,
|
required this.data,
|
||||||
required this.heroTag,
|
required this.heroTag,
|
||||||
this.filterQuality,
|
this.filterQuality,
|
||||||
|
this.onZoom,
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
@@ -92,7 +96,14 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildContent(context);
|
return GestureDetector(
|
||||||
|
child: _buildContent(context),
|
||||||
|
onTap: () {
|
||||||
|
if (data?.mimetype.startsWith('image') ?? false) {
|
||||||
|
onZoom?.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,20 +233,71 @@ class _AttachmentItemContentVideoState
|
|||||||
: sn.getAttachmentUrl(widget.data.compressed!.rid);
|
: sn.getAttachmentUrl(widget.data.compressed!.rid);
|
||||||
_videoPlayer = Player();
|
_videoPlayer = Player();
|
||||||
_videoController = VideoController(_videoPlayer!);
|
_videoController = VideoController(_videoPlayer!);
|
||||||
_videoPlayer!.open(Media(url), play: !widget.isAutoload);
|
|
||||||
|
String? uri;
|
||||||
|
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
|
||||||
|
if (inCacheInfo == null) {
|
||||||
|
logging.info('[MediaPlayer] Miss cache: $url');
|
||||||
|
final fileStream = DefaultCacheManager().getFileStream(
|
||||||
|
url,
|
||||||
|
withProgress: true,
|
||||||
|
);
|
||||||
|
await for (var fileInfo in fileStream) {
|
||||||
|
if (fileInfo is FileInfo) {
|
||||||
|
uri = fileInfo.file.path;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uri = inCacheInfo.file.path;
|
||||||
|
logging.info('[MediaPlayer] Hit cache: $url');
|
||||||
|
}
|
||||||
|
if (uri == null) {
|
||||||
|
if (mounted) {
|
||||||
|
context.showErrorDialog('attachmentFailedToLoadMedia'.tr());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_videoPlayer!.open(Media(uri), play: !widget.isAutoload);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleOriginal() {
|
void _toggleOriginal() async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (widget.data.compressedId == null) return;
|
if (widget.data.compressedId == null) return;
|
||||||
setState(() => _showOriginal = !_showOriginal);
|
setState(() => _showOriginal = !_showOriginal);
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final url = _showOriginal
|
||||||
|
? sn.getAttachmentUrl(widget.data.rid)
|
||||||
|
: sn.getAttachmentUrl(widget.data.compressed!.rid);
|
||||||
|
|
||||||
|
String? uri;
|
||||||
|
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
|
||||||
|
if (inCacheInfo == null) {
|
||||||
|
logging.info('[MediaPlayer] Miss cache: $url');
|
||||||
|
final fileStream = DefaultCacheManager().getFileStream(
|
||||||
|
url,
|
||||||
|
withProgress: true,
|
||||||
|
);
|
||||||
|
await for (var fileInfo in fileStream) {
|
||||||
|
if (fileInfo is FileInfo) {
|
||||||
|
uri = fileInfo.file.path;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uri = inCacheInfo.file.path;
|
||||||
|
logging.info('[MediaPlayer] Hit cache: $url');
|
||||||
|
}
|
||||||
|
if (uri == null) {
|
||||||
|
if (mounted) {
|
||||||
|
context.showErrorDialog('attachmentFailedToLoadMedia'.tr());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_videoPlayer?.open(
|
_videoPlayer?.open(
|
||||||
Media(
|
Media(uri),
|
||||||
_showOriginal
|
|
||||||
? sn.getAttachmentUrl(widget.data.rid)
|
|
||||||
: sn.getAttachmentUrl(widget.data.compressed!.rid),
|
|
||||||
),
|
|
||||||
play: true,
|
play: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -439,7 +501,33 @@ class _AttachmentItemContentAudioState
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final url = sn.getAttachmentUrl(widget.data.rid);
|
final url = sn.getAttachmentUrl(widget.data.rid);
|
||||||
_audioPlayer = Player();
|
_audioPlayer = Player();
|
||||||
await _audioPlayer!.open(Media(url), play: !widget.isAutoload);
|
|
||||||
|
String? uri;
|
||||||
|
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
|
||||||
|
if (inCacheInfo == null) {
|
||||||
|
logging.info('[MediaPlayer] Miss cache: $url');
|
||||||
|
final fileStream = DefaultCacheManager().getFileStream(
|
||||||
|
url,
|
||||||
|
withProgress: true,
|
||||||
|
);
|
||||||
|
await for (var fileInfo in fileStream) {
|
||||||
|
if (fileInfo is FileInfo) {
|
||||||
|
uri = fileInfo.file.path;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uri = inCacheInfo.file.path;
|
||||||
|
logging.info('[MediaPlayer] Hit cache: $url');
|
||||||
|
}
|
||||||
|
if (uri == null) {
|
||||||
|
if (mounted) {
|
||||||
|
context.showErrorDialog('attachmentFailedToLoadMedia'.tr());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _audioPlayer!.open(Media(uri), play: !widget.isAutoload);
|
||||||
_audioPlayer!.stream.playing.listen((v) => setState(() => _isPlaying = v));
|
_audioPlayer!.stream.playing.listen((v) => setState(() => _isPlaying = v));
|
||||||
_audioPlayer!.stream.position.listen((v) => setState(() => _position = v));
|
_audioPlayer!.stream.position.listen((v) => setState(() => _position = v));
|
||||||
_audioPlayer!.stream.duration.listen((v) => setState(() => _duration = v));
|
_audioPlayer!.stream.duration.listen((v) => setState(() => _duration = v));
|
||||||
@@ -567,6 +655,7 @@ class _AttachmentItemContentAudioState
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
constraints: const BoxConstraints(maxWidth: 320),
|
constraints: const BoxConstraints(maxWidth: 320),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|||||||
@@ -74,40 +74,35 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: widget.padding ?? EdgeInsets.zero,
|
padding: widget.padding ?? EdgeInsets.zero,
|
||||||
constraints: constraints,
|
constraints: constraints,
|
||||||
child: GestureDetector(
|
child: AspectRatio(
|
||||||
child: AspectRatio(
|
aspectRatio: singleAspectRatio,
|
||||||
aspectRatio: singleAspectRatio,
|
child: Container(
|
||||||
child: Container(
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: backgroundColor,
|
||||||
color: backgroundColor,
|
border: Border.fromBorderSide(borderSide),
|
||||||
border: Border.fromBorderSide(borderSide),
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
),
|
||||||
),
|
child: ClipRRect(
|
||||||
child: ClipRRect(
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
child: AttachmentItem(
|
||||||
child: AttachmentItem(
|
data: widget.data[0],
|
||||||
data: widget.data[0],
|
heroTag: heroTags[0],
|
||||||
heroTag: heroTags[0],
|
fit: widget.fit,
|
||||||
fit: widget.fit,
|
filterQuality: widget.filterQuality,
|
||||||
filterQuality: widget.filterQuality,
|
onZoom: () {
|
||||||
),
|
context.pushTransparentRoute(
|
||||||
|
AttachmentZoomView(
|
||||||
|
data: widget.data.where((ele) => ele != null).cast(),
|
||||||
|
initialIndex: 0,
|
||||||
|
heroTags: heroTags,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.7),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
context.pushTransparentRoute(
|
|
||||||
AttachmentZoomView(
|
|
||||||
data: widget.data.where((ele) => ele != null).cast(),
|
|
||||||
initialIndex: 0,
|
|
||||||
heroTags: heroTags,
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.black.withOpacity(0.7),
|
|
||||||
rootNavigator: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -133,33 +128,27 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
mainAxisSpacing: 4,
|
mainAxisSpacing: 4,
|
||||||
children: widget.data
|
children: widget.data
|
||||||
.mapIndexed(
|
.mapIndexed(
|
||||||
(idx, ele) => GestureDetector(
|
(idx, ele) => Container(
|
||||||
child: Container(
|
constraints: constraints,
|
||||||
constraints: constraints,
|
child: AttachmentItem(
|
||||||
child: AttachmentItem(
|
data: ele,
|
||||||
data: ele,
|
heroTag: heroTags[idx],
|
||||||
heroTag: heroTags[idx],
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
filterQuality: widget.filterQuality,
|
||||||
filterQuality: widget.filterQuality,
|
onZoom: () {
|
||||||
),
|
context.pushTransparentRoute(
|
||||||
|
AttachmentZoomView(
|
||||||
|
data: widget.data
|
||||||
|
.where((ele) => ele != null)
|
||||||
|
.cast(),
|
||||||
|
initialIndex: idx,
|
||||||
|
heroTags: heroTags,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.7),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
if (widget.data[idx]!.mediaType !=
|
|
||||||
SnMediaType.image) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
context.pushTransparentRoute(
|
|
||||||
AttachmentZoomView(
|
|
||||||
data: widget.data
|
|
||||||
.where((ele) => ele != null)
|
|
||||||
.cast(),
|
|
||||||
initialIndex: idx,
|
|
||||||
heroTags: heroTags,
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.black.withOpacity(0.7),
|
|
||||||
rootNavigator: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
@@ -181,17 +170,30 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: widget.data
|
children: widget.data
|
||||||
.mapIndexed(
|
.mapIndexed(
|
||||||
(idx, ele) => GestureDetector(
|
(idx, ele) => AspectRatio(
|
||||||
child: AspectRatio(
|
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
||||||
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
child: Container(
|
||||||
child: Container(
|
constraints: constraints,
|
||||||
constraints: constraints,
|
child: AttachmentItem(
|
||||||
child: AttachmentItem(
|
data: ele,
|
||||||
data: ele,
|
heroTag: heroTags[idx],
|
||||||
heroTag: heroTags[idx],
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
filterQuality: widget.filterQuality,
|
||||||
filterQuality: widget.filterQuality,
|
onZoom: () {
|
||||||
),
|
context.pushTransparentRoute(
|
||||||
|
AttachmentZoomView(
|
||||||
|
data: widget.data
|
||||||
|
.where((ele) =>
|
||||||
|
ele != null &&
|
||||||
|
ele.mediaType == SnMediaType.image)
|
||||||
|
.cast(),
|
||||||
|
initialIndex: idx,
|
||||||
|
heroTags: heroTags,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.7),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -222,52 +224,52 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio:
|
aspectRatio:
|
||||||
(widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
(widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||||
child: GestureDetector(
|
child: Stack(
|
||||||
onTap: () {
|
fit: StackFit.expand,
|
||||||
if (widget.data[idx]?.mediaType != SnMediaType.image)
|
children: [
|
||||||
return;
|
Container(
|
||||||
context.pushTransparentRoute(
|
decoration: BoxDecoration(
|
||||||
AttachmentZoomView(
|
color: backgroundColor,
|
||||||
data: widget.data
|
border: Border.all(
|
||||||
.where((ele) =>
|
width: 1,
|
||||||
ele != null &&
|
color: Theme.of(context).dividerColor,
|
||||||
ele.mediaType == SnMediaType.image)
|
|
||||||
.cast(),
|
|
||||||
initialIndex: idx,
|
|
||||||
heroTags: heroTags,
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.black.withOpacity(0.7),
|
|
||||||
rootNavigator: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: backgroundColor,
|
|
||||||
border:
|
|
||||||
Border(top: borderSide, bottom: borderSide),
|
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
),
|
||||||
child: AttachmentItem(
|
child: ClipRRect(
|
||||||
data: widget.data[idx],
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
heroTag: heroTags[idx],
|
child: AttachmentItem(
|
||||||
filterQuality: widget.filterQuality,
|
data: widget.data[idx],
|
||||||
),
|
heroTag: heroTags[idx],
|
||||||
|
filterQuality: widget.filterQuality,
|
||||||
|
onZoom: () {
|
||||||
|
context.pushTransparentRoute(
|
||||||
|
AttachmentZoomView(
|
||||||
|
data: widget.data
|
||||||
|
.where((ele) =>
|
||||||
|
ele != null &&
|
||||||
|
ele.mediaType ==
|
||||||
|
SnMediaType.image)
|
||||||
|
.cast(),
|
||||||
|
initialIndex: idx,
|
||||||
|
heroTags: heroTags,
|
||||||
|
),
|
||||||
|
backgroundColor:
|
||||||
|
Colors.black.withOpacity(0.7),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
),
|
||||||
right: 8,
|
Positioned(
|
||||||
bottom: 8,
|
right: 8,
|
||||||
child: Chip(
|
bottom: 8,
|
||||||
label:
|
child: Chip(
|
||||||
Text('${idx + 1}/${widget.data.length}')),
|
label: Text('${idx + 1}/${widget.data.length}'),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import 'package:photo_view/photo_view_gallery.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';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
@@ -65,7 +64,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
Future<void> _saveToAlbum(int idx) async {
|
Future<void> _saveToAlbum(int idx) async {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final item = widget.data.elementAt(idx);
|
final item = widget.data.elementAt(idx);
|
||||||
final url = sn.getAttachmentUrl(item.rid);
|
final url = sn.getAttachmentUrl(item.rid, preview: false);
|
||||||
|
|
||||||
if (kIsWeb || Platform.isLinux) {
|
if (kIsWeb || Platform.isLinux) {
|
||||||
await launchUrlString(url);
|
await launchUrlString(url);
|
||||||
@@ -182,7 +181,10 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
scaleState == PhotoViewScaleState.initial);
|
scaleState == PhotoViewScaleState.initial);
|
||||||
},
|
},
|
||||||
imageProvider: UniversalImage.provider(
|
imageProvider: UniversalImage.provider(
|
||||||
sn.getAttachmentUrl(widget.data.first.rid),
|
sn.getAttachmentUrl(
|
||||||
|
widget.data.first.rid,
|
||||||
|
preview: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -200,7 +202,10 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
widget.heroTags?.elementAt(idx) ?? uuid.v4();
|
widget.heroTags?.elementAt(idx) ?? uuid.v4();
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
imageProvider: UniversalImage.provider(
|
imageProvider: UniversalImage.provider(
|
||||||
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
|
sn.getAttachmentUrl(
|
||||||
|
widget.data.elementAt(idx).rid,
|
||||||
|
preview: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
||||||
@@ -368,7 +373,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
_showDetail = true;
|
_showDetail = true;
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _AttachmentZoomDetailPopup(
|
builder: (context) => AttachmentZoomDetailPopup(
|
||||||
data: widget.data.elementAt(_page),
|
data: widget.data.elementAt(_page),
|
||||||
),
|
),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
@@ -398,7 +403,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
_showDetail = true;
|
_showDetail = true;
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _AttachmentZoomDetailPopup(
|
builder: (context) => AttachmentZoomDetailPopup(
|
||||||
data: widget.data.elementAt(_page),
|
data: widget.data.elementAt(_page),
|
||||||
),
|
),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
@@ -411,15 +416,14 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AttachmentZoomDetailPopup extends StatelessWidget {
|
class AttachmentZoomDetailPopup extends StatelessWidget {
|
||||||
final SnAttachment data;
|
final SnAttachment data;
|
||||||
|
|
||||||
const _AttachmentZoomDetailPopup({required this.data});
|
const AttachmentZoomDetailPopup({required this.data});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final account = data.account!;
|
||||||
final account = ud.getFromCache(data.accountId);
|
|
||||||
|
|
||||||
const tableGap = TableRow(
|
const tableGap = TableRow(
|
||||||
children: [
|
children: [
|
||||||
@@ -461,12 +465,12 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (data.accountId > 0)
|
if (data.accountId > 0)
|
||||||
AccountImage(
|
AccountImage(
|
||||||
content: account?.avatar,
|
content: account.avatar,
|
||||||
radius: 8,
|
radius: 8,
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(data.accountId > 0
|
Text(data.accountId > 0
|
||||||
? account?.nick ?? 'unknown'.tr()
|
? account.nick
|
||||||
: 'unknown'.tr()),
|
: 'unknown'.tr()),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('#${data.accountId}',
|
Text('#${data.accountId}',
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import 'package:surface/widgets/account/account_image.dart';
|
|||||||
class NoContentWidget extends StatefulWidget {
|
class NoContentWidget extends StatefulWidget {
|
||||||
final SnAccount? userinfo;
|
final SnAccount? userinfo;
|
||||||
final bool isSpeaking;
|
final bool isSpeaking;
|
||||||
final bool isFixed;
|
final double? avatarSize;
|
||||||
|
|
||||||
const NoContentWidget({
|
const NoContentWidget({
|
||||||
super.key,
|
super.key,
|
||||||
this.userinfo,
|
this.userinfo,
|
||||||
this.isFixed = false,
|
this.avatarSize,
|
||||||
required this.isSpeaking,
|
required this.isSpeaking,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,41 +45,35 @@ class _NoContentWidgetState extends State<NoContentWidget>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final double radius = widget.isFixed
|
final double radius = widget.avatarSize ??
|
||||||
? 32
|
math.min(
|
||||||
: math.min(
|
MediaQuery.of(context).size.width * 0.1,
|
||||||
MediaQuery.of(context).size.width * 0.1,
|
MediaQuery.of(context).size.height * 0.1,
|
||||||
MediaQuery.of(context).size.height * 0.1,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return Container(
|
return Animate(
|
||||||
alignment: Alignment.center,
|
autoPlay: false,
|
||||||
child: Center(
|
controller: _animationController,
|
||||||
child: Animate(
|
effects: [
|
||||||
autoPlay: false,
|
CustomEffect(
|
||||||
controller: _animationController,
|
begin: widget.isSpeaking ? 2 : 0,
|
||||||
effects: [
|
end: 8,
|
||||||
CustomEffect(
|
curve: Curves.easeInOut,
|
||||||
begin: widget.isSpeaking ? 2 : 0,
|
duration: 1250.ms,
|
||||||
end: 8,
|
builder: (context, value, child) => Container(
|
||||||
curve: Curves.easeInOut,
|
decoration: BoxDecoration(
|
||||||
duration: 1250.ms,
|
borderRadius: BorderRadius.all(Radius.circular(radius + 8)),
|
||||||
builder: (context, value, child) => Container(
|
border: value > 0
|
||||||
decoration: BoxDecoration(
|
? Border.all(color: Colors.green, width: value)
|
||||||
borderRadius: BorderRadius.all(Radius.circular(radius + 8)),
|
: null,
|
||||||
border: value > 0
|
),
|
||||||
? Border.all(color: Colors.green, width: value)
|
child: child,
|
||||||
: null,
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
child: AccountImage(
|
|
||||||
content: widget.userinfo?.avatar,
|
|
||||||
radius: radius,
|
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
|
],
|
||||||
|
child: AccountImage(
|
||||||
|
content: widget.userinfo?.avatar,
|
||||||
|
radius: radius,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/chat/call/call_no_content.dart';
|
import 'package:surface/widgets/chat/call/call_no_content.dart';
|
||||||
@@ -11,23 +13,32 @@ import 'package:surface/widgets/chat/call/call_participant_menu.dart';
|
|||||||
import 'package:surface/widgets/chat/call/call_participant_stats.dart';
|
import 'package:surface/widgets/chat/call/call_participant_stats.dart';
|
||||||
|
|
||||||
abstract class ParticipantWidget extends StatefulWidget {
|
abstract class ParticipantWidget extends StatefulWidget {
|
||||||
static ParticipantWidget widgetFor(ParticipantTrack participantTrack,
|
static ParticipantWidget widgetFor(
|
||||||
{bool isFixed = false, bool showStatsLayer = false}) {
|
ParticipantTrack participantTrack, {
|
||||||
|
double? avatarSize,
|
||||||
|
EdgeInsets? padding,
|
||||||
|
bool showStatsLayer = false,
|
||||||
|
bool isList = false,
|
||||||
|
}) {
|
||||||
if (participantTrack.participant is LocalParticipant) {
|
if (participantTrack.participant is LocalParticipant) {
|
||||||
return LocalParticipantWidget(
|
return LocalParticipantWidget(
|
||||||
participantTrack.participant as LocalParticipant,
|
participantTrack.participant as LocalParticipant,
|
||||||
participantTrack.videoTrack,
|
participantTrack.videoTrack,
|
||||||
isFixed,
|
avatarSize,
|
||||||
participantTrack.isScreenShare,
|
participantTrack.isScreenShare,
|
||||||
showStatsLayer,
|
showStatsLayer,
|
||||||
|
isList,
|
||||||
|
padding,
|
||||||
);
|
);
|
||||||
} else if (participantTrack.participant is RemoteParticipant) {
|
} else if (participantTrack.participant is RemoteParticipant) {
|
||||||
return RemoteParticipantWidget(
|
return RemoteParticipantWidget(
|
||||||
participantTrack.participant as RemoteParticipant,
|
participantTrack.participant as RemoteParticipant,
|
||||||
participantTrack.videoTrack,
|
participantTrack.videoTrack,
|
||||||
isFixed,
|
avatarSize,
|
||||||
participantTrack.isScreenShare,
|
participantTrack.isScreenShare,
|
||||||
showStatsLayer,
|
showStatsLayer,
|
||||||
|
isList,
|
||||||
|
padding,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw UnimplementedError('Unknown participant type');
|
throw UnimplementedError('Unknown participant type');
|
||||||
@@ -36,8 +47,10 @@ abstract class ParticipantWidget extends StatefulWidget {
|
|||||||
abstract final Participant participant;
|
abstract final Participant participant;
|
||||||
abstract final VideoTrack? videoTrack;
|
abstract final VideoTrack? videoTrack;
|
||||||
abstract final bool isScreenShare;
|
abstract final bool isScreenShare;
|
||||||
abstract final bool isFixed;
|
abstract final double? avatarSize;
|
||||||
abstract final bool showStatsLayer;
|
abstract final bool showStatsLayer;
|
||||||
|
abstract final bool isList;
|
||||||
|
abstract final EdgeInsets? padding;
|
||||||
final VideoQuality quality;
|
final VideoQuality quality;
|
||||||
|
|
||||||
const ParticipantWidget({
|
const ParticipantWidget({
|
||||||
@@ -52,18 +65,24 @@ class LocalParticipantWidget extends ParticipantWidget {
|
|||||||
@override
|
@override
|
||||||
final VideoTrack? videoTrack;
|
final VideoTrack? videoTrack;
|
||||||
@override
|
@override
|
||||||
final bool isFixed;
|
final double? avatarSize;
|
||||||
@override
|
@override
|
||||||
final bool isScreenShare;
|
final bool isScreenShare;
|
||||||
@override
|
@override
|
||||||
final bool showStatsLayer;
|
final bool showStatsLayer;
|
||||||
|
@override
|
||||||
|
final bool isList;
|
||||||
|
@override
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const LocalParticipantWidget(
|
const LocalParticipantWidget(
|
||||||
this.participant,
|
this.participant,
|
||||||
this.videoTrack,
|
this.videoTrack,
|
||||||
this.isFixed,
|
this.avatarSize,
|
||||||
this.isScreenShare,
|
this.isScreenShare,
|
||||||
this.showStatsLayer, {
|
this.showStatsLayer,
|
||||||
|
this.isList,
|
||||||
|
this.padding, {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,18 +96,24 @@ class RemoteParticipantWidget extends ParticipantWidget {
|
|||||||
@override
|
@override
|
||||||
final VideoTrack? videoTrack;
|
final VideoTrack? videoTrack;
|
||||||
@override
|
@override
|
||||||
final bool isFixed;
|
final double? avatarSize;
|
||||||
@override
|
@override
|
||||||
final bool isScreenShare;
|
final bool isScreenShare;
|
||||||
@override
|
@override
|
||||||
final bool showStatsLayer;
|
final bool showStatsLayer;
|
||||||
|
@override
|
||||||
|
final bool isList;
|
||||||
|
@override
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const RemoteParticipantWidget(
|
const RemoteParticipantWidget(
|
||||||
this.participant,
|
this.participant,
|
||||||
this.videoTrack,
|
this.videoTrack,
|
||||||
this.isFixed,
|
this.avatarSize,
|
||||||
this.isScreenShare,
|
this.isScreenShare,
|
||||||
this.showStatsLayer, {
|
this.showStatsLayer,
|
||||||
|
this.isList,
|
||||||
|
this.padding, {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -136,19 +161,82 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext ctx) {
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.isList) {
|
||||||
|
return Padding(
|
||||||
|
padding: widget.padding ?? EdgeInsets.zero,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: (widget.avatarSize ?? 32) * 2,
|
||||||
|
height: (widget.avatarSize ?? 32) * 2,
|
||||||
|
child: Center(
|
||||||
|
child: NoContentWidget(
|
||||||
|
userinfo: _userinfoMetadata,
|
||||||
|
avatarSize: widget.avatarSize,
|
||||||
|
isSpeaking: widget.participant.isSpeaking,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: SizedBox(
|
||||||
|
height: (widget.avatarSize ?? 32) * 2,
|
||||||
|
child: ParticipantInfoWidget(
|
||||||
|
isList: true,
|
||||||
|
title: widget.participant.name.isNotEmpty
|
||||||
|
? widget.participant.name
|
||||||
|
: widget.participant.identity,
|
||||||
|
audioAvailable: _firstAudioPublication?.muted == false &&
|
||||||
|
_firstAudioPublication?.subscribed == true,
|
||||||
|
connectionQuality: widget.participant.connectionQuality,
|
||||||
|
isScreenShare: widget.isScreenShare,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_activeVideoTrack != null && !_activeVideoTrack!.muted)
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: Material(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer
|
||||||
|
.withOpacity(0.75),
|
||||||
|
child: VideoTrackRenderer(
|
||||||
|
_activeVideoTrack!,
|
||||||
|
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(top: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
_activeVideoTrack != null && !_activeVideoTrack!.muted
|
if (_activeVideoTrack != null && !_activeVideoTrack!.muted)
|
||||||
? VideoTrackRenderer(
|
VideoTrackRenderer(
|
||||||
_activeVideoTrack!,
|
_activeVideoTrack!,
|
||||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||||
)
|
)
|
||||||
: NoContentWidget(
|
else
|
||||||
userinfo: _userinfoMetadata,
|
Center(
|
||||||
isFixed: widget.isFixed,
|
child: NoContentWidget(
|
||||||
isSpeaking: widget.participant.isSpeaking,
|
userinfo: _userinfoMetadata,
|
||||||
),
|
avatarSize: widget.avatarSize,
|
||||||
|
isSpeaking: widget.participant.isSpeaking,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (widget.showStatsLayer)
|
if (widget.showStatsLayer)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 30,
|
top: 30,
|
||||||
@@ -199,44 +287,51 @@ class _RemoteParticipantWidgetState
|
|||||||
}
|
}
|
||||||
|
|
||||||
class InteractiveParticipantWidget extends StatelessWidget {
|
class InteractiveParticipantWidget extends StatelessWidget {
|
||||||
final double? width;
|
final double? avatarSize;
|
||||||
final double? height;
|
final bool isList;
|
||||||
final Color? color;
|
|
||||||
final bool isFixedAvatar;
|
|
||||||
final ParticipantTrack participant;
|
final ParticipantTrack participant;
|
||||||
final Function() onTap;
|
final Function? onTap;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const InteractiveParticipantWidget({
|
const InteractiveParticipantWidget({
|
||||||
super.key,
|
super.key,
|
||||||
this.width,
|
this.avatarSize,
|
||||||
this.height,
|
this.isList = false,
|
||||||
this.color,
|
this.padding,
|
||||||
this.isFixedAvatar = false,
|
|
||||||
required this.participant,
|
required this.participant,
|
||||||
required this.onTap,
|
this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return Material(
|
||||||
child: Container(
|
color: Colors.transparent,
|
||||||
width: width,
|
child: InkWell(
|
||||||
height: height,
|
onTap: onTap != null
|
||||||
color: color,
|
? () {
|
||||||
child: ParticipantWidget.widgetFor(participant, isFixed: isFixedAvatar),
|
onTap?.call();
|
||||||
),
|
}
|
||||||
onTap: () => onTap(),
|
: null,
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
if (participant.participant is LocalParticipant) return;
|
if (participant.participant is LocalParticipant) return;
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ParticipantMenu(
|
builder: (context) => ParticipantMenu(
|
||||||
participant: participant.participant as RemoteParticipant,
|
participant: participant.participant as RemoteParticipant,
|
||||||
videoTrack: participant.videoTrack,
|
videoTrack: participant.videoTrack,
|
||||||
isScreenShare: participant.isScreenShare,
|
isScreenShare: participant.isScreenShare,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
child: ParticipantWidget.widgetFor(
|
||||||
|
participant,
|
||||||
|
avatarSize: avatarSize,
|
||||||
|
isList: isList,
|
||||||
|
padding: padding,
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class ParticipantInfoWidget extends StatelessWidget {
|
|||||||
final bool audioAvailable;
|
final bool audioAvailable;
|
||||||
final ConnectionQuality connectionQuality;
|
final ConnectionQuality connectionQuality;
|
||||||
final bool isScreenShare;
|
final bool isScreenShare;
|
||||||
|
final bool isList;
|
||||||
|
|
||||||
const ParticipantInfoWidget({
|
const ParticipantInfoWidget({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -16,64 +17,124 @@ class ParticipantInfoWidget extends StatelessWidget {
|
|||||||
this.audioAvailable = true,
|
this.audioAvailable = true,
|
||||||
this.connectionQuality = ConnectionQuality.unknown,
|
this.connectionQuality = ConnectionQuality.unknown,
|
||||||
this.isScreenShare = false,
|
this.isScreenShare = false,
|
||||||
|
this.isList = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Container(
|
Widget build(BuildContext context) {
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
if (isList) {
|
||||||
padding: const EdgeInsets.symmetric(
|
return Column(
|
||||||
vertical: 7,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
horizontal: 10,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
children: [
|
||||||
child: Row(
|
if (title != null)
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
title!,
|
||||||
children: [
|
overflow: TextOverflow.ellipsis,
|
||||||
if (title != null)
|
style: const TextStyle(
|
||||||
Flexible(
|
color: Colors.white,
|
||||||
child: Text(
|
fontWeight: FontWeight.bold,
|
||||||
title!,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Gap(5),
|
).padding(left: 2),
|
||||||
isScreenShare
|
Row(
|
||||||
? const Icon(
|
children: [
|
||||||
Symbols.monitor,
|
isScreenShare
|
||||||
|
? const Icon(
|
||||||
|
Symbols.monitor,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
audioAvailable ? Symbols.mic : Symbols.mic_off,
|
||||||
|
color: audioAvailable ? Colors.white : Colors.red,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const Gap(3),
|
||||||
|
if (connectionQuality != ConnectionQuality.unknown)
|
||||||
|
Icon(
|
||||||
|
{
|
||||||
|
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
|
||||||
|
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
|
||||||
|
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
|
||||||
|
}[connectionQuality],
|
||||||
|
color: {
|
||||||
|
ConnectionQuality.excellent: Colors.green,
|
||||||
|
ConnectionQuality.good: Colors.orange,
|
||||||
|
ConnectionQuality.poor: Colors.red,
|
||||||
|
}[connectionQuality],
|
||||||
|
size: 16,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 16,
|
strokeWidth: 2,
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
audioAvailable ? Symbols.mic : Symbols.mic_off,
|
|
||||||
color: audioAvailable ? Colors.white : Colors.red,
|
|
||||||
size: 16,
|
|
||||||
),
|
),
|
||||||
const Gap(3),
|
).padding(all: 3),
|
||||||
if (connectionQuality != ConnectionQuality.unknown)
|
],
|
||||||
Icon(
|
)
|
||||||
{
|
],
|
||||||
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
|
|
||||||
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
|
|
||||||
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
|
|
||||||
}[connectionQuality],
|
|
||||||
color: {
|
|
||||||
ConnectionQuality.excellent: Colors.green,
|
|
||||||
ConnectionQuality.good: Colors.orange,
|
|
||||||
ConnectionQuality.poor: Colors.red,
|
|
||||||
}[connectionQuality],
|
|
||||||
size: 16,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const SizedBox(
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: Colors.white,
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
).padding(all: 3),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 7,
|
||||||
|
horizontal: 10,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (title != null)
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
title!,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(5),
|
||||||
|
isScreenShare
|
||||||
|
? const Icon(
|
||||||
|
Symbols.monitor,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
audioAvailable ? Symbols.mic : Symbols.mic_off,
|
||||||
|
color: audioAvailable ? Colors.white : Colors.red,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const Gap(3),
|
||||||
|
if (connectionQuality != ConnectionQuality.unknown)
|
||||||
|
Icon(
|
||||||
|
{
|
||||||
|
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
|
||||||
|
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
|
||||||
|
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
|
||||||
|
}[connectionQuality],
|
||||||
|
color: {
|
||||||
|
ConnectionQuality.excellent: Colors.green,
|
||||||
|
ConnectionQuality.good: Colors.orange,
|
||||||
|
ConnectionQuality.poor: Colors.red,
|
||||||
|
}[connectionQuality],
|
||||||
|
size: 16,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
).padding(all: 3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
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:flutter_context_menu/flutter_context_menu.dart';
|
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:popover/popover.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
@@ -120,20 +117,9 @@ class ChatMessage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
showPopover(
|
showModalBottomSheet(
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.surface,
|
|
||||||
context: context,
|
context: context,
|
||||||
transition: PopoverTransition.other,
|
builder: (context) => AccountPopoverCard(data: user),
|
||||||
bodyBuilder: (context) => SizedBox(
|
|
||||||
width: math.min(
|
|
||||||
400, MediaQuery.of(context).size.width - 10),
|
|
||||||
child: AccountPopoverCard(data: user),
|
|
||||||
),
|
|
||||||
direction: PopoverDirection.bottom,
|
|
||||||
arrowHeight: 5,
|
|
||||||
arrowWidth: 15,
|
|
||||||
arrowDxOffset: -190,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
final ws = context.watch<WebSocketProvider>();
|
final ws = context.watch<WebSocketProvider>();
|
||||||
final cfg = context.watch<ConfigProvider>();
|
final cfg = context.watch<ConfigProvider>();
|
||||||
|
|
||||||
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
|
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : 80.0;
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: ws,
|
listenable: ws,
|
||||||
@@ -30,7 +30,8 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
child: ua.isAuthorized
|
child: ua.isAuthorized
|
||||||
? Row(
|
? Row(
|
||||||
@@ -39,16 +40,29 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (ws.isBusy)
|
if (ws.isBusy)
|
||||||
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
Text(
|
||||||
|
'serverConnecting',
|
||||||
|
).tr().textColor(Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSecondaryContainer)
|
||||||
else if (!ws.isConnected)
|
else if (!ws.isConnected)
|
||||||
Text('serverDisconnected')
|
Text(
|
||||||
.tr()
|
'serverDisconnected',
|
||||||
.textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
).tr().textColor(Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSecondaryContainer)
|
||||||
else
|
else
|
||||||
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
Text(
|
||||||
|
'serverConnected',
|
||||||
|
).tr().textColor(Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSecondaryContainer),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (ws.isBusy)
|
if (ws.isBusy)
|
||||||
const CircularProgressIndicator(strokeWidth: 2.5)
|
const CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
)
|
||||||
.width(12)
|
.width(12)
|
||||||
.height(12)
|
.height(12)
|
||||||
.padding(horizontal: 4, right: 4)
|
.padding(horizontal: 4, right: 4)
|
||||||
@@ -59,10 +73,9 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
).padding(horizontal: 8, vertical: 4)
|
).padding(horizontal: 8, vertical: 4)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
).opacity(show ? 1 : 0, animate: true).animate(
|
)
|
||||||
const Duration(milliseconds: 300),
|
.opacity(show ? 1 : 0, animate: true)
|
||||||
Curves.easeInOut,
|
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
|
||||||
),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (!ws.isConnected && !ws.isBusy) {
|
if (!ws.isConnected && !ws.isBusy) {
|
||||||
ws.connect();
|
ws.connect();
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ class ContextMenuArea extends StatelessWidget {
|
|||||||
final cfg = context.read<ConfigProvider>();
|
final cfg = context.read<ConfigProvider>();
|
||||||
if (!cfg.drawerIsCollapsed) {
|
if (!cfg.drawerIsCollapsed) {
|
||||||
// Leave padding for side navigation
|
// Leave padding for side navigation
|
||||||
mousePosition = cfg.drawerIsExpanded
|
mousePosition = mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
|
||||||
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
|
||||||
: mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@@ -40,7 +38,8 @@ class ContextMenuArea extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showMenu(BuildContext context, Offset mousePosition) async {
|
void _showMenu(BuildContext context, Offset mousePosition) async {
|
||||||
final menu = contextMenu.copyWith(position: contextMenu.position ?? mousePosition);
|
final menu =
|
||||||
|
contextMenu.copyWith(position: contextMenu.position ?? mousePosition);
|
||||||
final value = await showContextMenu(context, contextMenu: menu);
|
final value = await showContextMenu(context, contextMenu: menu);
|
||||||
onItemSelected?.call(value);
|
onItemSelected?.call(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,13 @@ class _LinkPreviewWidgetState extends State<LinkPreviewWidget> {
|
|||||||
|
|
||||||
Future<void> _getLinkMeta() async {
|
Future<void> _getLinkMeta() async {
|
||||||
final linkRegex = RegExp(r'https?:\/\/[^\s/$.?#].[^\s]*');
|
final linkRegex = RegExp(r'https?:\/\/[^\s/$.?#].[^\s]*');
|
||||||
final links = linkRegex.allMatches(widget.text).map((e) => e.group(0)).toSet();
|
final links =
|
||||||
|
linkRegex.allMatches(widget.text).map((e) => e.group(0)).toSet();
|
||||||
|
|
||||||
final lp = context.read<SnLinkPreviewProvider>();
|
final lp = context.read<SnLinkPreviewProvider>();
|
||||||
|
|
||||||
final List<Future<SnLinkMeta?>> futures = links.where((e) => e != null).map((e) => lp.getLinkMeta(e!)).toList();
|
final List<Future<SnLinkMeta?>> futures =
|
||||||
|
links.where((e) => e != null).map((e) => lp.getLinkMeta(e!)).toList();
|
||||||
final results = await Future.wait(futures);
|
final results = await Future.wait(futures);
|
||||||
|
|
||||||
_links.addAll(results.where((e) => e != null).map((e) => e!).toList());
|
_links.addAll(results.where((e) => e != null).map((e) => e!).toList());
|
||||||
@@ -66,7 +68,9 @@ class _LinkPreviewEntry extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE) ? double.infinity : 480,
|
maxWidth: ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
||||||
|
? double.infinity
|
||||||
|
: 480,
|
||||||
),
|
),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: Card(
|
child: Card(
|
||||||
@@ -74,16 +78,25 @@ class _LinkPreviewEntry extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (meta.image != null)
|
if (meta.image != null)
|
||||||
Container(
|
ClipRRect(
|
||||||
margin: const EdgeInsets.only(bottom: 4),
|
borderRadius: BorderRadius.only(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
topLeft: Radius.circular(8),
|
||||||
child: AspectRatio(
|
topRight: Radius.circular(8),
|
||||||
aspectRatio: 16 / 9,
|
),
|
||||||
child: ClipRRect(
|
child: Container(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
margin: const EdgeInsets.only(bottom: 4),
|
||||||
child: AutoResizeUniversalImage(
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!,
|
child: AspectRatio(
|
||||||
fit: BoxFit.contain,
|
aspectRatio: 16 / 9,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius:
|
||||||
|
const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: AutoResizeUniversalImage(
|
||||||
|
meta.image!.startsWith('//')
|
||||||
|
? 'https:${meta.image}'
|
||||||
|
: meta.image!,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -98,7 +111,8 @@ class _LinkPreviewEntry extends StatelessWidget {
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
child: meta.icon!.endsWith('.svg')
|
child: meta.icon!.endsWith('.svg')
|
||||||
? SvgPicture.network(meta.icon!, width: 36, height: 36)
|
? SvgPicture.network(meta.icon!,
|
||||||
|
width: 36, height: 36)
|
||||||
: UniversalImage(
|
: UniversalImage(
|
||||||
meta.icon!,
|
meta.icon!,
|
||||||
noErrorWidget: true,
|
noErrorWidget: true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
|
import 'package:collection/collection.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';
|
||||||
@@ -10,14 +10,9 @@ 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/channel.dart';
|
|
||||||
import 'package:surface/providers/config.dart';
|
|
||||||
import 'package:surface/providers/navigation.dart';
|
import 'package:surface/providers/navigation.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
|
||||||
import 'package:surface/providers/sn_realm.dart';
|
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
|
||||||
import 'package:surface/widgets/version_label.dart';
|
import 'package:surface/widgets/version_label.dart';
|
||||||
|
|
||||||
class AppNavigationDrawer extends StatefulWidget {
|
class AppNavigationDrawer extends StatefulWidget {
|
||||||
@@ -44,25 +39,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
final nav = context.watch<NavigationProvider>();
|
final nav = context.watch<NavigationProvider>();
|
||||||
final cfg = context.watch<ConfigProvider>();
|
|
||||||
|
|
||||||
final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
|
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: nav,
|
listenable: nav,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return Drawer(
|
return Drawer(
|
||||||
elevation: widget.elevation,
|
elevation: widget.elevation,
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (!kIsWeb &&
|
if (!kIsWeb &&
|
||||||
(Platform.isWindows ||
|
(Platform.isWindows || Platform.isLinux || Platform.isMacOS))
|
||||||
Platform.isLinux ||
|
|
||||||
Platform.isMacOS) &&
|
|
||||||
!cfg.drawerIsExpanded)
|
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
@@ -75,61 +63,66 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
|||||||
child: WindowTitleBarBox(),
|
child: WindowTitleBarBox(),
|
||||||
),
|
),
|
||||||
Gap(MediaQuery.of(context).padding.top),
|
Gap(MediaQuery.of(context).padding.top),
|
||||||
Expanded(
|
Column(
|
||||||
child: _DrawerContentList(),
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Solar Network').bold(),
|
||||||
|
AppVersionLabel(),
|
||||||
|
],
|
||||||
|
).padding(
|
||||||
|
horizontal: 32,
|
||||||
|
vertical: 12,
|
||||||
),
|
),
|
||||||
if (cfg.hideBottomNav)
|
Expanded(
|
||||||
Row(
|
child: ListView(
|
||||||
spacing: 8,
|
padding: EdgeInsets.zero,
|
||||||
children: nav.destinations.where((ele) => ele.isPinned).map(
|
children: [
|
||||||
(ele) {
|
...nav.destinations.mapIndexed((idx, ele) {
|
||||||
return Expanded(
|
return ListTile(
|
||||||
child: Tooltip(
|
leading: ele.icon,
|
||||||
message: ele.label.tr(),
|
title: Text(ele.label).tr(),
|
||||||
child: IconButton.filledTonal(
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
icon: ele.icon,
|
selected: nav.currentIndex == idx,
|
||||||
color: Theme.of(context)
|
onTap: () {
|
||||||
.colorScheme
|
GoRouter.of(context).pushNamed(ele.screen);
|
||||||
.onPrimaryContainer,
|
nav.setIndex(idx);
|
||||||
onPressed: () {
|
Scaffold.of(context).closeDrawer();
|
||||||
GoRouter.of(context).goNamed(ele.screen);
|
},
|
||||||
Scaffold.of(context).closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
})
|
||||||
).toList(),
|
],
|
||||||
).padding(horizontal: 16),
|
),
|
||||||
|
),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: AccountImage(content: ua.user?.avatar),
|
leading: AccountImage(
|
||||||
title: Text(ua.user?.nick ?? 'unknown'.tr()).fontSize(15),
|
content: ua.user?.avatar,
|
||||||
subtitle:
|
backgroundColor: Colors.transparent,
|
||||||
Text('@${ua.user?.name ?? 'unknown'.tr()}').fontSize(13),
|
fallbackWidget:
|
||||||
|
ua.isAuthorized ? null : const Icon(Symbols.login),
|
||||||
|
),
|
||||||
|
title: ua.isAuthorized
|
||||||
|
? Text(ua.user?.nick ?? 'unknown'.tr()).fontSize(15)
|
||||||
|
: Text('screenAuthLogin').tr(),
|
||||||
|
subtitle: ua.isAuthorized
|
||||||
|
? Text('@${ua.user?.name ?? 'unknown'.tr()}').fontSize(13)
|
||||||
|
: Text('navBottomUnauthorizedCaption').fontSize(13).tr(),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
if (ua.isAuthorized)
|
||||||
icon: const Icon(Symbols.notifications, fill: 1),
|
IconButton(
|
||||||
padding: EdgeInsets.zero,
|
icon: const Icon(Symbols.notifications, fill: 1),
|
||||||
visualDensity: VisualDensity.compact,
|
padding: EdgeInsets.zero,
|
||||||
onPressed: () {
|
visualDensity: VisualDensity.compact,
|
||||||
GoRouter.of(context).pushNamed('notification');
|
onPressed: () {
|
||||||
Scaffold.of(context).closeDrawer();
|
GoRouter.of(context).pushNamed('notification');
|
||||||
},
|
Scaffold.of(context).closeDrawer();
|
||||||
),
|
},
|
||||||
IconButton(
|
),
|
||||||
icon: const Icon(Symbols.settings, fill: 1),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
onPressed: () {
|
|
||||||
GoRouter.of(context).pushNamed('settings');
|
|
||||||
Scaffold.of(context).closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -138,7 +131,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Gap(MediaQuery.of(context).padding.bottom),
|
Gap(MediaQuery.of(context).padding.bottom + 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -146,163 +139,3 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DrawerContentList extends StatelessWidget {
|
|
||||||
const _DrawerContentList();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final ct = context.read<ChatChannelProvider>();
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
final nav = context.watch<NavigationProvider>();
|
|
||||||
final rel = context.read<SnRealmProvider>();
|
|
||||||
|
|
||||||
return PageTransitionSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
transitionBuilder: (Widget child, Animation<double> primaryAnimation,
|
|
||||||
Animation<double> secondaryAnimation) {
|
|
||||||
return SharedAxisTransition(
|
|
||||||
animation: primaryAnimation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
transitionType: SharedAxisTransitionType.horizontal,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: nav.focusedRealm == null
|
|
||||||
? ListView(
|
|
||||||
key: const Key('realm-list-view'),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text('Solar Network').bold(),
|
|
||||||
AppVersionLabel(),
|
|
||||||
],
|
|
||||||
).padding(
|
|
||||||
horizontal: 32,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
minTileHeight: 48,
|
|
||||||
contentPadding: EdgeInsets.only(left: 28, right: 16),
|
|
||||||
leading: const Icon(Symbols.home),
|
|
||||||
title: Text('screenHome').tr(),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).goNamed('home');
|
|
||||||
Scaffold.of(context).closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
...rel.availableRealms.map((ele) {
|
|
||||||
return ListTile(
|
|
||||||
minTileHeight: 48,
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
leading: AccountImage(
|
|
||||||
content: ele.avatar,
|
|
||||||
radius: 16,
|
|
||||||
),
|
|
||||||
title: Text(ele.name),
|
|
||||||
onTap: () {
|
|
||||||
nav.setFocusedRealm(ele);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: ListView(
|
|
||||||
key: ValueKey(nav.focusedRealm),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
children: [
|
|
||||||
if (nav.focusedRealm!.banner != null)
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
child: AutoResizeUniversalImage(
|
|
||||||
sn.getAttachmentUrl(
|
|
||||||
nav.focusedRealm!.banner!,
|
|
||||||
),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
minTileHeight: 48,
|
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
contentPadding: EdgeInsets.only(
|
|
||||||
left: 24,
|
|
||||||
right: 16,
|
|
||||||
),
|
|
||||||
leading: AccountImage(
|
|
||||||
content: nav.focusedRealm!.avatar,
|
|
||||||
radius: 16,
|
|
||||||
),
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(Symbols.close),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
onPressed: () {
|
|
||||||
nav.setFocusedRealm(null);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
title: Text(nav.focusedRealm!.name),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'realmDetail',
|
|
||||||
pathParameters: {
|
|
||||||
'alias': nav.focusedRealm!.alias,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Scaffold.of(context).closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
minTileHeight: 48,
|
|
||||||
contentPadding: EdgeInsets.only(
|
|
||||||
left: 28,
|
|
||||||
right: 8,
|
|
||||||
),
|
|
||||||
leading: const Icon(Symbols.globe),
|
|
||||||
title: Text('community').tr(),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'realmCommunity',
|
|
||||||
pathParameters: {
|
|
||||||
'alias': nav.focusedRealm!.alias,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Scaffold.of(context).closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (ct.availableChannels
|
|
||||||
.where((ele) => ele.realmId == nav.focusedRealm?.id)
|
|
||||||
.isNotEmpty)
|
|
||||||
const Divider(height: 1),
|
|
||||||
...(ct.availableChannels
|
|
||||||
.where((ele) => ele.realmId == nav.focusedRealm?.id)
|
|
||||||
.map((ele) {
|
|
||||||
return ListTile(
|
|
||||||
minTileHeight: 48,
|
|
||||||
contentPadding: EdgeInsets.only(
|
|
||||||
left: 28,
|
|
||||||
right: 8,
|
|
||||||
),
|
|
||||||
leading: const Icon(Symbols.tag),
|
|
||||||
title: Text(ele.name),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'chatRoom',
|
|
||||||
pathParameters: {
|
|
||||||
'scope': ele.realm?.alias ?? 'global',
|
|
||||||
'alias': ele.alias,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Scaffold.of(context).closeDrawer();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
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: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/navigation.dart';
|
import 'package:surface/providers/navigation.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
|
||||||
class AppRailNavigation extends StatefulWidget {
|
class AppRailNavigation extends StatefulWidget {
|
||||||
const AppRailNavigation({super.key});
|
const AppRailNavigation({super.key});
|
||||||
@@ -18,43 +20,59 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
|
context
|
||||||
|
.read<NavigationProvider>()
|
||||||
|
.autoDetectIndex(GoRouter.maybeOf(context));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final ua = context.watch<UserProvider>();
|
||||||
final nav = context.watch<NavigationProvider>();
|
final nav = context.watch<NavigationProvider>();
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: nav,
|
listenable: nav,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
|
final destinations = nav.destinations.toList();
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: 80,
|
width: 80,
|
||||||
child: NavigationRail(
|
child: NavigationRail(
|
||||||
selectedIndex:
|
labelType: NavigationRailLabelType.selected,
|
||||||
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
|
backgroundColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerLow
|
||||||
|
.withOpacity(0.5),
|
||||||
|
selectedIndex: nav.currentIndex != null &&
|
||||||
|
nav.currentIndex! < nav.destinations.length
|
||||||
|
? nav.currentIndex
|
||||||
|
: null,
|
||||||
destinations: [
|
destinations: [
|
||||||
...destinations.where((ele) => ele.isPinned).map((ele) {
|
...destinations.map((ele) {
|
||||||
return NavigationRailDestination(
|
return NavigationRailDestination(
|
||||||
icon: ele.icon,
|
icon: ele.icon,
|
||||||
label: Text(ele.label).tr(),
|
label: Text(ele.label).tr(),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
leading: const Gap(4),
|
||||||
trailing: Expanded(
|
trailing: Expanded(
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: StyledWidget(
|
child: Padding(
|
||||||
IconButton(
|
padding: EdgeInsets.only(bottom: 24),
|
||||||
icon: const Icon(Symbols.menu),
|
child: GestureDetector(
|
||||||
onPressed: () {
|
child: AccountImage(
|
||||||
Scaffold.of(context).openDrawer();
|
content: ua.user?.avatar,
|
||||||
|
fallbackWidget:
|
||||||
|
ua.isAuthorized ? null : const Icon(Symbols.login),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).goNamed('account');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).padding(bottom: 16),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onDestinationSelected: (idx) {
|
onDestinationSelected: (idx) {
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ class AppScaffold extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
extendBody: true,
|
extendBody: true,
|
||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: noBackground
|
||||||
|
? Colors.transparent
|
||||||
|
: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: SizedBox.expand(
|
body: SizedBox.expand(
|
||||||
child: noBackground
|
child: noBackground
|
||||||
? content
|
? content
|
||||||
@@ -111,7 +113,6 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
final isCollapseDrawer = cfg.drawerIsCollapsed;
|
final isCollapseDrawer = cfg.drawerIsCollapsed;
|
||||||
final isExpandedDrawer = cfg.drawerIsExpanded;
|
|
||||||
|
|
||||||
final routeName = GoRouter.of(context)
|
final routeName = GoRouter.of(context)
|
||||||
.routerDelegate
|
.routerDelegate
|
||||||
@@ -132,19 +133,7 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
? body
|
? body
|
||||||
: Row(
|
: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
AppRailNavigation(),
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
right: BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: isExpandedDrawer
|
|
||||||
? AppNavigationDrawer(elevation: 0)
|
|
||||||
: AppRailNavigation(),
|
|
||||||
),
|
|
||||||
Expanded(child: body),
|
Expanded(child: body),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -232,10 +221,72 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
|
||||||
drawerEdgeDragWidth: isPopable ? 0 : null,
|
drawerEdgeDragWidth: isPopable ? 0 : null,
|
||||||
|
drawer: isCollapseDrawer ? const AppNavigationDrawer() : null,
|
||||||
bottomNavigationBar:
|
bottomNavigationBar:
|
||||||
isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ResponsiveScaffold extends StatelessWidget {
|
||||||
|
final Widget aside;
|
||||||
|
final Widget? child;
|
||||||
|
final int asideFlex;
|
||||||
|
final int contentFlex;
|
||||||
|
const ResponsiveScaffold({
|
||||||
|
super.key,
|
||||||
|
required this.aside,
|
||||||
|
required this.child,
|
||||||
|
this.asideFlex = 1,
|
||||||
|
this.contentFlex = 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
static bool getIsExpand(BuildContext context) {
|
||||||
|
return ResponsiveBreakpoints.of(context).largerOrEqualTo(TABLET);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (getIsExpand(context)) {
|
||||||
|
return AppBackground(
|
||||||
|
isRoot: true,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: asideFlex,
|
||||||
|
child: aside,
|
||||||
|
),
|
||||||
|
VerticalDivider(width: 1),
|
||||||
|
if (child != null && child != aside)
|
||||||
|
Flexible(flex: contentFlex, child: child!)
|
||||||
|
else
|
||||||
|
Flexible(
|
||||||
|
flex: contentFlex,
|
||||||
|
child: ResponsiveScaffoldLanding(child: null),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppBackground(isRoot: true, child: child ?? aside);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResponsiveScaffoldLanding extends StatelessWidget {
|
||||||
|
final Widget? child;
|
||||||
|
const ResponsiveScaffoldLanding({super.key, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (ResponsiveScaffold.getIsExpand(context) || child == null) {
|
||||||
|
return AppScaffold(
|
||||||
|
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||||
|
appBar: AppBar(),
|
||||||
|
body: const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return child!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,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/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
@@ -30,24 +29,14 @@ class PostCommentQuickAction extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
height: 240,
|
height: 240,
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||||
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
|
||||||
? const EdgeInsets.symmetric(vertical: 8)
|
|
||||||
: EdgeInsets.zero,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
borderRadius: BorderRadius.zero,
|
||||||
? const BorderRadius.all(Radius.circular(8))
|
border: Border.symmetric(
|
||||||
: BorderRadius.zero,
|
horizontal: BorderSide(
|
||||||
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
color: Theme.of(context).dividerColor,
|
||||||
? Border.all(
|
width: 1 / devicePixelRatio,
|
||||||
color: Theme.of(context).dividerColor,
|
),
|
||||||
width: 1 / devicePixelRatio,
|
),
|
||||||
)
|
|
||||||
: Border.symmetric(
|
|
||||||
horizontal: BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: PostMiniEditor(
|
child: PostMiniEditor(
|
||||||
postReplyId: parentPost.id,
|
postReplyId: parentPost.id,
|
||||||
@@ -103,7 +92,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client
|
await sn.client
|
||||||
.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
|
.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
|
||||||
'publisher': answer.publisherId,
|
'publisher': widget.parentPost.publisherId,
|
||||||
'answer_id': answer.id,
|
'answer_id': answer.id,
|
||||||
});
|
});
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -190,57 +179,54 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
|||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
return SizedBox(
|
return Column(
|
||||||
height: MediaQuery.of(context).size.height * 0.85,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Row(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Row(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
const Icon(Symbols.comment, size: 24),
|
||||||
children: [
|
const Gap(16),
|
||||||
const Icon(Symbols.comment, size: 24),
|
Text('postCommentsDetailed')
|
||||||
const Gap(16),
|
.plural(widget.commentCount)
|
||||||
Text('postCommentsDetailed')
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
.plural(widget.commentCount)
|
],
|
||||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
],
|
Expanded(
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
child: CustomScrollView(
|
||||||
Expanded(
|
slivers: [
|
||||||
child: CustomScrollView(
|
if (ua.isAuthorized)
|
||||||
slivers: [
|
SliverToBoxAdapter(
|
||||||
if (ua.isAuthorized)
|
child: Container(
|
||||||
SliverToBoxAdapter(
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
child: Container(
|
height: 240,
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
decoration: BoxDecoration(
|
||||||
height: 240,
|
border: Border.symmetric(
|
||||||
decoration: BoxDecoration(
|
horizontal: BorderSide(
|
||||||
border: Border.symmetric(
|
color: Theme.of(context).dividerColor,
|
||||||
horizontal: BorderSide(
|
width: 1 / devicePixelRatio,
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: PostMiniEditor(
|
),
|
||||||
postReplyId: widget.post.id,
|
child: PostMiniEditor(
|
||||||
onPost: () {
|
postReplyId: widget.post.id,
|
||||||
_childListKey.currentState!.refresh();
|
onPost: () {
|
||||||
},
|
_childListKey.currentState!.refresh();
|
||||||
onExpand: () {
|
},
|
||||||
Navigator.pop(context);
|
onExpand: () {
|
||||||
},
|
Navigator.pop(context);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PostCommentSliverList(
|
|
||||||
parentPost: widget.post,
|
|
||||||
key: _childListKey,
|
|
||||||
),
|
),
|
||||||
],
|
PostCommentSliverList(
|
||||||
),
|
parentPost: widget.post,
|
||||||
|
key: _childListKey,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_saver/file_saver.dart';
|
import 'package:file_saver/file_saver.dart';
|
||||||
@@ -13,7 +12,6 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.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';
|
||||||
import 'package:popover/popover.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
@@ -26,7 +24,6 @@ import 'package:surface/providers/sn_network.dart';
|
|||||||
import 'package:surface/providers/translation.dart';
|
import 'package:surface/providers/translation.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/screens/post/post_detail.dart';
|
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/types/reaction.dart';
|
import 'package:surface/types/reaction.dart';
|
||||||
@@ -53,6 +50,7 @@ class OpenablePostItem extends StatelessWidget {
|
|||||||
final bool showMenu;
|
final bool showMenu;
|
||||||
final bool showFullPost;
|
final bool showFullPost;
|
||||||
final bool showExpandableComments;
|
final bool showExpandableComments;
|
||||||
|
final bool useReplace;
|
||||||
final double? maxWidth;
|
final double? maxWidth;
|
||||||
final Function(SnPost data)? onChanged;
|
final Function(SnPost data)? onChanged;
|
||||||
final Function()? onDeleted;
|
final Function()? onDeleted;
|
||||||
@@ -66,6 +64,7 @@ class OpenablePostItem extends StatelessWidget {
|
|||||||
this.showMenu = true,
|
this.showMenu = true,
|
||||||
this.showFullPost = false,
|
this.showFullPost = false,
|
||||||
this.showExpandableComments = false,
|
this.showExpandableComments = false,
|
||||||
|
this.useReplace = false,
|
||||||
this.maxWidth,
|
this.maxWidth,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.onDeleted,
|
this.onDeleted,
|
||||||
@@ -74,40 +73,32 @@ class OpenablePostItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cfg = context.read<ConfigProvider>();
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: OpenContainer(
|
child: GestureDetector(
|
||||||
closedBuilder: (_, __) => Container(
|
child: PostItem(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
data: data,
|
||||||
child: PostItem(
|
maxWidth: maxWidth,
|
||||||
data: data,
|
showComments: showComments,
|
||||||
maxWidth: maxWidth,
|
showFullPost: showFullPost,
|
||||||
showComments: showComments,
|
showExpandableComments: showExpandableComments,
|
||||||
showFullPost: showFullPost,
|
onChanged: onChanged,
|
||||||
showExpandableComments: showExpandableComments,
|
onDeleted: onDeleted,
|
||||||
onChanged: onChanged,
|
onSelectAnswer: onSelectAnswer,
|
||||||
onDeleted: onDeleted,
|
|
||||||
onSelectAnswer: onSelectAnswer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
openBuilder: (_, close) => PostDetailScreen(
|
|
||||||
slug: data.id.toString(),
|
|
||||||
preload: data,
|
|
||||||
onBack: close,
|
|
||||||
),
|
|
||||||
openColor: Colors.transparent,
|
|
||||||
openElevation: 0,
|
|
||||||
transitionType: ContainerTransitionType.fade,
|
|
||||||
closedElevation: 0,
|
|
||||||
closedColor: Theme.of(context).colorScheme.surface.withOpacity(
|
|
||||||
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0 : 1,
|
|
||||||
),
|
|
||||||
closedShape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
|
||||||
),
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (useReplace) {
|
||||||
|
GoRouter.of(context)
|
||||||
|
.pushReplacementNamed('postDetail', pathParameters: {
|
||||||
|
'slug': data.id.toString(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
GoRouter.of(context).pushNamed('postDetail', pathParameters: {
|
||||||
|
'slug': data.id.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -279,9 +270,13 @@ class _PostItemState extends State<PostItem> {
|
|||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
final isAuthor =
|
final isAuthor =
|
||||||
ua.isAuthorized && widget.data.publisher.accountId == ua.user?.id;
|
ua.isAuthorized && widget.data.publisher.accountId == ua.user?.id;
|
||||||
|
final isParentAuthor = ua.isAuthorized &&
|
||||||
|
widget.data.replyTo?.publisher.accountId == ua.user?.id;
|
||||||
|
|
||||||
final displayableAttachments = widget.data.preload?.attachments
|
final displayableAttachments = widget.data.body['attachments']
|
||||||
?.where((ele) =>
|
?.map((e) => SnAttachment.fromJson(e))
|
||||||
|
.cast<SnAttachment>()
|
||||||
|
.where((ele) =>
|
||||||
ele?.mediaType != SnMediaType.image ||
|
ele?.mediaType != SnMediaType.image ||
|
||||||
widget.data.type != 'article')
|
widget.data.type != 'article')
|
||||||
.toList();
|
.toList();
|
||||||
@@ -290,7 +285,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
|
|
||||||
var attachmentSize = math.min(
|
var attachmentSize = math.min(
|
||||||
MediaQuery.of(context).size.width, widget.maxWidth ?? double.infinity);
|
MediaQuery.of(context).size.width, widget.maxWidth ?? double.infinity);
|
||||||
if ((widget.data.preload?.attachments?.length ?? 0) > 1) {
|
if ((widget.data.body['attachments']?.length ?? 0) > 1) {
|
||||||
attachmentSize -= 80;
|
attachmentSize -= 80;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,6 +328,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
_PostActionPopup(
|
_PostActionPopup(
|
||||||
data: widget.data,
|
data: widget.data,
|
||||||
isAuthor: isAuthor,
|
isAuthor: isAuthor,
|
||||||
|
isParentAuthor: isParentAuthor,
|
||||||
onShare: () => _doShare(context),
|
onShare: () => _doShare(context),
|
||||||
onShareImage: () => _doShareViaPicture(context),
|
onShareImage: () => _doShareViaPicture(context),
|
||||||
onSelectAnswer: widget.onSelectAnswer,
|
onSelectAnswer: widget.onSelectAnswer,
|
||||||
@@ -346,7 +342,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (widget.data.preload?.thumbnail != null)
|
if (widget.data.body['thumbnail'] != null)
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -366,14 +362,14 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
child: AutoResizeUniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(
|
sn.getAttachmentUrl(
|
||||||
widget.data.preload!.thumbnail!.rid,
|
widget.data.body['thumbnail']['rid'],
|
||||||
),
|
),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.data.preload?.video != null)
|
if (widget.data.body['video'] != null)
|
||||||
_PostVideoPlayer(data: widget.data).padding(bottom: 8),
|
_PostVideoPlayer(data: widget.data).padding(bottom: 8),
|
||||||
if (widget.data.type == 'question')
|
if (widget.data.type == 'question')
|
||||||
_PostQuestionHint(data: widget.data).padding(bottom: 8),
|
_PostQuestionHint(data: widget.data).padding(bottom: 8),
|
||||||
@@ -460,10 +456,10 @@ class _PostItemState extends State<PostItem> {
|
|||||||
if (widget.data.repostTo != null)
|
if (widget.data.repostTo != null)
|
||||||
_PostQuoteContent(child: widget.data.repostTo!).padding(
|
_PostQuoteContent(child: widget.data.repostTo!).padding(
|
||||||
top: 4,
|
top: 4,
|
||||||
bottom: widget.data.preload?.attachments?.isNotEmpty ??
|
bottom:
|
||||||
false
|
widget.data.body['attachments'].isNotEmpty ?? false
|
||||||
? 12
|
? 12
|
||||||
: 0,
|
: 0,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(
|
).padding(
|
||||||
@@ -484,11 +480,11 @@ class _PostItemState extends State<PostItem> {
|
|||||||
fit: widget.showFullPost ? BoxFit.cover : BoxFit.contain,
|
fit: widget.showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||||
padding: EdgeInsets.only(left: 12, right: 12),
|
padding: EdgeInsets.only(left: 12, right: 12),
|
||||||
),
|
),
|
||||||
if (widget.data.preload?.poll != null)
|
if (widget.data.poll != null)
|
||||||
StyledWidget(Container(
|
StyledWidget(Container(
|
||||||
constraints:
|
constraints:
|
||||||
BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
|
BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
|
||||||
child: PostPoll(poll: widget.data.preload!.poll!),
|
child: PostPoll(poll: widget.data.poll!),
|
||||||
))
|
))
|
||||||
.padding(
|
.padding(
|
||||||
left: 12,
|
left: 12,
|
||||||
@@ -577,6 +573,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
_PostActionPopup(
|
_PostActionPopup(
|
||||||
data: widget.data,
|
data: widget.data,
|
||||||
isAuthor: isAuthor,
|
isAuthor: isAuthor,
|
||||||
|
isParentAuthor: isParentAuthor,
|
||||||
onShare: () => _doShare(context),
|
onShare: () => _doShare(context),
|
||||||
onShareImage: () => _doShareViaPicture(context),
|
onShareImage: () => _doShareViaPicture(context),
|
||||||
onSelectAnswer: widget.onSelectAnswer,
|
onSelectAnswer: widget.onSelectAnswer,
|
||||||
@@ -589,7 +586,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(bottom: widget.showCompactAvatar ? 4 : 0),
|
).padding(bottom: widget.showCompactAvatar ? 4 : 0),
|
||||||
if (widget.data.preload?.thumbnail != null)
|
if (widget.data.body['thumbnail'] != null)
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -609,14 +606,14 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
child: AutoResizeUniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(
|
sn.getAttachmentUrl(
|
||||||
widget.data.preload!.thumbnail!.rid,
|
widget.data.body['thumbnail']['rid'],
|
||||||
),
|
),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.data.preload?.video != null)
|
if (widget.data.body['video'] != null)
|
||||||
_PostVideoPlayer(data: widget.data)
|
_PostVideoPlayer(data: widget.data)
|
||||||
.padding(bottom: 8),
|
.padding(bottom: 8),
|
||||||
if (widget.data.type == 'question')
|
if (widget.data.type == 'question')
|
||||||
@@ -716,7 +713,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
_isTranslated ||
|
_isTranslated ||
|
||||||
_isTranslating) &&
|
_isTranslating) &&
|
||||||
(widget.data.repostTo != null ||
|
(widget.data.repostTo != null ||
|
||||||
(widget.data.preload?.attachments
|
(widget.data.body['attachments']
|
||||||
?.isNotEmpty ??
|
?.isNotEmpty ??
|
||||||
false))
|
false))
|
||||||
? 8
|
? 8
|
||||||
@@ -726,7 +723,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
_PostQuoteContent(child: widget.data.repostTo!)
|
_PostQuoteContent(child: widget.data.repostTo!)
|
||||||
.padding(
|
.padding(
|
||||||
bottom:
|
bottom:
|
||||||
(widget.data.preload?.attachments?.isNotEmpty ??
|
(widget.data.body['attachments']?.isNotEmpty ??
|
||||||
false)
|
false)
|
||||||
? 8
|
? 8
|
||||||
: 0,
|
: 0,
|
||||||
@@ -750,8 +747,8 @@ class _PostItemState extends State<PostItem> {
|
|||||||
padding:
|
padding:
|
||||||
EdgeInsets.only(left: widget.showAvatar ? 60 : 12, right: 12),
|
EdgeInsets.only(left: widget.showAvatar ? 60 : 12, right: 12),
|
||||||
),
|
),
|
||||||
if (widget.data.preload?.poll != null)
|
if (widget.data.poll != null)
|
||||||
PostPoll(poll: widget.data.preload!.poll!).padding(
|
PostPoll(poll: widget.data.poll!).padding(
|
||||||
left: widget.showAvatar ? 60 : 12,
|
left: widget.showAvatar ? 60 : 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
top: 12,
|
top: 12,
|
||||||
@@ -812,7 +809,7 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (data.preload?.thumbnail != null)
|
if (data.body['thumbnail'] != null)
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
@@ -821,7 +818,7 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
topRight: Radius.circular(8),
|
topRight: Radius.circular(8),
|
||||||
),
|
),
|
||||||
child: AutoResizeUniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(data.preload!.thumbnail!.rid),
|
sn.getAttachmentUrl(data.body['thumbnail']['rid']),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
@@ -859,9 +856,13 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
isRelativeDate: false,
|
isRelativeDate: false,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).padding(horizontal: 16, bottom: 8),
|
||||||
if (data.type != 'article' &&
|
if (data.type != 'article' &&
|
||||||
(data.preload?.attachments?.isNotEmpty ?? false))
|
(data.body['attachments']?.isNotEmpty ?? false))
|
||||||
StyledWidget(AttachmentList(
|
StyledWidget(AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.body['attachments']
|
||||||
|
?.map((e) => SnAttachment.fromJson(e))
|
||||||
|
.cast<SnAttachment>()
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
columned: true,
|
columned: true,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
@@ -1150,31 +1151,9 @@ class _PostHeadline extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (isEnlarge) {
|
if (isEnlarge) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (data.preload?.thumbnail != null)
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: AutoResizeUniversalImage(
|
|
||||||
sn.getAttachmentUrl(data.preload!.thumbnail!.rid),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (data.body['title'] != null || (title?.isNotEmpty ?? false))
|
if (data.body['title'] != null || (title?.isNotEmpty ?? false))
|
||||||
Text(
|
Text(
|
||||||
title ?? data.body['title'],
|
title ?? data.body['title'],
|
||||||
@@ -1259,7 +1238,7 @@ class _PostAvatar extends StatelessWidget {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: data.preload?.realm == null
|
child: data.realm == null
|
||||||
? AccountImage(
|
? AccountImage(
|
||||||
filterQuality: filterQuality,
|
filterQuality: filterQuality,
|
||||||
content: data.publisher.avatar,
|
content: data.publisher.avatar,
|
||||||
@@ -1275,7 +1254,7 @@ class _PostAvatar extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: AccountImage(
|
: AccountImage(
|
||||||
filterQuality: filterQuality,
|
filterQuality: filterQuality,
|
||||||
content: data.preload!.realm!.avatar,
|
content: data.realm!.avatar,
|
||||||
radius: isCompact ? 12 : 20,
|
radius: isCompact ? 12 : 20,
|
||||||
borderRadius: isCompact ? 4 : 8,
|
borderRadius: isCompact ? 4 : 8,
|
||||||
badgeOffset: Offset(-6, -4),
|
badgeOffset: Offset(-6, -4),
|
||||||
@@ -1294,20 +1273,11 @@ class _PostAvatar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showPopover(
|
showModalBottomSheet(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
||||||
context: context,
|
context: context,
|
||||||
transition: PopoverTransition.other,
|
builder: (context) => PublisherPopoverCard(
|
||||||
bodyBuilder: (context) => SizedBox(
|
data: data.publisher,
|
||||||
width: math.min(400, MediaQuery.of(context).size.width - 10),
|
|
||||||
child: PublisherPopoverCard(
|
|
||||||
data: data.publisher,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
direction: PopoverDirection.bottom,
|
|
||||||
arrowHeight: 5,
|
|
||||||
arrowWidth: 15,
|
|
||||||
arrowDxOffset: -190,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1317,6 +1287,7 @@ class _PostAvatar extends StatelessWidget {
|
|||||||
class _PostActionPopup extends StatelessWidget {
|
class _PostActionPopup extends StatelessWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
final bool isAuthor;
|
final bool isAuthor;
|
||||||
|
final bool isParentAuthor;
|
||||||
final Function onDeleted;
|
final Function onDeleted;
|
||||||
final Function() onShare, onShareImage;
|
final Function() onShare, onShareImage;
|
||||||
final Function()? onSelectAnswer;
|
final Function()? onSelectAnswer;
|
||||||
@@ -1324,6 +1295,7 @@ class _PostActionPopup extends StatelessWidget {
|
|||||||
const _PostActionPopup({
|
const _PostActionPopup({
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.isAuthor,
|
required this.isAuthor,
|
||||||
|
required this.isParentAuthor,
|
||||||
required this.onDeleted,
|
required this.onDeleted,
|
||||||
required this.onShare,
|
required this.onShare,
|
||||||
required this.onShareImage,
|
required this.onShareImage,
|
||||||
@@ -1397,7 +1369,7 @@ class _PostActionPopup extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (onTranslate != null) PopupMenuDivider(),
|
if (onTranslate != null) PopupMenuDivider(),
|
||||||
if (isAuthor && onSelectAnswer != null)
|
if (isParentAuthor && onSelectAnswer != null)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -1410,7 +1382,7 @@ class _PostActionPopup extends StatelessWidget {
|
|||||||
onSelectAnswer?.call();
|
onSelectAnswer?.call();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (isAuthor && onSelectAnswer != null) PopupMenuDivider(),
|
if (isParentAuthor && onSelectAnswer != null) PopupMenuDivider(),
|
||||||
if (isAuthor)
|
if (isAuthor)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -1570,6 +1542,7 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (isCompact) {
|
if (isCompact) {
|
||||||
return Row(
|
return Row(
|
||||||
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -1577,7 +1550,6 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
).bold(),
|
).bold(),
|
||||||
),
|
),
|
||||||
const Gap(4),
|
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
isRelativeDate
|
isRelativeDate
|
||||||
@@ -1589,6 +1561,10 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
).fontSize(13).opacity(0.8),
|
).fontSize(13).opacity(0.8),
|
||||||
),
|
),
|
||||||
|
if (data.editedAt != null)
|
||||||
|
Flexible(
|
||||||
|
child: Text('postEditedHint').tr().fontSize(13).opacity(0.8),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -1598,20 +1574,20 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(data.publisher.nick).bold(),
|
Text(data.publisher.nick).bold(),
|
||||||
if (data.preload?.realm != null)
|
if (data.realm != null)
|
||||||
const Icon(Symbols.arrow_right, size: 16)
|
const Icon(Symbols.arrow_right, size: 16)
|
||||||
.padding(horizontal: 2)
|
.padding(horizontal: 2)
|
||||||
.opacity(0.5),
|
.opacity(0.5),
|
||||||
if (data.preload?.realm != null) Text(data.preload!.realm!.name),
|
if (data.realm != null) Text(data.realm!.name),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'@${data.publisher.name}',
|
'@${data.publisher.name}',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
).fontSize(13),
|
).fontSize(13),
|
||||||
const Gap(4),
|
|
||||||
Text(
|
Text(
|
||||||
isRelativeDate
|
isRelativeDate
|
||||||
? RelativeTime(context)
|
? RelativeTime(context)
|
||||||
@@ -1621,6 +1597,8 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
).fontSize(13),
|
).fontSize(13),
|
||||||
|
if (data.editedAt != null)
|
||||||
|
Text('postEditedHint').tr().fontSize(13),
|
||||||
],
|
],
|
||||||
).opacity(0.8),
|
).opacity(0.8),
|
||||||
],
|
],
|
||||||
@@ -1650,7 +1628,11 @@ class _PostContentBody extends StatelessWidget {
|
|||||||
RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
|
RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
|
||||||
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
|
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
|
||||||
content: text,
|
content: text,
|
||||||
attachments: data.preload?.attachments,
|
attachments: data.body['attachments']
|
||||||
|
?.map((e) => SnAttachment.fromJson(e))
|
||||||
|
.cast<SnAttachment>()
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSelectable) {
|
if (isSelectable) {
|
||||||
@@ -1708,14 +1690,14 @@ class _PostQuoteContent extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
if (child.type != 'article' &&
|
if (child.type != 'article' &&
|
||||||
(child.preload?.attachments?.isNotEmpty ?? false))
|
(child.body['attachments']?.isNotEmpty ?? false))
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(8),
|
bottomLeft: Radius.circular(8),
|
||||||
bottomRight: Radius.circular(8),
|
bottomRight: Radius.circular(8),
|
||||||
),
|
),
|
||||||
child: AttachmentList(
|
child: AttachmentList(
|
||||||
data: child.preload!.attachments!,
|
data: child.body['attachments']!,
|
||||||
maxHeight: 360,
|
maxHeight: 360,
|
||||||
minWidth: 640,
|
minWidth: 640,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
@@ -2064,8 +2046,6 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: true,
|
|
||||||
isScrollControlled: true,
|
|
||||||
builder: (context) => PostCommentListPopup(
|
builder: (context) => PostCommentListPopup(
|
||||||
post: widget.data,
|
post: widget.data,
|
||||||
commentCount: widget.data.metric.replyCount,
|
commentCount: widget.data.metric.replyCount,
|
||||||
@@ -2354,7 +2334,7 @@ class _PostVideoPlayer extends StatelessWidget {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: AttachmentItem(
|
child: AttachmentItem(
|
||||||
data: data.preload!.video!,
|
data: data.body['video'],
|
||||||
heroTag: 'post-video-${data.id}',
|
heroTag: 'post-video-${data.id}',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user