Compare commits
88 Commits
71b41d470a
...
2.5.2+92
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a8e40b288 | |||
| cb0986efee | |||
| ce3d19fb7b | |||
| 935cf774b1 | |||
| aa50561247 | |||
| 7501139d4c | |||
| 33fc7b287e | |||
| 5c9569ef36 | |||
| 48f40099f4 | |||
| 151f917b07 | |||
| cead09f3aa | |||
| aed7c61ba0 | |||
| 9d685fa0d9 | |||
| 60afc96da2 | |||
| 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 | |||
| 6e6c3f42f6 | |||
| dc38b46b2c | |||
| b4990308e9 | |||
| 237abe564d |
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.
|
|
||||||
@@ -9,11 +9,13 @@
|
|||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
android:maxSdkVersion="29" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
tools:replace="android:label"
|
||||||
android:label="Solian"
|
android:label="Solian"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,8 +11,5 @@ post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
{
|
{}
|
||||||
"sources": ["taiwan-pts"],
|
|
||||||
"eager": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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 |
@@ -543,6 +543,7 @@
|
|||||||
"attachmentSaved": "Saved to album",
|
"attachmentSaved": "Saved to album",
|
||||||
"attachmentSavedDesktop": "Saved to Downloads folder",
|
"attachmentSavedDesktop": "Saved to Downloads folder",
|
||||||
"openInAlbum": "Open in album",
|
"openInAlbum": "Open in album",
|
||||||
|
"openInBrowser": "Open in browser",
|
||||||
"postAbuseReport": "Report Post",
|
"postAbuseReport": "Report Post",
|
||||||
"postAbuseReportDescription": "Report posts that violate our user agreement and community guidelines to help us improve the content on Solar Network. Please describe how this post violates the relevant rules. Do not include any sensitive information. We will process your report within 24 hours.",
|
"postAbuseReportDescription": "Report posts that violate our user agreement and community guidelines to help us improve the content on Solar Network. Please describe how this post violates the relevant rules. Do not include any sensitive information. We will process your report within 24 hours.",
|
||||||
"abuseReport": "Abuse Report",
|
"abuseReport": "Abuse Report",
|
||||||
@@ -639,6 +640,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",
|
||||||
@@ -881,5 +883,87 @@
|
|||||||
"appInitUserDirectory": "Initializing User Directory",
|
"appInitUserDirectory": "Initializing User Directory",
|
||||||
"appInitRealm": "Initializing Realms",
|
"appInitRealm": "Initializing Realms",
|
||||||
"appInitChat": "Initializing Chat",
|
"appInitChat": "Initializing Chat",
|
||||||
"appInitDone": "Completed"
|
"appInitDone": "Completed",
|
||||||
|
"community": "Community",
|
||||||
|
"realmCommunity": "{}'s Community",
|
||||||
|
"postTotalCount": {
|
||||||
|
"one": "Total {} post",
|
||||||
|
"other": "Total {} posts"
|
||||||
|
},
|
||||||
|
"settingsHideBottomNav": "Hide Bottom Navigation",
|
||||||
|
"settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.",
|
||||||
|
"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™",
|
||||||
|
"attachmentEditor": "Attachment editor",
|
||||||
|
"attachmentEditorUnUploadHint": "This attachment is not uploaded, metadata editing is unavailable, and you can crop this attachment.",
|
||||||
|
"attachmentEditorUploadHint": "This attachment is uploaded.",
|
||||||
|
"attachmentRating": "Rating",
|
||||||
|
"fieldAttachmentRating": "Content Rating",
|
||||||
|
"fieldAttachmentQuality": "Quality Rating",
|
||||||
|
"attachmentReferenceLink": "Use external attachment",
|
||||||
|
"fieldAttachmentReferenceLink": "Reference Link",
|
||||||
|
"attachmentReferenceLinkDescription": "It will be used as the source file of the attachment. The link needs to allow cross-origin access.",
|
||||||
|
"fieldAttachmentMimetype": "Mimetype",
|
||||||
|
"postVideoLive": "Live Stream",
|
||||||
|
"postVideoLiveDescription": "This is a live video, you can embed the source site by yourself.",
|
||||||
|
"postVideoRendererWeb": "WebView Rendering",
|
||||||
|
"postVideoRendererWebDescription": "Use WebView to render the content",
|
||||||
|
"fieldPostVideoStreamUrl": "Live Stream URL",
|
||||||
|
"fieldPostVideoStreamUrlDescription": "Live stream URL, can be HLS or webpage; use webpage renderer if you want to embed the webpage."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -541,6 +541,7 @@
|
|||||||
"attachmentSaved": "已保存到相册",
|
"attachmentSaved": "已保存到相册",
|
||||||
"attachmentSavedDesktop": "已保存到下载目录",
|
"attachmentSavedDesktop": "已保存到下载目录",
|
||||||
"openInAlbum": "在相册中打开",
|
"openInAlbum": "在相册中打开",
|
||||||
|
"openInBrowser": "在浏览器中打开",
|
||||||
"postAbuseReport": "检举帖子",
|
"postAbuseReport": "检举帖子",
|
||||||
"postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
|
"postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
|
||||||
"abuseReport": "检举",
|
"abuseReport": "检举",
|
||||||
@@ -879,5 +880,87 @@
|
|||||||
"appInitUserDirectory": "正在初始化用户目录",
|
"appInitUserDirectory": "正在初始化用户目录",
|
||||||
"appInitRealm": "正在初始化领域信息",
|
"appInitRealm": "正在初始化领域信息",
|
||||||
"appInitChat": "正在初始化聊天",
|
"appInitChat": "正在初始化聊天",
|
||||||
"appInitDone": "完成"
|
"appInitDone": "完成",
|
||||||
|
"community": "社区",
|
||||||
|
"realmCommunity": "{}的社区",
|
||||||
|
"postTotalCount": {
|
||||||
|
"zero": "没有帖子",
|
||||||
|
"one": "共 {} 条帖子"
|
||||||
|
},
|
||||||
|
"settingsHideBottomNav": "隐藏底部导航栏",
|
||||||
|
"settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。",
|
||||||
|
"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™ 取得太阳链连接",
|
||||||
|
"attachmentEditor": "附件编辑器",
|
||||||
|
"attachmentEditorUnUploadHint": "该附件未上传,元数据编辑不可用,同时你可以裁剪本附件。",
|
||||||
|
"attachmentEditorUploadHint": "该附件已上传。",
|
||||||
|
"attachmentRating": "评级",
|
||||||
|
"fieldAttachmentRating": "内容分级",
|
||||||
|
"fieldAttachmentQuality": "质量评分",
|
||||||
|
"attachmentReferenceLink": "引用外部附件",
|
||||||
|
"fieldAttachmentReferenceLink": "引用连接",
|
||||||
|
"attachmentReferenceLinkDescription": "作为附件的源文件。需要链接允许跨域访问。",
|
||||||
|
"fieldAttachmentMimetype": "文件类型",
|
||||||
|
"postVideoLive": "直播",
|
||||||
|
"postVideoLiveDescription": "这是一条直播影片,允许用户自行嵌入源站。",
|
||||||
|
"postVideoRendererWeb": "网页渲染器",
|
||||||
|
"postVideoRendererWebDescription": "使用 WebView 渲染内容。",
|
||||||
|
"fieldPostVideoStreamUrl": "直播流地址",
|
||||||
|
"fieldPostVideoStreamUrlDescription": "直播流地址,可以为 HLS,或者网页;使用网页嵌入请启用网页渲染器。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -879,5 +879,21 @@
|
|||||||
"appInitUserDirectory": "正在初始化用户目錄",
|
"appInitUserDirectory": "正在初始化用户目錄",
|
||||||
"appInitRealm": "正在初始化領域信息",
|
"appInitRealm": "正在初始化領域信息",
|
||||||
"appInitChat": "正在初始化聊天",
|
"appInitChat": "正在初始化聊天",
|
||||||
"appInitDone": "完成"
|
"appInitDone": "完成",
|
||||||
|
"community": "社區",
|
||||||
|
"realmCommunity": "{}的社區",
|
||||||
|
"postTotalCount": {
|
||||||
|
"zero": "沒有帖子",
|
||||||
|
"one": "共 {} 條帖子"
|
||||||
|
},
|
||||||
|
"settingsHideBottomNav": "隱藏底部導航欄",
|
||||||
|
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
|
||||||
|
"reCaptcha": "人機驗證",
|
||||||
|
"friends": "好友",
|
||||||
|
"friendsDescription": "管理好友關係。",
|
||||||
|
"album": "相冊",
|
||||||
|
"albumDescription": "查看相冊與管理上傳附件。",
|
||||||
|
"stickers": "貼圖",
|
||||||
|
"stickersDescription": "查看貼圖包與管理貼圖。",
|
||||||
|
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -879,5 +879,21 @@
|
|||||||
"appInitUserDirectory": "正在初始化用戶目錄",
|
"appInitUserDirectory": "正在初始化用戶目錄",
|
||||||
"appInitRealm": "正在初始化領域信息",
|
"appInitRealm": "正在初始化領域信息",
|
||||||
"appInitChat": "正在初始化聊天",
|
"appInitChat": "正在初始化聊天",
|
||||||
"appInitDone": "完成"
|
"appInitDone": "完成",
|
||||||
|
"community": "社區",
|
||||||
|
"realmCommunity": "{}的社區",
|
||||||
|
"postTotalCount": {
|
||||||
|
"zero": "沒有帖子",
|
||||||
|
"one": "共 {} 條帖子"
|
||||||
|
},
|
||||||
|
"settingsHideBottomNav": "隱藏底部導航欄",
|
||||||
|
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
|
||||||
|
"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.
@@ -1,5 +1,5 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
# Uncomment this line to define a global platform for your project
|
||||||
platform :ios, '13.0'
|
platform :ios, '15.1'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|||||||
228
ios/Podfile.lock
228
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)
|
||||||
@@ -120,27 +122,26 @@ PODS:
|
|||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- flutter_webrtc (0.12.6):
|
|
||||||
- Flutter
|
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
|
||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- GoogleAppMeasurement (11.8.0):
|
- Giphy (2.2.12):
|
||||||
- GoogleAppMeasurement/AdIdSupport (= 11.8.0)
|
- libwebp
|
||||||
|
- GoogleAppMeasurement (11.10.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,15 +183,28 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.2.0)
|
- jitsi_meet_flutter_sdk (11.1.1):
|
||||||
- livekit_client (2.4.1):
|
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_webrtc
|
- JitsiMeetSDK (= 11.1.1)
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- JitsiMeetSDK (11.1.1):
|
||||||
|
- Giphy (= 2.2.12)
|
||||||
|
- JitsiWebRTC (~> 124.0)
|
||||||
|
- JitsiWebRTC (124.0.2)
|
||||||
|
- Kingfisher (8.3.1)
|
||||||
|
- libwebp (1.5.0):
|
||||||
|
- libwebp/demux (= 1.5.0)
|
||||||
|
- libwebp/mux (= 1.5.0)
|
||||||
|
- libwebp/sharpyuv (= 1.5.0)
|
||||||
|
- libwebp/webp (= 1.5.0)
|
||||||
|
- libwebp/demux (1.5.0):
|
||||||
|
- libwebp/webp
|
||||||
|
- libwebp/mux (1.5.0):
|
||||||
|
- libwebp/demux
|
||||||
|
- libwebp/sharpyuv (1.5.0)
|
||||||
|
- libwebp/webp (1.5.0):
|
||||||
|
- libwebp/sharpyuv
|
||||||
- media_kit_libs_ios_video (1.0.4):
|
- media_kit_libs_ios_video (1.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- media_kit_native_event_loop (1.0.0):
|
|
||||||
- Flutter
|
|
||||||
- media_kit_video (0.0.1):
|
- media_kit_video (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- nanopb (3.30910.0):
|
- nanopb (3.30910.0):
|
||||||
@@ -212,11 +226,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):
|
||||||
@@ -232,6 +244,8 @@ PODS:
|
|||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/fts5 (3.49.1):
|
- sqlite3/fts5 (3.49.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
|
- sqlite3/math (3.49.1):
|
||||||
|
- sqlite3/common
|
||||||
- sqlite3/perf-threadsafe (3.49.1):
|
- sqlite3/perf-threadsafe (3.49.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/rtree (3.49.1):
|
- sqlite3/rtree (3.49.1):
|
||||||
@@ -242,6 +256,7 @@ PODS:
|
|||||||
- sqlite3 (~> 3.49.1)
|
- sqlite3 (~> 3.49.1)
|
||||||
- sqlite3/dbstatvtab
|
- sqlite3/dbstatvtab
|
||||||
- sqlite3/fts5
|
- sqlite3/fts5
|
||||||
|
- sqlite3/math
|
||||||
- sqlite3/perf-threadsafe
|
- sqlite3/perf-threadsafe
|
||||||
- sqlite3/rtree
|
- sqlite3/rtree
|
||||||
- SwiftyGif (5.4.5)
|
- SwiftyGif (5.4.5)
|
||||||
@@ -253,12 +268,12 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- wakelock_plus (0.0.1):
|
- wakelock_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (125.6422.06)
|
|
||||||
- workmanager (0.0.1):
|
- workmanager (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
|
||||||
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`)
|
||||||
@@ -274,22 +289,19 @@ DEPENDENCIES:
|
|||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
|
||||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||||
|
- jitsi_meet_flutter_sdk (from `.symlinks/plugins/jitsi_meet_flutter_sdk/ios`)
|
||||||
- Kingfisher (~> 8.0)
|
- Kingfisher (~> 8.0)
|
||||||
- livekit_client (from `.symlinks/plugins/livekit_client/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`)
|
||||||
@@ -311,10 +323,14 @@ SPEC REPOS:
|
|||||||
- FirebaseCoreInternal
|
- FirebaseCoreInternal
|
||||||
- FirebaseInstallations
|
- FirebaseInstallations
|
||||||
- FirebaseMessaging
|
- FirebaseMessaging
|
||||||
|
- Giphy
|
||||||
- GoogleAppMeasurement
|
- GoogleAppMeasurement
|
||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
|
- JitsiMeetSDK
|
||||||
|
- JitsiWebRTC
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
|
- libwebp
|
||||||
- nanopb
|
- nanopb
|
||||||
- OrderedSet
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
@@ -322,9 +338,10 @@ SPEC REPOS:
|
|||||||
- SDWebImage
|
- SDWebImage
|
||||||
- sqlite3
|
- sqlite3
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
- 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:
|
||||||
@@ -355,8 +372,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_timezone/ios"
|
:path: ".symlinks/plugins/flutter_timezone/ios"
|
||||||
flutter_udid:
|
flutter_udid:
|
||||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||||
flutter_webrtc:
|
|
||||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
|
||||||
gal:
|
gal:
|
||||||
:path: ".symlinks/plugins/gal/darwin"
|
:path: ".symlinks/plugins/gal/darwin"
|
||||||
home_widget:
|
home_widget:
|
||||||
@@ -365,12 +380,10 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
in_app_review:
|
in_app_review:
|
||||||
:path: ".symlinks/plugins/in_app_review/ios"
|
:path: ".symlinks/plugins/in_app_review/ios"
|
||||||
livekit_client:
|
jitsi_meet_flutter_sdk:
|
||||||
:path: ".symlinks/plugins/livekit_client/ios"
|
:path: ".symlinks/plugins/jitsi_meet_flutter_sdk/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:
|
||||||
@@ -383,8 +396,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:
|
||||||
@@ -406,66 +417,67 @@ 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
|
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
Giphy: 83628960ed04e1c3428ff1b4fb2b027f65e82f50
|
||||||
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
|
jitsi_meet_flutter_sdk: 0283a60730922d608fbad9872e07afdd5bb3578a
|
||||||
livekit_client: 170022ce5f7c8c70d7f862ac9c17e11508ad5fbc
|
JitsiMeetSDK: 4e1c269aaaed8f2cb7b0fff2d3c00f08359b170e
|
||||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
JitsiWebRTC: b47805ab5668be38e7ee60e2258f49badfe8e1d0
|
||||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3
|
||||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
|
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: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
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
|
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
|
||||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc
|
PODFILE CHECKSUM: d278ce52a331dda323590121247d2046cd085ae7
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -961,7 +961,7 @@
|
|||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -1521,7 +1521,7 @@
|
|||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -1549,7 +1549,7 @@
|
|||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>NSCalendarsUsageDescription</key>
|
||||||
|
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
||||||
<key>AppGroupId</key>
|
<key>AppGroupId</key>
|
||||||
<string>group.solsynth.solian</string>
|
<string>group.solsynth.solian</string>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
|||||||
@@ -219,6 +219,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
List<PostWriteMedia> attachments = List.empty(growable: true);
|
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||||
DateTime? publishedAt, publishedUntil;
|
DateTime? publishedAt, publishedUntil;
|
||||||
SnAttachment? videoAttachment;
|
SnAttachment? videoAttachment;
|
||||||
|
String videoUrl = '';
|
||||||
|
bool videoLive = false;
|
||||||
SnPoll? poll;
|
SnPoll? poll;
|
||||||
|
|
||||||
Future<void> fetchRelatedPost(
|
Future<void> fetchRelatedPost(
|
||||||
@@ -241,7 +243,13 @@ 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;
|
if (post.body['video'] != null) {
|
||||||
|
if (post.body['video'] is String) {
|
||||||
|
videoUrl = post.body['video'];
|
||||||
|
} else {
|
||||||
|
videoAttachment = SnAttachment.fromJson(post.body['video']);
|
||||||
|
}
|
||||||
|
}
|
||||||
publishedAt = post.publishedAt;
|
publishedAt = post.publishedAt;
|
||||||
publishedUntil = post.publishedUntil;
|
publishedUntil = post.publishedUntil;
|
||||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||||
@@ -252,17 +260,23 @@ 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;
|
||||||
|
|
||||||
|
videoLive = post.body['is_live'] ?? false;
|
||||||
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;
|
||||||
@@ -438,8 +452,9 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
titleController.text = data['title'] ?? '';
|
titleController.text = data['title'] ?? '';
|
||||||
descriptionController.text = data['description'] ?? '';
|
descriptionController.text = data['description'] ?? '';
|
||||||
rewardController.text = data['reward']?.toString() ?? '';
|
rewardController.text = data['reward']?.toString() ?? '';
|
||||||
if (data['thumbnail'] != null)
|
if (data['thumbnail'] != null) {
|
||||||
thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||||
|
}
|
||||||
attachments.addAll(data['attachments']
|
attachments.addAll(data['attachments']
|
||||||
.map((ele) => PostWriteMedia(SnAttachment.fromJson(ele)))
|
.map((ele) => PostWriteMedia(SnAttachment.fromJson(ele)))
|
||||||
.cast<PostWriteMedia>());
|
.cast<PostWriteMedia>());
|
||||||
@@ -448,10 +463,12 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
visibility = data['visibility'];
|
visibility = data['visibility'];
|
||||||
visibleUsers = List.from(data['visible_users_list'] ?? []);
|
visibleUsers = List.from(data['visible_users_list'] ?? []);
|
||||||
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
|
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
|
||||||
if (data['published_at'] != null)
|
if (data['published_at'] != null) {
|
||||||
publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
||||||
if (data['published_until'] != null)
|
}
|
||||||
|
if (data['published_until'] != null) {
|
||||||
publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||||
|
}
|
||||||
replyingPost =
|
replyingPost =
|
||||||
data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||||
repostingPost =
|
repostingPost =
|
||||||
@@ -588,9 +605,11 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
||||||
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
||||||
if (reward != null) 'reward': reward,
|
if (reward != null) 'reward': reward,
|
||||||
if (videoAttachment != null) 'video': videoAttachment!.rid,
|
if (videoAttachment != null || videoUrl.isNotEmpty)
|
||||||
|
'video': videoUrl.isNotEmpty ? videoUrl : videoAttachment!.rid,
|
||||||
if (poll != null) 'poll': poll!.id,
|
if (poll != null) 'poll': poll!.id,
|
||||||
if (realm != null) 'realm': realm!.id,
|
if (realm != null) 'realm': realm!.id,
|
||||||
|
if (videoLive) 'is_live': videoLive,
|
||||||
'is_draft': saveAsDraft,
|
'is_draft': saveAsDraft,
|
||||||
},
|
},
|
||||||
onSendProgress: (count, total) {
|
onSendProgress: (count, total) {
|
||||||
@@ -731,6 +750,15 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setVideoUrl(String value) {
|
||||||
|
videoUrl = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setVideoLive(bool value) {
|
||||||
|
videoLive = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void setPoll(SnPoll? value) {
|
void setPoll(SnPoll? value) {
|
||||||
poll = value;
|
poll = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
438
lib/main.dart
438
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';
|
||||||
@@ -25,7 +26,6 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/firebase_options.dart';
|
import 'package:surface/firebase_options.dart';
|
||||||
import 'package:surface/logger.dart';
|
import 'package:surface/logger.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/chat_call.dart';
|
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/database.dart';
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/keypair.dart';
|
import 'package:surface/providers/keypair.dart';
|
||||||
@@ -49,6 +49,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 +58,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 +77,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 +120,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 +163,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 +187,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)),
|
||||||
@@ -171,7 +197,6 @@ class SolianApp extends StatelessWidget {
|
|||||||
Provider(create: (ctx) => KeyPairProvider(ctx)),
|
Provider(create: (ctx) => KeyPairProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
|
|
||||||
Provider(create: (ctx) => SnTranslator()),
|
Provider(create: (ctx) => SnTranslator()),
|
||||||
|
|
||||||
// Additional helper layer
|
// Additional helper layer
|
||||||
@@ -233,6 +258,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 {
|
||||||
@@ -264,11 +290,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
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 +307,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,28 +333,36 @@ 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();
|
||||||
|
try {
|
||||||
|
if (!mounted) return;
|
||||||
|
_initPercentage = 0.9;
|
||||||
|
_setPhaseText('keyPair');
|
||||||
|
final kp = context.read<KeyPairProvider>();
|
||||||
|
kp.reloadActive();
|
||||||
|
kp.listen();
|
||||||
|
} catch (_) {}
|
||||||
|
if (ua.isAuthorized) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_setPhaseText('notification');
|
_setPhaseText('notification');
|
||||||
final notify = context.read<NotificationProvider>();
|
final notify = context.read<NotificationProvider>();
|
||||||
notify.listen();
|
notify.listen();
|
||||||
await notify.registerPushNotifications();
|
try {
|
||||||
if (!mounted) return;
|
notify.registerPushNotifications();
|
||||||
_setPhaseText('keyPair');
|
|
||||||
final kp = context.read<KeyPairProvider>();
|
|
||||||
await kp.reloadActive();
|
|
||||||
kp.listen();
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_setPhaseText('stickers');
|
_setPhaseText('stickers');
|
||||||
final sticker = context.read<SnStickerProvider>();
|
final sticker = context.read<SnStickerProvider>();
|
||||||
@@ -349,7 +379,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
_setPhaseText('chat');
|
_setPhaseText('chat');
|
||||||
final ct = context.read<ChatChannelProvider>();
|
final ct = context.read<ChatChannelProvider>();
|
||||||
await ct.refreshAvailableChannels();
|
await ct.refreshAvailableChannels();
|
||||||
|
_initPercentage = 1;
|
||||||
_setPhaseText('done');
|
_setPhaseText('done');
|
||||||
|
} catch (_) {}
|
||||||
|
_playIntro();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await context.showErrorDialog(err);
|
await context.showErrorDialog(err);
|
||||||
@@ -365,28 +399,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 +462,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,11 +473,11 @@ 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);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
_trayInitialization();
|
_trayInitialization();
|
||||||
_hotkeyInitialization();
|
_hotkeyInitialization();
|
||||||
_notifyInitialization();
|
_notifyInitialization();
|
||||||
@@ -440,7 +486,13 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
_tryRequestRating();
|
_tryRequestRating();
|
||||||
_checkForUpdate();
|
_checkForUpdate();
|
||||||
setState(() => _isBusy = false);
|
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 +501,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 +583,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 +597,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: [
|
||||||
// Generate random points
|
Container(
|
||||||
List<Offset> nodes = List.generate(
|
decoration: BoxDecoration(
|
||||||
numNodes,
|
image: DecorationImage(
|
||||||
(_) => Offset(
|
image: AssetImage('assets/icon/kanban-1st.jpg'),
|
||||||
random.nextDouble() * size.width,
|
fit: BoxFit.cover,
|
||||||
random.nextDouble() * size.height,
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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
|
return Material(
|
||||||
for (var node in nodes) {
|
key: Key('app-splash-screen-$isBusy'),
|
||||||
canvas.drawCircle(node, 4, paintNode);
|
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: 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@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(
|
||||||
|
|||||||
@@ -1,459 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:surface/providers/sn_network.dart';
|
|
||||||
import 'package:surface/types/chat.dart';
|
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
||||||
|
|
||||||
class ChatCallProvider extends ChangeNotifier {
|
|
||||||
late final SnNetworkProvider _sn;
|
|
||||||
|
|
||||||
ChatCallProvider(BuildContext context) {
|
|
||||||
_sn = context.read<SnNetworkProvider>();
|
|
||||||
}
|
|
||||||
|
|
||||||
SnChatCall? _current;
|
|
||||||
SnChannel? _channel;
|
|
||||||
|
|
||||||
bool _isReady = false;
|
|
||||||
bool _isMounted = false;
|
|
||||||
bool _isInitialized = false;
|
|
||||||
bool _isBusy = false;
|
|
||||||
|
|
||||||
String _lastDuration = '00:00:00';
|
|
||||||
Timer? _lastDurationUpdateTimer;
|
|
||||||
|
|
||||||
String? token;
|
|
||||||
String? endpoint;
|
|
||||||
|
|
||||||
StreamSubscription? hwSubscription;
|
|
||||||
List<MediaDevice> _audioInputs = [];
|
|
||||||
List<MediaDevice> _videoInputs = [];
|
|
||||||
|
|
||||||
bool _enableAudio = true;
|
|
||||||
bool _enableVideo = false;
|
|
||||||
LocalAudioTrack? _audioTrack;
|
|
||||||
LocalVideoTrack? _videoTrack;
|
|
||||||
MediaDevice? _videoDevice;
|
|
||||||
MediaDevice? _audioDevice;
|
|
||||||
|
|
||||||
late Room _room;
|
|
||||||
late EventsListener<RoomEvent> _listener;
|
|
||||||
|
|
||||||
List<ParticipantTrack> _participantTracks = [];
|
|
||||||
ParticipantTrack? _focusTrack;
|
|
||||||
|
|
||||||
// Getters for private fields
|
|
||||||
SnChatCall? get current => _current;
|
|
||||||
SnChannel? get channel => _channel;
|
|
||||||
bool get isReady => _isReady;
|
|
||||||
bool get isMounted => _isMounted;
|
|
||||||
bool get isInitialized => _isInitialized;
|
|
||||||
bool get isBusy => _isBusy;
|
|
||||||
String get lastDuration => _lastDuration;
|
|
||||||
List<MediaDevice> get audioInputs => _audioInputs;
|
|
||||||
List<MediaDevice> get videoInputs => _videoInputs;
|
|
||||||
bool get enableAudio => _enableAudio;
|
|
||||||
bool get enableVideo => _enableVideo;
|
|
||||||
LocalAudioTrack? get audioTrack => _audioTrack;
|
|
||||||
LocalVideoTrack? get videoTrack => _videoTrack;
|
|
||||||
MediaDevice? get videoDevice => _videoDevice;
|
|
||||||
MediaDevice? get audioDevice => _audioDevice;
|
|
||||||
List<ParticipantTrack> get participantTracks => _participantTracks;
|
|
||||||
ParticipantTrack? get focusTrack => _focusTrack;
|
|
||||||
Room get room => _room;
|
|
||||||
|
|
||||||
void _updateDuration() {
|
|
||||||
if (_current == null) {
|
|
||||||
_lastDuration = '00:00:00';
|
|
||||||
} else {
|
|
||||||
Duration duration = DateTime.now().difference(_current!.createdAt);
|
|
||||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
|
||||||
_lastDuration = '${twoDigits(duration.inHours)}:'
|
|
||||||
'${twoDigits(duration.inMinutes.remainder(60))}:'
|
|
||||||
'${twoDigits(duration.inSeconds.remainder(60))}';
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void enableDurationUpdater() {
|
|
||||||
_updateDuration();
|
|
||||||
_lastDurationUpdateTimer = Timer.periodic(
|
|
||||||
const Duration(seconds: 1),
|
|
||||||
(_) => _updateDuration(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void disableDurationUpdater() {
|
|
||||||
_lastDurationUpdateTimer?.cancel();
|
|
||||||
_lastDurationUpdateTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> checkPermissions() async {
|
|
||||||
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Permission.camera.request();
|
|
||||||
await Permission.microphone.request();
|
|
||||||
await Permission.bluetooth.request();
|
|
||||||
await Permission.bluetoothConnect.request();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setCall(SnChatCall call, SnChannel related) {
|
|
||||||
_current = call;
|
|
||||||
_channel = related;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<(String, String)> getRoomToken() async {
|
|
||||||
final resp = await _sn.client.post(
|
|
||||||
'/cgi/im/channels/${_channel!.keyPath}/calls/ongoing/token',
|
|
||||||
);
|
|
||||||
token = resp.data['token'];
|
|
||||||
endpoint = 'wss://${resp.data['endpoint']}';
|
|
||||||
return (token!, endpoint!);
|
|
||||||
}
|
|
||||||
|
|
||||||
void initHardware() {
|
|
||||||
if (_isReady) return;
|
|
||||||
|
|
||||||
_isReady = true;
|
|
||||||
hwSubscription = Hardware.instance.onDeviceChange.stream.listen(
|
|
||||||
_revertDevices,
|
|
||||||
);
|
|
||||||
Hardware.instance.enumerateDevices().then(_revertDevices);
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void initRoom() {
|
|
||||||
initHardware();
|
|
||||||
_room = Room(
|
|
||||||
roomOptions: const RoomOptions(
|
|
||||||
dynacast: true,
|
|
||||||
adaptiveStream: true,
|
|
||||||
defaultAudioPublishOptions: AudioPublishOptions(
|
|
||||||
name: 'call_voice',
|
|
||||||
stream: 'call_stream',
|
|
||||||
),
|
|
||||||
defaultVideoPublishOptions: VideoPublishOptions(
|
|
||||||
name: 'call_video',
|
|
||||||
stream: 'call_stream',
|
|
||||||
simulcast: true,
|
|
||||||
backupVideoCodec: BackupVideoCodec(enabled: true),
|
|
||||||
),
|
|
||||||
defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(
|
|
||||||
useiOSBroadcastExtension: true,
|
|
||||||
params: VideoParametersPresets.screenShareH1080FPS30,
|
|
||||||
),
|
|
||||||
defaultCameraCaptureOptions: CameraCaptureOptions(
|
|
||||||
maxFrameRate: 30,
|
|
||||||
params: VideoParametersPresets.h1080_169,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_listener = _room.createListener();
|
|
||||||
WakelockPlus.enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> joinRoom(String url, String token) async {
|
|
||||||
if (_isMounted) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await _room.connect(
|
|
||||||
url,
|
|
||||||
token,
|
|
||||||
fastConnectOptions: FastConnectOptions(
|
|
||||||
microphone: TrackOption(track: _audioTrack),
|
|
||||||
camera: TrackOption(track: _videoTrack),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
_isMounted = true;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupRoom() {
|
|
||||||
if (isInitialized) return;
|
|
||||||
|
|
||||||
sortParticipants();
|
|
||||||
_room.addListener(_onRoomDidUpdate);
|
|
||||||
WidgetsBindingCompatible.instance?.addPostFrameCallback(
|
|
||||||
(_) => autoPublish(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (lkPlatformIsMobile()) {
|
|
||||||
Hardware.instance.setSpeakerphoneOn(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
_isBusy = false;
|
|
||||||
_isInitialized = true;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void autoPublish() async {
|
|
||||||
try {
|
|
||||||
if (enableVideo) {
|
|
||||||
await _room.localParticipant?.setCameraEnabled(true);
|
|
||||||
}
|
|
||||||
if (enableAudio) {
|
|
||||||
await _room.localParticipant?.setMicrophoneEnabled(true);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setEnableAudio(bool value) async {
|
|
||||||
_enableAudio = value;
|
|
||||||
if (!_enableAudio) {
|
|
||||||
await _audioTrack?.stop();
|
|
||||||
_audioTrack = null;
|
|
||||||
} else {
|
|
||||||
await _changeLocalAudioTrack();
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setEnableVideo(bool value) async {
|
|
||||||
_enableVideo = value;
|
|
||||||
if (!_enableVideo) {
|
|
||||||
await _videoTrack?.stop();
|
|
||||||
_videoTrack = null;
|
|
||||||
} else {
|
|
||||||
await _changeLocalVideoTrack();
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupRoomListeners({
|
|
||||||
required Function(DisconnectReason?) onDisconnected,
|
|
||||||
}) {
|
|
||||||
_listener
|
|
||||||
..on<RoomDisconnectedEvent>((event) async {
|
|
||||||
onDisconnected(event.reason);
|
|
||||||
})
|
|
||||||
..on<ParticipantEvent>((event) => sortParticipants())
|
|
||||||
..on<LocalTrackPublishedEvent>((_) => sortParticipants())
|
|
||||||
..on<LocalTrackUnpublishedEvent>((_) => sortParticipants())
|
|
||||||
..on<TrackSubscribedEvent>((_) => sortParticipants())
|
|
||||||
..on<TrackUnsubscribedEvent>((_) => sortParticipants())
|
|
||||||
..on<ParticipantNameUpdatedEvent>((event) {
|
|
||||||
sortParticipants();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void sortParticipants() {
|
|
||||||
Map<String, ParticipantTrack> mediaTracks = {};
|
|
||||||
for (var participant in _room.remoteParticipants.values) {
|
|
||||||
mediaTracks[participant.sid] = ParticipantTrack(
|
|
||||||
participant: participant,
|
|
||||||
videoTrack: null,
|
|
||||||
isScreenShare: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (var t in participant.videoTrackPublications) {
|
|
||||||
mediaTracks[participant.sid]?.videoTrack = t.track;
|
|
||||||
mediaTracks[participant.sid]?.isScreenShare = t.isScreenShare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final newTracks = List<ParticipantTrack>.empty(growable: true);
|
|
||||||
|
|
||||||
final mediaTrackList = mediaTracks.values.toList();
|
|
||||||
mediaTrackList.sort((a, b) {
|
|
||||||
// Loudest people first
|
|
||||||
if (a.participant.isSpeaking && b.participant.isSpeaking) {
|
|
||||||
if (a.participant.audioLevel > b.participant.audioLevel) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last spoke first
|
|
||||||
final aSpokeAt = a.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
|
||||||
final bSpokeAt = b.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
|
||||||
|
|
||||||
if (aSpokeAt != bSpokeAt) {
|
|
||||||
return aSpokeAt > bSpokeAt ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has video first
|
|
||||||
if (a.participant.hasVideo != b.participant.hasVideo) {
|
|
||||||
return a.participant.hasVideo ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First joined people first
|
|
||||||
return a.participant.joinedAt.millisecondsSinceEpoch -
|
|
||||||
b.participant.joinedAt.millisecondsSinceEpoch;
|
|
||||||
});
|
|
||||||
|
|
||||||
newTracks.addAll(mediaTrackList);
|
|
||||||
|
|
||||||
if (_room.localParticipant != null) {
|
|
||||||
ParticipantTrack localTrack = ParticipantTrack(
|
|
||||||
participant: _room.localParticipant!,
|
|
||||||
videoTrack: null,
|
|
||||||
isScreenShare: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
final localParticipantTracks =
|
|
||||||
_room.localParticipant?.videoTrackPublications;
|
|
||||||
if (localParticipantTracks != null) {
|
|
||||||
for (var t in localParticipantTracks) {
|
|
||||||
localTrack.videoTrack = t.track;
|
|
||||||
localTrack.isScreenShare = t.isScreenShare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newTracks.add(localTrack);
|
|
||||||
}
|
|
||||||
|
|
||||||
_participantTracks = newTracks;
|
|
||||||
|
|
||||||
if (focusTrack != null) {
|
|
||||||
final idx = participantTracks
|
|
||||||
.indexWhere((x) => x.participant.sid == _focusTrack!.participant.sid);
|
|
||||||
if (idx == -1) {
|
|
||||||
_focusTrack = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (focusTrack == null) {
|
|
||||||
_focusTrack = participantTracks.firstOrNull;
|
|
||||||
} else {
|
|
||||||
final idx = participantTracks.indexWhere(
|
|
||||||
(x) => _focusTrack!.participant.sid == x.participant.sid,
|
|
||||||
);
|
|
||||||
if (idx > -1) {
|
|
||||||
_focusTrack = participantTracks[idx];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _changeLocalAudioTrack() async {
|
|
||||||
if (_audioTrack != null) {
|
|
||||||
await _audioTrack!.stop();
|
|
||||||
_audioTrack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_audioDevice != null) {
|
|
||||||
_audioTrack = await LocalAudioTrack.create(
|
|
||||||
AudioCaptureOptions(deviceId: _audioDevice!.deviceId),
|
|
||||||
);
|
|
||||||
await _audioTrack!.start();
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _changeLocalVideoTrack() async {
|
|
||||||
if (_videoTrack != null) {
|
|
||||||
await _videoTrack!.stop();
|
|
||||||
_videoTrack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_videoDevice != null) {
|
|
||||||
_videoTrack = await LocalVideoTrack.createCameraTrack(
|
|
||||||
CameraCaptureOptions(
|
|
||||||
deviceId: _videoDevice!.deviceId,
|
|
||||||
params: VideoParametersPresets.h1080_169,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await _videoTrack!.start();
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _revertDevices(List<MediaDevice> devices) {
|
|
||||||
_audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
|
|
||||||
_videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onRoomDidUpdate() => sortParticipants();
|
|
||||||
|
|
||||||
Future<void> changeLocalAudioTrack() async {
|
|
||||||
if (audioTrack != null) {
|
|
||||||
await audioTrack!.stop();
|
|
||||||
_audioTrack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioDevice != null) {
|
|
||||||
_audioTrack = await LocalAudioTrack.create(
|
|
||||||
AudioCaptureOptions(
|
|
||||||
deviceId: audioDevice!.deviceId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await audioTrack!.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> changeLocalVideoTrack() async {
|
|
||||||
if (videoTrack != null) {
|
|
||||||
await _videoTrack!.stop();
|
|
||||||
_videoTrack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoDevice != null) {
|
|
||||||
_videoTrack = await LocalVideoTrack.createCameraTrack(
|
|
||||||
CameraCaptureOptions(
|
|
||||||
deviceId: videoDevice!.deviceId,
|
|
||||||
params: VideoParametersPresets.h1080_169,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await videoTrack!.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void deactivateHardware() {
|
|
||||||
hwSubscription?.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void disposeRoom() {
|
|
||||||
_isBusy = false;
|
|
||||||
_isMounted = false;
|
|
||||||
_isInitialized = false;
|
|
||||||
_current = null;
|
|
||||||
_channel = null;
|
|
||||||
_room.removeListener(_onRoomDidUpdate);
|
|
||||||
_room.disconnect();
|
|
||||||
_room.dispose();
|
|
||||||
_listener.dispose();
|
|
||||||
WakelockPlus.disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
void disposeHardware() {
|
|
||||||
_isReady = false;
|
|
||||||
_audioTrack?.stop();
|
|
||||||
_audioTrack = null;
|
|
||||||
_videoTrack?.stop();
|
|
||||||
_videoTrack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setVideoDevice(MediaDevice? value) {
|
|
||||||
_videoDevice = value;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAudioDevice(MediaDevice? value) {
|
|
||||||
_audioDevice = value;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setFocusTrack(ParticipantTrack? value) {
|
|
||||||
_focusTrack = value;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setIsBusy(bool value) {
|
|
||||||
_isBusy = value;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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';
|
||||||
@@ -21,6 +20,10 @@ const kAppRealmCompactView = 'app_realm_compact_view';
|
|||||||
const kAppCustomFonts = 'app_custom_fonts';
|
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 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,
|
||||||
@@ -43,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();
|
||||||
}
|
}
|
||||||
@@ -91,6 +84,33 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
return prefs.getBool(kAppAutoTranslate) ?? false;
|
return prefs.getBool(kAppAutoTranslate) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get hideBottomNav {
|
||||||
|
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) {
|
||||||
|
prefs.setBool(kAppHideBottomNav, value);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
set autoTranslate(bool value) {
|
set autoTranslate(bool value) {
|
||||||
prefs.setBool(kAppAutoTranslate, value);
|
prefs.setBool(kAppAutoTranslate, 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;
|
||||||
@@ -25,13 +38,10 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
int? get currentIndex => _currentIndex;
|
int? get currentIndex => _currentIndex;
|
||||||
|
|
||||||
static const List<String> kShowBottomNavScreen = [
|
List<String> get showBottomNavScreen => destinations
|
||||||
'home',
|
.where((ele) => ele.isPinned)
|
||||||
'explore',
|
.map((ele) => ele.screen)
|
||||||
'account',
|
.toList();
|
||||||
'album',
|
|
||||||
'chat',
|
|
||||||
];
|
|
||||||
|
|
||||||
static const List<AppNavDestination> kAllDestination = [
|
static const List<AppNavDestination> kAllDestination = [
|
||||||
AppNavDestination(
|
AppNavDestination(
|
||||||
@@ -49,40 +59,15 @@ 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',
|
||||||
label: 'screenRealm',
|
label: 'screenRealm',
|
||||||
),
|
),
|
||||||
AppNavDestination(
|
AppNavDestination(
|
||||||
icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
|
icon: Icon(Symbols.settings, weight: 400, opticalSize: 20),
|
||||||
screen: 'news',
|
screen: 'settings',
|
||||||
label: 'screenNews',
|
label: 'screenSettings',
|
||||||
),
|
|
||||||
AppNavDestination(
|
|
||||||
icon: Icon(Symbols.emoji_emotions, weight: 400, opticalSize: 20),
|
|
||||||
screen: 'stickers',
|
|
||||||
label: 'screenStickers',
|
|
||||||
),
|
|
||||||
AppNavDestination(
|
|
||||||
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
|
||||||
screen: 'album',
|
|
||||||
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 = [
|
||||||
@@ -144,11 +129,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');
|
||||||
|
|
||||||
|
try {
|
||||||
await _sn.client.post(
|
await _sn.client.post(
|
||||||
'/cgi/id/notifications/subscription',
|
'/cgi/id/notifications/subscription',
|
||||||
data: {
|
data: {
|
||||||
'provider': provider,
|
'provider': provider,
|
||||||
'device_token': token,
|
'device_token': token,
|
||||||
'device_id': deviceUuid,
|
'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',
|
||||||
|
queryParameters: {
|
||||||
'take': take,
|
'take': take,
|
||||||
if (cursor != null) 'cursor': cursor.toUtc().millisecondsSinceEpoch,
|
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',
|
||||||
|
queryParameters: {
|
||||||
'take': take,
|
'take': take,
|
||||||
'offset': offset,
|
'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(
|
||||||
|
'/cgi/co/posts/search',
|
||||||
|
queryParameters: {
|
||||||
'take': take,
|
'take': take,
|
||||||
'offset': offset,
|
'offset': offset,
|
||||||
'probe': searchTerm,
|
'probe': searchTerm,
|
||||||
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
||||||
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
|
if (categories?.isNotEmpty ?? false)
|
||||||
});
|
'categories': categories!.join(','),
|
||||||
|
},
|
||||||
|
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),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -120,6 +120,25 @@ class SnAttachmentProvider {
|
|||||||
'webp': 'image/webp',
|
'webp': 'image/webp',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Future<SnAttachment> createWithReferenceLink(
|
||||||
|
String url,
|
||||||
|
String pool,
|
||||||
|
Map<String, dynamic>? metadata, {
|
||||||
|
String? mimetype,
|
||||||
|
}) async {
|
||||||
|
final resp = await _sn.client.post(
|
||||||
|
'/cgi/uc/attachments/referenced',
|
||||||
|
data: {
|
||||||
|
'url': url,
|
||||||
|
'pool': pool,
|
||||||
|
'metadata': metadata,
|
||||||
|
if (mimetype != null) 'mimetype': mimetype,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return SnAttachment.fromJson(resp.data);
|
||||||
|
}
|
||||||
|
|
||||||
Future<SnAttachment> directUploadOne(
|
Future<SnAttachment> directUploadOne(
|
||||||
Uint8List data,
|
Uint8List data,
|
||||||
String filename,
|
String filename,
|
||||||
@@ -311,6 +330,23 @@ class SnAttachmentProvider {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<SnAttachment> rateOne(
|
||||||
|
SnAttachment item, {
|
||||||
|
int? content,
|
||||||
|
int? quality,
|
||||||
|
}) async {
|
||||||
|
final resp = await _sn.client.put(
|
||||||
|
'/cgi/uc/attachments/${item.id}/rating',
|
||||||
|
data: {
|
||||||
|
'content_rating': content ?? item.contentRating,
|
||||||
|
'quality_rating': quality ?? item.qualityRating,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final out = SnAttachment.fromJson(resp.data);
|
||||||
|
_saveToLocal([out]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _saveToLocal(Iterable<SnAttachment> out) async {
|
Future<void> _saveToLocal(Iterable<SnAttachment> out) async {
|
||||||
for (final ele in out) {
|
for (final ele in out) {
|
||||||
if (!ele.isAnalyzed || ele.destination == 0) continue;
|
if (!ele.isAnalyzed || ele.destination == 0) continue;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
145
lib/router.dart
145
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';
|
||||||
@@ -21,7 +22,6 @@ import 'package:surface/screens/album.dart';
|
|||||||
import 'package:surface/screens/auth/login.dart';
|
import 'package:surface/screens/auth/login.dart';
|
||||||
import 'package:surface/screens/auth/register.dart';
|
import 'package:surface/screens/auth/register.dart';
|
||||||
import 'package:surface/screens/chat.dart';
|
import 'package:surface/screens/chat.dart';
|
||||||
import 'package:surface/screens/chat/call_room.dart';
|
|
||||||
import 'package:surface/screens/chat/channel_detail.dart';
|
import 'package:surface/screens/chat/channel_detail.dart';
|
||||||
import 'package:surface/screens/chat/manage.dart';
|
import 'package:surface/screens/chat/manage.dart';
|
||||||
import 'package:surface/screens/chat/room.dart';
|
import 'package:surface/screens/chat/room.dart';
|
||||||
@@ -29,8 +29,7 @@ import 'package:surface/screens/explore.dart';
|
|||||||
import 'package:surface/screens/friend.dart';
|
import 'package:surface/screens/friend.dart';
|
||||||
import 'package:surface/screens/home.dart';
|
import 'package:surface/screens/home.dart';
|
||||||
import 'package:surface/screens/logging.dart';
|
import 'package:surface/screens/logging.dart';
|
||||||
import 'package:surface/screens/news/news_detail.dart';
|
import 'package:surface/screens/feed/feed_detail.dart';
|
||||||
import 'package:surface/screens/news/news_list.dart';
|
|
||||||
import 'package:surface/screens/notification.dart';
|
import 'package:surface/screens/notification.dart';
|
||||||
import 'package:surface/screens/post/post_detail.dart';
|
import 'package:surface/screens/post/post_detail.dart';
|
||||||
import 'package:surface/screens/post/post_draft.dart';
|
import 'package:surface/screens/post/post_draft.dart';
|
||||||
@@ -39,6 +38,7 @@ import 'package:surface/screens/post/post_shuffle.dart';
|
|||||||
import 'package:surface/screens/post/publisher_page.dart';
|
import 'package:surface/screens/post/publisher_page.dart';
|
||||||
import 'package:surface/screens/post/post_search.dart';
|
import 'package:surface/screens/post/post_search.dart';
|
||||||
import 'package:surface/screens/realm.dart';
|
import 'package:surface/screens/realm.dart';
|
||||||
|
import 'package:surface/screens/realm/community.dart';
|
||||||
import 'package:surface/screens/realm/manage.dart';
|
import 'package:surface/screens/realm/manage.dart';
|
||||||
import 'package:surface/screens/realm/realm_detail.dart';
|
import 'package:surface/screens/realm/realm_detail.dart';
|
||||||
import 'package:surface/screens/realm/realm_discovery.dart';
|
import 'package:surface/screens/realm/realm_discovery.dart';
|
||||||
@@ -51,16 +51,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: '/',
|
||||||
@@ -69,8 +59,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',
|
||||||
@@ -108,27 +98,69 @@ 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(
|
||||||
|
path: '/pages/:id',
|
||||||
|
name: 'readerFeedDetail',
|
||||||
|
builder: (context, state) => ReaderPageScreen(
|
||||||
|
id: state.pathParameters['id']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
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?,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
ShellRoute(
|
||||||
|
builder: (context, state, child) => ResponsiveScaffold(
|
||||||
|
aside: const AccountScreen(),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/account',
|
path: '/account',
|
||||||
name: 'account',
|
name: 'account',
|
||||||
builder: (context, state) => const AccountScreen(),
|
builder: (context, state) =>
|
||||||
|
const ResponsiveScaffoldLanding(child: AccountScreen()),
|
||||||
routes: [
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/punishments',
|
||||||
|
name: 'accountPunishments',
|
||||||
|
builder: (context, state) => const PunishmentsScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/programs',
|
||||||
|
name: 'accountProgram',
|
||||||
|
builder: (context, state) => const AccountProgramScreen(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/contacts',
|
path: '/contacts',
|
||||||
name: 'accountContactMethods',
|
name: 'accountContactMethods',
|
||||||
@@ -203,37 +235,40 @@ final _appRoutes = [
|
|||||||
name: state.pathParameters['name']!,
|
name: state.pathParameters['name']!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/profile/:name',
|
path: '/accounts/:name',
|
||||||
name: 'accountProfilePage',
|
name: 'accountProfilePage',
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
pageBuilder: (context, state) => NoTransitionPage(
|
||||||
child: UserScreen(name: state.pathParameters['name']!),
|
child: UserScreen(name: state.pathParameters['name']!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
ShellRoute(
|
||||||
),
|
builder: (context, state, child) =>
|
||||||
|
ResponsiveScaffold(aside: const ChatScreen(), child: child),
|
||||||
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/chat',
|
path: '/chat',
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
builder: (context, state) => const ChatScreen(),
|
builder: (context, state) => const ResponsiveScaffoldLanding(
|
||||||
|
child: ChatScreen(),
|
||||||
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/:scope/:alias',
|
path: '/:scope/:alias',
|
||||||
name: 'chatRoom',
|
name: 'chatRoom',
|
||||||
builder: (context, state) => ChatRoomScreen(
|
builder: (context, state) => ChatRoomScreen(
|
||||||
|
key: ValueKey(
|
||||||
|
'${state.pathParameters['scope']!}:${state.pathParameters['alias']!}',
|
||||||
|
),
|
||||||
scope: state.pathParameters['scope']!,
|
scope: state.pathParameters['scope']!,
|
||||||
alias: state.pathParameters['alias']!,
|
alias: state.pathParameters['alias']!,
|
||||||
extra: state.extra as ChatRoomScreenExtra?,
|
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(
|
GoRoute(
|
||||||
path: '/:scope/:alias/detail',
|
path: '/:scope/:alias/detail',
|
||||||
name: 'channelDetail',
|
name: 'channelDetail',
|
||||||
@@ -251,14 +286,20 @@ final _appRoutes = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
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(
|
||||||
|
path: '/:alias/community',
|
||||||
|
name: 'realmCommunity',
|
||||||
|
builder: (context, state) => RealmCommunityScreen(
|
||||||
|
alias: state.pathParameters['alias']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/manage',
|
path: '/manage',
|
||||||
name: 'realmManage',
|
name: 'realmManage',
|
||||||
@@ -279,20 +320,6 @@ final _appRoutes = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
path: '/news',
|
|
||||||
name: 'news',
|
|
||||||
builder: (context, state) => const NewsScreen(),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: '/:hash',
|
|
||||||
name: 'newsDetail',
|
|
||||||
builder: (context, state) => NewsDetailScreen(
|
|
||||||
hash: state.pathParameters['hash']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/stickers',
|
path: '/stickers',
|
||||||
name: 'stickers',
|
name: 'stickers',
|
||||||
@@ -363,4 +390,10 @@ final appRouter = GoRouter(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
onException: (context, state, router) {
|
||||||
|
if (state.error is GoException) {
|
||||||
|
router.goNamed('/');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigatorKey: GlobalKey(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,99 +219,25 @@ 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(),
|
||||||
|
child: ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
title: Text(item.title).tr(),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Symbols.face),
|
leading: Icon(item.icon),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed('accountPublishers');
|
GoRouter.of(context).pushNamed(item.screen);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
|
||||||
title: Text('abuseReport').tr(),
|
|
||||||
subtitle: Text('abuseReportActionDescription').tr(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
leading: const Icon(Symbols.flag),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed('abuseReport');
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
Tooltip(
|
||||||
title: Text('factorSettings').tr(),
|
message: 'accountLogoutSubtitle'.tr(),
|
||||||
subtitle: Text('factorSettingsSubtitle').tr(),
|
child: ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
leading: const Icon(Symbols.lock),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed('factorSettings');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text('accountWallet').tr(),
|
|
||||||
subtitle: Text('accountWalletSubtitle').tr(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
leading: const Icon(Symbols.wallet),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed('accountWallet');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text('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(),
|
title: Text('accountLogout').tr(),
|
||||||
subtitle: Text('accountLogoutSubtitle').tr(),
|
minTileHeight: 48,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Symbols.logout),
|
leading: const Icon(Symbols.logout),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
@@ -257,6 +255,7 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
|||||||
context.read<DatabaseProvider>().removeDatabase();
|
context.read<DatabaseProvider>().removeDatabase();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -295,14 +294,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
|
|||||||
leading: const Icon(Symbols.login),
|
leading: const Icon(Symbols.login),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed('authLogin').then((value) {
|
GoRouter.of(context).pushNamed('authLogin');
|
||||||
if (value == true && context.mounted) {
|
|
||||||
final ua = context.read<UserProvider>();
|
|
||||||
context.showSnackbar('loginSuccess'.tr(args: [
|
|
||||||
'@${ua.user?.name} (${ua.user?.nick})',
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
@@ -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,21 +66,23 @@ 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: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
margin:
|
||||||
|
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,
|
||||||
@@ -91,7 +93,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
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,11 +112,12 @@ 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)];
|
||||||
|
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||||
? await showCupertinoImageCropper(
|
? await showCupertinoImageCropper(
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
context,
|
context,
|
||||||
@@ -131,7 +135,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
|||||||
|
|
||||||
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,12 +263,15 @@ 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
|
||||||
|
? AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(_banner!),
|
||||||
|
fit: BoxFit.cover)
|
||||||
: const SizedBox.shrink(),
|
: 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,12 +504,13 @@ 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),
|
||||||
|
if (_account!.badges.isNotEmpty)
|
||||||
Wrap(
|
Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
children: _account!.badges
|
children: _account!.badges
|
||||||
.map(
|
.map((ele) => AccountBadge(badge: ele))
|
||||||
(ele) => AccountBadge(badge: ele),
|
|
||||||
)
|
|
||||||
.toList(),
|
.toList(),
|
||||||
).padding(horizontal: 8),
|
).padding(horizontal: 8),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@@ -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,11 +117,12 @@ 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)];
|
||||||
|
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||||
? await showCupertinoImageCropper(
|
? await showCupertinoImageCropper(
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
context,
|
context,
|
||||||
@@ -137,7 +140,9 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
|||||||
|
|
||||||
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,12 +214,15 @@ 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
|
||||||
|
? AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(_banner!),
|
||||||
|
fit: BoxFit.cover)
|
||||||
: const SizedBox.shrink(),
|
: 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(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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,15 +102,14 @@ 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: [
|
|
||||||
SliverAppBar(
|
|
||||||
leading: AutoAppBarLeading(),
|
|
||||||
title: Text('screenAlbum').tr(),
|
title: Text('screenAlbum').tr(),
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
body: Column(
|
||||||
child: Card(
|
children: [
|
||||||
|
Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -119,7 +118,8 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
|||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value: _billing?.includedRatio ?? 0,
|
value: _billing?.includedRatio ?? 0,
|
||||||
strokeWidth: 8,
|
strokeWidth: 8,
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
),
|
),
|
||||||
).padding(all: 12),
|
).padding(all: 12),
|
||||||
const Gap(24),
|
const Gap(24),
|
||||||
@@ -149,47 +149,82 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 24, vertical: 8),
|
).padding(horizontal: 24, vertical: 8),
|
||||||
),
|
).padding(horizontal: 8, top: 8),
|
||||||
),
|
Expanded(
|
||||||
SliverMasonryGrid.extent(
|
child: InfiniteList(
|
||||||
childCount: _attachments.length,
|
padding: EdgeInsets.only(top: 8),
|
||||||
maxCrossAxisExtent: 320,
|
itemCount: _attachments.length,
|
||||||
mainAxisSpacing: 4,
|
isLoading: _isBusy,
|
||||||
crossAxisSpacing: 4,
|
hasReachedMax:
|
||||||
itemBuilder: (context, idx) {
|
_totalCount != null && _attachments.length >= _totalCount!,
|
||||||
final attachment = _attachments[idx];
|
onFetchData: _fetchAttachments,
|
||||||
return GestureDetector(
|
itemBuilder: (context, index) {
|
||||||
child: ClipRRect(
|
final ele = _attachments[index];
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: attachment.metadata['ratio']?.toDouble() ?? 1,
|
aspectRatio: (ele.data['ratio'] ?? 1).toDouble(),
|
||||||
child: AttachmentItem(
|
child: AttachmentItem(
|
||||||
data: attachment,
|
data: ele,
|
||||||
heroTag: _heroTags[idx],
|
heroTag: _heroTags[index],
|
||||||
),
|
onZoom: () {
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
AttachmentZoomView(
|
AttachmentZoomView(
|
||||||
data: [attachment],
|
data: [ele],
|
||||||
heroTags: [_heroTags[idx]],
|
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.black.withOpacity(0.7),
|
backgroundColor: Colors.black.withOpacity(0.7),
|
||||||
rootNavigator: true,
|
rootNavigator: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: 12, right: 12, top: 4),
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Symbols.info),
|
||||||
|
onPressed: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AttachmentZoomDetailPopup(
|
||||||
|
data: ele,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (_isBusy)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
child: const CircularProgressIndicator(),
|
|
||||||
).center(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) => const Gap(8),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
|
|||||||
sn.setTokenPair(atk, rtk);
|
sn.setTokenPair(atk, rtk);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final user = context.read<UserProvider>();
|
final user = context.read<UserProvider>();
|
||||||
|
user.isAuthorized = true;
|
||||||
await user.refreshUser();
|
await user.refreshUser();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final ws = context.read<WebSocketProvider>();
|
final ws = context.read<WebSocketProvider>();
|
||||||
|
|||||||
@@ -7,6 +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/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';
|
||||||
@@ -33,10 +34,20 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
final username = _usernameController.value.text;
|
final username = _usernameController.value.text;
|
||||||
final nickname = _nicknameController.value.text;
|
final nickname = _nicknameController.value.text;
|
||||||
final password = _passwordController.value.text;
|
final password = _passwordController.value.text;
|
||||||
if (email.isEmpty || username.isEmpty || nickname.isEmpty || password.isEmpty) {
|
if (email.isEmpty ||
|
||||||
|
username.isEmpty ||
|
||||||
|
nickname.isEmpty ||
|
||||||
|
password.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final captchaTk = await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => CaptchaScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (captchaTk == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.post('/cgi/id/users', data: {
|
await sn.client.post('/cgi/id/users', data: {
|
||||||
@@ -45,6 +56,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
'email': email,
|
'email': email,
|
||||||
'password': password,
|
'password': password,
|
||||||
'language': EasyLocalization.of(context)!.currentLocale.toString(),
|
'language': EasyLocalization.of(context)!.currentLocale.toString(),
|
||||||
|
'captcha_token': captchaTk,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@@ -91,8 +103,11 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.length < 4 || value.length > 32) {
|
if (value == null ||
|
||||||
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
value.length < 4 ||
|
||||||
|
value.length > 32) {
|
||||||
|
return 'fieldUsernameLengthLimit'
|
||||||
|
.tr(args: [4.toString(), 32.toString()]);
|
||||||
}
|
}
|
||||||
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
||||||
return 'fieldUsernameAlphanumOnly'.tr();
|
return 'fieldUsernameAlphanumOnly'.tr();
|
||||||
@@ -108,13 +123,17 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldUsername'.tr(),
|
labelText: 'fieldUsername'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.length < 4 || value.length > 32) {
|
if (value == null ||
|
||||||
return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
value.length < 4 ||
|
||||||
|
value.length > 32) {
|
||||||
|
return 'fieldNicknameLengthLimit'
|
||||||
|
.tr(args: [4.toString(), 32.toString()]);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -127,7 +146,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldNickname'.tr(),
|
labelText: 'fieldNickname'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -149,7 +169,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldEmail'.tr(),
|
labelText: 'fieldEmail'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -169,7 +190,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldPassword'.tr(),
|
labelText: 'fieldPassword'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 7),
|
).padding(horizontal: 7),
|
||||||
@@ -186,8 +208,12 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
Text(
|
Text(
|
||||||
'termAcceptNextWithAgree'.tr(),
|
'termAcceptNextWithAgree'.tr(),
|
||||||
textAlign: TextAlign.end,
|
textAlign: TextAlign.end,
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style:
|
||||||
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha((255 * 0.75).round()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Material(
|
Material(
|
||||||
|
|||||||
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';
|
||||||
37
lib/screens/captcha/captcha_native.dart
Normal file
37
lib/screens/captcha/captcha_native.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.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
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(title: Text("reCaptcha").tr()),
|
||||||
|
body: InAppWebView(
|
||||||
|
initialUrlRequest: URLRequest(
|
||||||
|
url: WebUri('${cfg.serverUrl}/captcha?redirect_uri=solink://captcha'),
|
||||||
|
),
|
||||||
|
shouldOverrideUrlLoading: (controller, navigationAction) async {
|
||||||
|
Uri? url = navigationAction.request.url;
|
||||||
|
if (url != null && url.queryParameters.containsKey('captcha_tk')) {
|
||||||
|
Navigator.pop(context, url.queryParameters['captcha_tk']!);
|
||||||
|
return NavigationActionPolicy.CANCEL;
|
||||||
|
}
|
||||||
|
return NavigationActionPolicy.ALLOW;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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,22 +164,38 @@ 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);
|
||||||
}
|
}
|
||||||
|
if (channel.type == 1) {
|
||||||
|
_unreadCountsGrouped?[0] =
|
||||||
|
(_unreadCountsGrouped?[0] ?? 0) - (_unreadCounts?[channel.id] ?? 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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(
|
GoRouter.of(context).pushNamed(
|
||||||
'chatRoom',
|
'chatRoom',
|
||||||
pathParameters: {
|
pathParameters: {
|
||||||
@@ -153,17 +203,21 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
'alias': channel.alias,
|
'alias': channel.alias,
|
||||||
},
|
},
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (mounted) {
|
if (mounted && value == true) {
|
||||||
_unreadCounts?[channel.id] = 0;
|
_refreshChannels();
|
||||||
setState(() => _unreadCounts?[channel.id] = 0);
|
|
||||||
_refreshChannels(noRemote: true);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,65 +300,199 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
LoadingIndicator(isActive: _isBusy),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
if (_channels != null && ResponsiveScaffold.getIsExpand(context))
|
||||||
Expanded(
|
Expanded(
|
||||||
child: MediaQuery.removePadding(
|
|
||||||
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,
|
|
||||||
itemBuilder: (context, idx) {
|
|
||||||
final channel = _channels![idx];
|
|
||||||
final lastMessage = _lastMessages?[channel.id];
|
|
||||||
|
|
||||||
return _ChatChannelEntry(
|
|
||||||
channel: channel,
|
|
||||||
lastMessage: lastMessage,
|
|
||||||
unreadCount: _unreadCounts?[channel.id],
|
|
||||||
onTap: () {
|
|
||||||
if (doExpand) {
|
|
||||||
_unreadCounts?[channel.id] = 0;
|
|
||||||
setState(() => _focusChannel = channel);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_onTapChannel(channel);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (doExpand) {
|
|
||||||
return AppBackground(
|
|
||||||
isRoot: true,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
SizedBox(width: 340, child: chatList),
|
ListTile(
|
||||||
const VerticalDivider(width: 1),
|
minTileHeight: 48,
|
||||||
if (_focusChannel != null)
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final directChatList = ListView(
|
||||||
|
key: Key('direct-chat-list-view'),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
leading: const Icon(Symbols.arrow_left_alt),
|
||||||
|
contentPadding: EdgeInsets.only(left: 24),
|
||||||
|
title: Text('back').tr(),
|
||||||
|
onTap: () {
|
||||||
|
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(
|
Expanded(
|
||||||
child: ChatRoomScreen(
|
child: RefreshIndicator(
|
||||||
key: ValueKey(_focusChannel!.id),
|
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||||
scope: _focusChannel!.realm?.alias ?? 'global',
|
child: ListView(
|
||||||
alias: _focusChannel!.alias,
|
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),
|
||||||
|
);
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return chatList;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatChannelEntry extends StatelessWidget {
|
class _ChatChannelEntry extends StatelessWidget {
|
||||||
@@ -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(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,301 +0,0 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart' as livekit;
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
import 'package:surface/providers/chat_call.dart';
|
|
||||||
import 'package:surface/widgets/chat/call/call_controls.dart';
|
|
||||||
import 'package:surface/widgets/chat/call/call_participant.dart';
|
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
|
||||||
|
|
||||||
class CallRoomScreen extends StatefulWidget {
|
|
||||||
final String scope;
|
|
||||||
final String alias;
|
|
||||||
|
|
||||||
const CallRoomScreen({super.key, required this.scope, required this.alias});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CallRoomScreen> createState() => _CallRoomScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CallRoomScreenState extends State<CallRoomScreen> {
|
|
||||||
int _layoutMode = 0;
|
|
||||||
|
|
||||||
void _switchLayout() {
|
|
||||||
if (_layoutMode < 1) {
|
|
||||||
setState(() => _layoutMode++);
|
|
||||||
} else {
|
|
||||||
setState(() => _layoutMode = 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildListLayout() {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
|
||||||
child: call.focusTrack != null
|
|
||||||
? InteractiveParticipantWidget(
|
|
||||||
isFixedAvatar: false,
|
|
||||||
participant: call.focusTrack!,
|
|
||||||
onTap: () {},
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 128,
|
|
||||||
child: ListView.builder(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: math.max(0, call.participantTracks.length),
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
final track = call.participantTracks[index];
|
|
||||||
if (track.participant.sid == call.focusTrack?.participant.sid) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8, left: 8),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: InteractiveParticipantWidget(
|
|
||||||
isFixedAvatar: true,
|
|
||||||
width: 120,
|
|
||||||
height: 120,
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
participant: track,
|
|
||||||
onTap: () {
|
|
||||||
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
|
||||||
call.setFocusTrack(track);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildGridLayout() {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
|
|
||||||
return LayoutBuilder(builder: (context, constraints) {
|
|
||||||
double screenWidth = constraints.maxWidth;
|
|
||||||
double screenHeight = constraints.maxHeight;
|
|
||||||
|
|
||||||
int columns = (math.sqrt(call.participantTracks.length)).ceil();
|
|
||||||
int rows = (call.participantTracks.length / columns).ceil();
|
|
||||||
|
|
||||||
double tileWidth = screenWidth / columns;
|
|
||||||
double tileHeight = screenHeight / rows;
|
|
||||||
|
|
||||||
return StyledWidget(GridView.builder(
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
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
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
|
|
||||||
Future.delayed(Duration.zero, () {
|
|
||||||
call
|
|
||||||
..setupRoom()
|
|
||||||
..enableDurationUpdater();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
|
|
||||||
return ListenableBuilder(
|
|
||||||
listenable: call,
|
|
||||||
builder: (context, _) {
|
|
||||||
return AppScaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: RichText(
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
text: TextSpan(children: [
|
|
||||||
TextSpan(
|
|
||||||
text: 'call'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const TextSpan(text: '\n'),
|
|
||||||
TextSpan(
|
|
||||||
text: call.lastDuration.toString(),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: GestureDetector(
|
|
||||||
behavior: HitTestBehavior.translucent,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
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(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)
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
child: ControlsWidget(
|
|
||||||
call.room,
|
|
||||||
call.room.localParticipant!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void deactivate() {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
call.disableDurationUpdater();
|
|
||||||
super.deactivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void activate() {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
call.enableDurationUpdater();
|
|
||||||
super.activate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:jitsi_meet_flutter_sdk/jitsi_meet_flutter_sdk.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/controllers/chat_message_controller.dart';
|
import 'package:surface/controllers/chat_message_controller.dart';
|
||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/chat_call.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/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
@@ -21,13 +22,13 @@ import 'package:surface/providers/userinfo.dart';
|
|||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/types/websocket.dart';
|
import 'package:surface/types/websocket.dart';
|
||||||
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
|
||||||
import 'package:surface/widgets/chat/chat_message.dart';
|
import 'package:surface/widgets/chat/chat_message.dart';
|
||||||
import 'package:surface/widgets/chat/chat_message_input.dart';
|
import 'package:surface/widgets/chat/chat_message_input.dart';
|
||||||
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
|
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class ChatRoomScreenExtra {
|
class ChatRoomScreenExtra {
|
||||||
@@ -51,13 +52,11 @@ class ChatRoomScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
bool _isCalling = false;
|
|
||||||
bool _isJoining = false;
|
bool _isJoining = false;
|
||||||
|
|
||||||
SnChannel? _channel;
|
SnChannel? _channel;
|
||||||
SnChannelMember? _currentMember;
|
SnChannelMember? _currentMember;
|
||||||
SnChannelMember? _otherMember;
|
SnChannelMember? _otherMember;
|
||||||
SnChatCall? _ongoingCall;
|
|
||||||
|
|
||||||
final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey();
|
final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey();
|
||||||
late final ChatMessageController _messageController;
|
late final ChatMessageController _messageController;
|
||||||
@@ -139,88 +138,35 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchOngoingCall() async {
|
Future<void> _joinCall() async {
|
||||||
setState(() => _isCalling = true);
|
if (kIsWeb || !(Platform.isIOS || Platform.isAndroid)) {
|
||||||
|
return await _joinCallWeb();
|
||||||
try {
|
}
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get(
|
final ua = context.read<UserProvider>();
|
||||||
'/cgi/im/channels/${_messageController.channel!.keyPath}/calls/ongoing',
|
final meet = JitsiMeet();
|
||||||
options: Options(
|
final confOpts = JitsiMeetConferenceOptions(
|
||||||
validateStatus: (status) => status != null && status < 500,
|
room: 'sn-chat-${_channel!.alias}-${_channel!.id}',
|
||||||
receiveTimeout: const Duration(seconds: 60),
|
serverURL: 'https://meet.element.io',
|
||||||
sendTimeout: const Duration(seconds: 60),
|
configOverrides: {
|
||||||
),
|
"subject": _channel!.name,
|
||||||
);
|
|
||||||
if (resp.statusCode == 200) {
|
|
||||||
_ongoingCall = SnChatCall.fromJson(resp.data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isCalling = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _makeCall() async {
|
|
||||||
setState(() => _isCalling = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client.post(
|
|
||||||
'/cgi/im/channels/${_messageController.channel!.keyPath}/calls',
|
|
||||||
options: Options(
|
|
||||||
sendTimeout: const Duration(seconds: 30),
|
|
||||||
receiveTimeout: const Duration(seconds: 30),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
if (_ongoingCall == null) {
|
|
||||||
// ignore the error because the call is already ongoing
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setState(() => _isCalling = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _endCall() async {
|
|
||||||
setState(() => _isCalling = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client.delete(
|
|
||||||
'/cgi/im/channels/${_messageController.channel!.keyPath}/calls/ongoing',
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isCalling = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onCallJoin() async {
|
|
||||||
await showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ChatCallPrejoinPopup(
|
|
||||||
ongoingCall: _ongoingCall!,
|
|
||||||
channel: _channel!,
|
|
||||||
onJoin: _onCallResume,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onCallResume() {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'chatCallRoom',
|
|
||||||
pathParameters: {
|
|
||||||
'scope': _channel!.realm?.alias ?? 'global',
|
|
||||||
'alias': _channel!.alias,
|
|
||||||
},
|
},
|
||||||
|
userInfo: JitsiMeetUserInfo(
|
||||||
|
avatar: ua.user!.avatar.isNotEmpty
|
||||||
|
? sn.getAttachmentUrl(ua.user!.avatar)
|
||||||
|
: null,
|
||||||
|
displayName: _currentMember!.nick ?? ua.user!.nick,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
meet.join(confOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _joinCallWeb() async {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
final url =
|
||||||
|
'${sn.client.options.baseUrl}/meet/${_channel!.alias}-${_channel!.id}?tk=${await ua.atk}';
|
||||||
|
launchUrlString(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) {
|
bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) {
|
||||||
@@ -248,10 +194,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await Future.wait([
|
await _messageController.checkUpdate();
|
||||||
_messageController.checkUpdate(),
|
|
||||||
_fetchOngoingCall(),
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,23 +203,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_messageController = ChatMessageController(context);
|
_messageController = ChatMessageController(context);
|
||||||
_initializeChat();
|
_initializeChat();
|
||||||
|
|
||||||
_wsSubscription = _ws.pk.stream.listen((event) {
|
|
||||||
switch (event.method) {
|
|
||||||
case 'calls.new':
|
|
||||||
final payload = SnChatCall.fromJson(event.payload!);
|
|
||||||
if (payload.channelId == _channel?.id) {
|
|
||||||
setState(() => _ongoingCall = payload);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'calls.end':
|
|
||||||
final payload = SnChatCall.fromJson(event.payload!);
|
|
||||||
if (payload.channelId == _channel?.id) {
|
|
||||||
setState(() => _ongoingCall = null);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -300,10 +226,10 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final call = context.watch<ChatCallProvider>();
|
|
||||||
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
|
||||||
@@ -323,14 +249,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
),
|
),
|
||||||
if (_currentMember != null)
|
if (_currentMember != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: _ongoingCall == null
|
icon: const Icon(Symbols.video_call),
|
||||||
? const Icon(Symbols.call)
|
onPressed: _joinCall,
|
||||||
: const Icon(Symbols.call_end),
|
onLongPress: _joinCallWeb,
|
||||||
onPressed: _isCalling
|
|
||||||
? null
|
|
||||||
: _ongoingCall == null
|
|
||||||
? _makeCall
|
|
||||||
: _endCall,
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.more_vert),
|
icon: const Icon(Symbols.more_vert),
|
||||||
@@ -358,28 +279,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
LoadingIndicator(
|
LoadingIndicator(
|
||||||
isActive: _isBusy || _messageController.isAggressiveLoading,
|
isActive: _isBusy || _messageController.isAggressiveLoading,
|
||||||
),
|
),
|
||||||
SingleChildScrollView(
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
child: MaterialBanner(
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
leading: const Icon(Symbols.call_received),
|
|
||||||
content: Text('callOngoingNotice').tr().padding(top: 2),
|
|
||||||
actions: [
|
|
||||||
if (call.current == null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: _onCallJoin,
|
|
||||||
child: Text('callJoin').tr(),
|
|
||||||
)
|
|
||||||
else if (call.current?.channelId == _channel?.id)
|
|
||||||
TextButton(
|
|
||||||
onPressed: _onCallResume,
|
|
||||||
child: Text('callResume').tr(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
).height(_ongoingCall != null ? 54 : 0, animate: true).animate(
|
|
||||||
const Duration(milliseconds: 300),
|
|
||||||
Curves.fastLinearToSlowEaseIn),
|
|
||||||
if (_currentMember == null && !_isBusy)
|
if (_currentMember == null && !_isBusy)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
|
|||||||
@@ -17,10 +17,9 @@ 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/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/feed/feed_news.dart';
|
import 'package:surface/widgets/feed/feed_reader.dart';
|
||||||
import 'package:surface/widgets/feed/feed_unknown.dart';
|
import 'package:surface/widgets/feed/feed_unknown.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/post/fediverse_post_item.dart';
|
|
||||||
import 'package:surface/widgets/post/post_item.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';
|
||||||
|
|
||||||
@@ -157,6 +156,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 +243,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(
|
||||||
@@ -463,7 +465,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
final pt = context.read<SnPostContentProvider>();
|
final pt = context.read<SnPostContentProvider>();
|
||||||
final result = await pt.getFeed(
|
final result = await pt.getFeed(
|
||||||
cursor: _feed
|
cursor: _feed
|
||||||
.where((ele) => !['reader.news'].contains(ele.type))
|
.where((ele) => !['reader.feed'].contains(ele.type))
|
||||||
.lastOrNull
|
.lastOrNull
|
||||||
?.createdAt,
|
?.createdAt,
|
||||||
);
|
);
|
||||||
@@ -534,6 +536,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) {
|
||||||
@@ -545,12 +548,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
refreshPosts();
|
refreshPosts();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
case 'fediverse.post':
|
case 'reader.feed':
|
||||||
return FediversePostWidget(
|
|
||||||
data: SnFediversePost.fromJson(ele.data),
|
|
||||||
maxWidth: 640,
|
|
||||||
);
|
|
||||||
case 'reader.news':
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
|||||||
@@ -14,23 +14,23 @@ import 'package:surface/widgets/navigation/app_scaffold.dart';
|
|||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class NewsDetailScreen extends StatefulWidget {
|
class ReaderPageScreen extends StatefulWidget {
|
||||||
final String hash;
|
final String id;
|
||||||
|
|
||||||
const NewsDetailScreen({super.key, required this.hash});
|
const ReaderPageScreen({super.key, required this.id});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NewsDetailScreen> createState() => _NewsDetailScreenState();
|
State<ReaderPageScreen> createState() => _ReaderPageScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
class _ReaderPageScreenState extends State<ReaderPageScreen> {
|
||||||
SnNewsArticle? _article;
|
SnSubscriptionItem? _article;
|
||||||
|
|
||||||
Future<void> _fetchArticle() async {
|
Future<void> _fetchArticle() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
|
final resp = await sn.client.get('/cgi/re/subscriptions/${widget.id}');
|
||||||
_article = SnNewsArticle.fromJson(resp.data);
|
_article = SnSubscriptionItem.fromJson(resp.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err).then((_) {
|
context.showErrorDialog(err).then((_) {
|
||||||
@@ -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;
|
||||||
@@ -147,8 +139,10 @@ 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();
|
||||||
@@ -159,8 +153,9 @@ 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,6 +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/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';
|
||||||
@@ -395,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]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -508,11 +518,20 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _doCheckIn() async {
|
Future<void> _doCheckIn() async {
|
||||||
|
final captchaTk = await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => CaptchaScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (captchaTk == null) return;
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final home = context.read<HomeWidgetProvider>();
|
final home = context.read<HomeWidgetProvider>();
|
||||||
final resp = await sn.client.post('/cgi/id/check-in');
|
final resp = await sn.client.post('/cgi/id/check-in', data: {
|
||||||
|
'captcha_token': captchaTk,
|
||||||
|
});
|
||||||
_todayRecord = SnCheckInRecord.fromJson(resp.data);
|
_todayRecord = SnCheckInRecord.fromJson(resp.data);
|
||||||
await home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
|
await home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -796,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');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
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:html/parser.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:relative_time/relative_time.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
import 'package:surface/providers/sn_network.dart';
|
|
||||||
import 'package:surface/types/news.dart';
|
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
|
||||||
import 'package:surface/widgets/dialog.dart';
|
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
|
||||||
|
|
||||||
class NewsScreen extends StatefulWidget {
|
|
||||||
const NewsScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<NewsScreen> createState() => _NewsScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NewsScreenState extends State<NewsScreen> {
|
|
||||||
List<SnNewsSource>? _sources;
|
|
||||||
|
|
||||||
@override
|
|
||||||
initState() {
|
|
||||||
super.initState();
|
|
||||||
_fetchSources();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _fetchSources() async {
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
final resp = await sn.client.get('/cgi/re/well-known/sources');
|
|
||||||
_sources = List<SnNewsSource>.from(
|
|
||||||
resp.data?.map((e) => SnNewsSource.fromJson(e)) ?? [],
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (_sources == null) {
|
|
||||||
return AppScaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: AutoAppBarLeading(),
|
|
||||||
title: Text('screenNews').tr(),
|
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DefaultTabController(
|
|
||||||
length: _sources!.length + 1,
|
|
||||||
child: AppScaffold(
|
|
||||||
body: NestedScrollView(
|
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
|
||||||
return <Widget>[
|
|
||||||
SliverOverlapAbsorber(
|
|
||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
|
||||||
sliver: SliverAppBar(
|
|
||||||
leading: AutoAppBarLeading(),
|
|
||||||
title: Text('screenNews').tr(),
|
|
||||||
floating: true,
|
|
||||||
snap: true,
|
|
||||||
bottom: TabBar(
|
|
||||||
isScrollable: true,
|
|
||||||
tabs: [
|
|
||||||
Tab(child: Text('newsAllSources'.tr()).textColor(Theme.of(context).appBarTheme.foregroundColor)),
|
|
||||||
for (final source in _sources!)
|
|
||||||
Tab(
|
|
||||||
child: Text(source.label).textColor(Theme.of(context).appBarTheme.foregroundColor),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
body: TabBarView(
|
|
||||||
children: [
|
|
||||||
_NewsArticleListWidget(allSources: _sources!),
|
|
||||||
for (final source in _sources!)
|
|
||||||
_NewsArticleListWidget(
|
|
||||||
source: source.id,
|
|
||||||
allSources: _sources!,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NewsArticleListWidget extends StatefulWidget {
|
|
||||||
final String? source;
|
|
||||||
final List<SnNewsSource> allSources;
|
|
||||||
|
|
||||||
const _NewsArticleListWidget({this.source, required this.allSources});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_NewsArticleListWidget> createState() => _NewsArticleListWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
|
|
||||||
bool _isBusy = false;
|
|
||||||
|
|
||||||
int? _totalCount;
|
|
||||||
final List<SnNewsArticle> _articles = List.empty(growable: true);
|
|
||||||
|
|
||||||
Future<void> _fetchArticles() async {
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
final resp = await sn.client.get('/cgi/re/news', queryParameters: {
|
|
||||||
'take': 10,
|
|
||||||
'offset': _articles.length,
|
|
||||||
if (widget.source != null) 'source': widget.source,
|
|
||||||
});
|
|
||||||
_totalCount = resp.data['count'];
|
|
||||||
_articles.addAll(List<SnNewsArticle>.from(
|
|
||||||
resp.data['data']?.map((e) => SnNewsArticle.fromJson(e)) ?? [],
|
|
||||||
));
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isBusy = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_fetchArticles();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MediaQuery.removePadding(
|
|
||||||
context: context,
|
|
||||||
removeTop: true,
|
|
||||||
child: Center(
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
|
||||||
child: RefreshIndicator(
|
|
||||||
onRefresh: _fetchArticles,
|
|
||||||
child: InfiniteList(
|
|
||||||
isLoading: _isBusy,
|
|
||||||
itemCount: _articles.length,
|
|
||||||
hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
|
|
||||||
onFetchData: () {
|
|
||||||
_fetchArticles();
|
|
||||||
},
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final article = _articles[index];
|
|
||||||
|
|
||||||
final baseUri = Uri.parse(article.url);
|
|
||||||
final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
|
||||||
|
|
||||||
final htmlDescription = parse(article.description);
|
|
||||||
final date = article.publishedAt ?? article.createdAt;
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
child: InkWell(
|
|
||||||
radius: 8,
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'newsDetail',
|
|
||||||
pathParameters: {'hash': article.hash},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topRight: Radius.circular(8),
|
|
||||||
topLeft: Radius.circular(8),
|
|
||||||
),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
child: Container(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
child: AutoResizeUniversalImage(
|
|
||||||
article.thumbnail.startsWith('http')
|
|
||||||
? article.thumbnail
|
|
||||||
: '$baseUrl/${article.thumbnail}',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
|
|
||||||
const Gap(8),
|
|
||||||
Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
|
|
||||||
.textStyle(Theme.of(context).textTheme.bodyMedium!)
|
|
||||||
.padding(horizontal: 16),
|
|
||||||
const Gap(8),
|
|
||||||
Row(
|
|
||||||
spacing: 2,
|
|
||||||
children: [
|
|
||||||
Text(widget.allSources.where((x) => x.id == article.source).first.label)
|
|
||||||
.textStyle(Theme.of(context).textTheme.bodySmall!),
|
|
||||||
],
|
|
||||||
).opacity(0.75).padding(horizontal: 16),
|
|
||||||
Row(
|
|
||||||
spacing: 2,
|
|
||||||
children: [
|
|
||||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
|
||||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
|
||||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
|
||||||
],
|
|
||||||
).opacity(0.75).padding(horizontal: 16),
|
|
||||||
const Gap(16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,9 +65,8 @@ 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: () {
|
||||||
@@ -89,16 +87,14 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
TextSpan(
|
TextSpan(
|
||||||
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
color:
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'postDetail'.tr(),
|
text: 'postDetail'.tr(),
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
color:
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
@@ -175,7 +171,6 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_context_menu/flutter_context_menu.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:hotkey_manager/hotkey_manager.dart';
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||||
@@ -16,7 +15,6 @@ import 'package:responsive_framework/responsive_framework.dart';
|
|||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/sn_realm.dart';
|
import 'package:surface/providers/sn_realm.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
@@ -25,8 +23,7 @@ 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/attachment/attachment_input.dart';
|
import 'package:surface/widgets/attachment/attachment_input.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||||
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
|
import 'package:surface/widgets/attachment/pending_attachment_actions.dart';
|
||||||
import 'package:surface/widgets/attachment/pending_attachment_boost.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';
|
||||||
@@ -45,12 +42,14 @@ class PostEditorExtra {
|
|||||||
final String? title;
|
final String? title;
|
||||||
final String? description;
|
final String? description;
|
||||||
final List<PostWriteMedia>? attachments;
|
final List<PostWriteMedia>? attachments;
|
||||||
|
final SnRealm? realm;
|
||||||
|
|
||||||
const PostEditorExtra({
|
const PostEditorExtra({
|
||||||
this.text,
|
this.text,
|
||||||
this.title,
|
this.title,
|
||||||
this.description,
|
this.description,
|
||||||
this.attachments,
|
this.attachments,
|
||||||
|
this.realm,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +262,7 @@ class _PostEditorScreenState extends State<PostEditorScreen>
|
|||||||
_writeController.descriptionController.text =
|
_writeController.descriptionController.text =
|
||||||
widget.extraProps!.description ?? '';
|
widget.extraProps!.description ?? '';
|
||||||
_writeController.addAttachments(widget.extraProps!.attachments ?? []);
|
_writeController.addAttachments(widget.extraProps!.attachments ?? []);
|
||||||
|
_writeController.setRealm(widget.extraProps!.realm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,9 +343,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(
|
||||||
|
child: Text(
|
||||||
|
'postEditingNotice',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
).tr(args: [
|
||||||
'@${_writeController.editingPost!.publisher.name}'
|
'@${_writeController.editingPost!.publisher.name}'
|
||||||
]),
|
]),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -449,7 +455,9 @@ class _PostEditorScreenState extends State<PostEditorScreen>
|
|||||||
isBusy: _writeController.isBusy,
|
isBusy: _writeController.isBusy,
|
||||||
onUpload: (int idx) async {
|
onUpload: (int idx) async {
|
||||||
await _writeController.uploadSingleAttachment(
|
await _writeController.uploadSingleAttachment(
|
||||||
context, idx);
|
context,
|
||||||
|
idx,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onInsertLink: (int idx) async {
|
onInsertLink: (int idx) async {
|
||||||
_writeController.contentController.text +=
|
_writeController.contentController.text +=
|
||||||
@@ -1097,7 +1105,7 @@ class _PostQuestionEditor extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PostVideoEditor extends StatelessWidget {
|
class _PostVideoEditor extends StatefulWidget {
|
||||||
final PostWriteController controller;
|
final PostWriteController controller;
|
||||||
final Function? onTapPublisher;
|
final Function? onTapPublisher;
|
||||||
final Function? onTapRealm;
|
final Function? onTapRealm;
|
||||||
@@ -1105,7 +1113,14 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
const _PostVideoEditor(
|
const _PostVideoEditor(
|
||||||
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||||
|
|
||||||
void _selectVideo(BuildContext context) async {
|
@override
|
||||||
|
State<_PostVideoEditor> createState() => _PostVideoEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostVideoEditorState extends State<_PostVideoEditor> {
|
||||||
|
final TextEditingController _streamUrlController = TextEditingController();
|
||||||
|
|
||||||
|
void _selectVideo() async {
|
||||||
final video = await showDialog<SnAttachment?>(
|
final video = await showDialog<SnAttachment?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AttachmentInputDialog(
|
builder: (context) => AttachmentInputDialog(
|
||||||
@@ -1116,78 +1131,25 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (video == null) return;
|
if (video == null) return;
|
||||||
controller.setVideoAttachment(video);
|
widget.controller.setVideoAttachment(video);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setAlt(BuildContext context) async {
|
@override
|
||||||
if (controller.videoAttachment == null) return;
|
void initState() {
|
||||||
|
_streamUrlController.addListener(() {
|
||||||
final result = await showDialog<SnAttachment?>(
|
if (_streamUrlController.text.isEmpty) {
|
||||||
context: context,
|
widget.controller.setVideoUrl('');
|
||||||
builder: (context) => PendingAttachmentAltDialog(
|
} else {
|
||||||
media: PostWriteMedia(controller.videoAttachment)),
|
widget.controller.setVideoUrl(_streamUrlController.text);
|
||||||
);
|
}
|
||||||
if (result == null) return;
|
});
|
||||||
|
super.initState();
|
||||||
controller.setVideoAttachment(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _createBoost(BuildContext context) async {
|
@override
|
||||||
if (controller.videoAttachment == null) return;
|
void dispose() {
|
||||||
|
_streamUrlController.dispose();
|
||||||
final result = await showDialog<SnAttachmentBoost?>(
|
super.dispose();
|
||||||
context: context,
|
|
||||||
builder: (context) => PendingAttachmentBoostDialog(
|
|
||||||
media: PostWriteMedia(controller.videoAttachment)),
|
|
||||||
);
|
|
||||||
if (result == null) return;
|
|
||||||
|
|
||||||
final newAttach = controller.videoAttachment!.copyWith(
|
|
||||||
boosts: [...controller.videoAttachment!.boosts, result],
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.setVideoAttachment(newAttach);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setThumbnail(BuildContext context) async {
|
|
||||||
if (controller.videoAttachment == null) return;
|
|
||||||
|
|
||||||
final thumbnail = await showDialog<SnAttachment?>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AttachmentInputDialog(
|
|
||||||
title: 'attachmentSetThumbnail'.tr(),
|
|
||||||
pool: 'interactive',
|
|
||||||
analyzeNow: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (thumbnail == null) return;
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
final attach = context.read<SnAttachmentProvider>();
|
|
||||||
final newAttach = await attach.updateOne(
|
|
||||||
controller.videoAttachment!,
|
|
||||||
thumbnailId: thumbnail.id,
|
|
||||||
);
|
|
||||||
controller.setVideoAttachment(newAttach);
|
|
||||||
} catch (err) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _deleteAttachment(BuildContext context) async {
|
|
||||||
if (controller.videoAttachment == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client
|
|
||||||
.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
|
|
||||||
controller.setVideoAttachment(null);
|
|
||||||
} catch (err) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1205,10 +1167,10 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onTapPublisher?.call();
|
widget.onTapPublisher?.call();
|
||||||
},
|
},
|
||||||
child: AccountImage(
|
child: AccountImage(
|
||||||
content: controller.publisher?.avatar,
|
content: widget.controller.publisher?.avatar,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1218,10 +1180,10 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onTapRealm?.call();
|
widget.onTapRealm?.call();
|
||||||
},
|
},
|
||||||
child: AccountImage(
|
child: AccountImage(
|
||||||
content: controller.realm?.avatar,
|
content: widget.controller.realm?.avatar,
|
||||||
fallbackWidget: const Icon(Symbols.globe, size: 20),
|
fallbackWidget: const Icon(Symbols.globe, size: 20),
|
||||||
radius: 14,
|
radius: 14,
|
||||||
),
|
),
|
||||||
@@ -1234,7 +1196,7 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Gap(6),
|
const Gap(6),
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller.titleController,
|
controller: widget.controller.titleController,
|
||||||
decoration: InputDecoration.collapsed(
|
decoration: InputDecoration.collapsed(
|
||||||
hintText: 'fieldPostTitle'.tr(),
|
hintText: 'fieldPostTitle'.tr(),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
@@ -1245,7 +1207,7 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller.descriptionController,
|
controller: widget.controller.descriptionController,
|
||||||
decoration: InputDecoration.collapsed(
|
decoration: InputDecoration.collapsed(
|
||||||
hintText: 'fieldPostDescription'.tr(),
|
hintText: 'fieldPostDescription'.tr(),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
@@ -1257,66 +1219,50 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
|
if (widget.controller.videoLive)
|
||||||
|
TextField(
|
||||||
|
controller: _streamUrlController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'fieldPostVideoStreamUrl'.tr(),
|
||||||
|
helperText: 'fieldPostVideoStreamUrlDescription'.tr(),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
).padding(horizontal: 12)
|
||||||
|
else
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(left: 16, right: 16),
|
margin: const EdgeInsets.only(left: 16, right: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: Theme.of(context).dividerColor),
|
border: Border.all(color: Theme.of(context).dividerColor),
|
||||||
),
|
),
|
||||||
child: ContextMenuRegion(
|
|
||||||
contextMenu: ContextMenu(
|
|
||||||
entries: [
|
|
||||||
MenuItem(
|
|
||||||
label: 'attachmentSetAlt'.tr(),
|
|
||||||
icon: Symbols.description,
|
|
||||||
onSelected: () {
|
|
||||||
_setAlt(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
label: 'attachmentBoost'.tr(),
|
|
||||||
icon: Symbols.bolt,
|
|
||||||
onSelected: () {
|
|
||||||
_createBoost(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
label: 'attachmentSetThumbnail'.tr(),
|
|
||||||
icon: Symbols.image,
|
|
||||||
onSelected: () {
|
|
||||||
_setThumbnail(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
label: 'attachmentCopyRandomId'.tr(),
|
|
||||||
icon: Symbols.content_copy,
|
|
||||||
onSelected: () {
|
|
||||||
Clipboard.setData(ClipboardData(
|
|
||||||
text: controller.videoAttachment!.rid));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
label: 'delete'.tr(),
|
|
||||||
icon: Symbols.delete,
|
|
||||||
onSelected: () => _deleteAttachment(context),
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
label: 'unlink'.tr(),
|
|
||||||
icon: Symbols.link_off,
|
|
||||||
onSelected: () {
|
|
||||||
controller.setVideoAttachment(null);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
onTap: controller.videoAttachment == null
|
onTap: widget.controller.videoAttachment == null
|
||||||
? () => _selectVideo(context)
|
? () => _selectVideo()
|
||||||
: null,
|
: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
PendingAttachmentActionSheet(
|
||||||
|
media: PostWriteMedia(
|
||||||
|
widget.controller.videoAttachment!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).then((value) async {
|
||||||
|
if (value is PostWriteMedia) {
|
||||||
|
widget.controller
|
||||||
|
.setVideoAttachment(value.attachment);
|
||||||
|
} else if (value == false) {
|
||||||
|
widget.controller.setVideoAttachment(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: controller.videoAttachment == null
|
child: widget.controller.videoAttachment == null
|
||||||
? Center(
|
? Center(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -1332,13 +1278,21 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
: ClipRRect(
|
: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: AttachmentItem(
|
child: AttachmentItem(
|
||||||
data: controller.videoAttachment!,
|
data: widget.controller.videoAttachment!,
|
||||||
heroTag: const Uuid().v4(),
|
heroTag: const Uuid().v4(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Gap(8),
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.live_tv),
|
||||||
|
title: Text('postVideoLive').tr(),
|
||||||
|
subtitle: Text('postVideoLiveDescription').tr(),
|
||||||
|
value: widget.controller.videoLive,
|
||||||
|
onChanged: (value) =>
|
||||||
|
widget.controller.setVideoLive(value ?? false),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,10 +73,13 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
|
|||||||
final ele = _posts[idx];
|
final ele = _posts[idx];
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Center(
|
child: Center(
|
||||||
|
child: Card(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
child: OpenablePostItem(
|
child: OpenablePostItem(
|
||||||
key: ValueKey(ele),
|
key: ValueKey(ele),
|
||||||
data: ele,
|
data: ele,
|
||||||
maxWidth: 640,
|
maxWidth: 640,
|
||||||
|
useReplace: true,
|
||||||
onChanged: (ele) {
|
onChanged: (ele) {
|
||||||
_posts[idx] = ele;
|
_posts[idx] = ele;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
@@ -92,6 +87,7 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
|
|||||||
onDeleted: () {
|
onDeleted: () {
|
||||||
_fetchPosts();
|
_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)),
|
||||||
|
|||||||
149
lib/screens/realm/community.dart
Normal file
149
lib/screens/realm/community.dart
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/post.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/screens/post/post_editor.dart';
|
||||||
|
import 'package:surface/types/post.dart';
|
||||||
|
import 'package:surface/types/realm.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
|
class RealmCommunityScreen extends StatefulWidget {
|
||||||
|
final String alias;
|
||||||
|
const RealmCommunityScreen({super.key, required this.alias});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RealmCommunityScreen> createState() => _RealmCommunityScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RealmCommunityScreenState extends State<RealmCommunityScreen> {
|
||||||
|
SnRealm? _realm;
|
||||||
|
|
||||||
|
Future<void> _fetchRealm() async {
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/id/realms/${widget.alias}');
|
||||||
|
_realm = SnRealm.fromJson(resp.data);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
int? _totalCount;
|
||||||
|
final List<SnPost> _posts = List.empty(growable: true);
|
||||||
|
|
||||||
|
Future<void> _fetchPosts() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final pt = context.read<SnPostContentProvider>();
|
||||||
|
final out = await pt.listPosts(
|
||||||
|
take: 10,
|
||||||
|
offset: _posts.length,
|
||||||
|
realm: _realm?.id.toString(),
|
||||||
|
);
|
||||||
|
_totalCount = out.$2;
|
||||||
|
_posts.addAll(out.$1);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchRealm();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||||
|
),
|
||||||
|
floatingActionButton: _realm != null
|
||||||
|
? FloatingActionButton(
|
||||||
|
child: const Icon(Symbols.edit),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'postEditor',
|
||||||
|
extra: PostEditorExtra(realm: _realm!),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (_realm == null)
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator().center(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_realm != null)
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('realmCommunity'.tr(args: [_realm!.name]))
|
||||||
|
.fontSize(17)
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
|
Text('postTotalCount'.plural(_totalCount ?? 0))
|
||||||
|
.fontSize(13)
|
||||||
|
.opacity(0.8)
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, vertical: 16),
|
||||||
|
const Divider(height: 1),
|
||||||
|
if (_realm != null)
|
||||||
|
Expanded(
|
||||||
|
child: MediaQuery.removePadding(
|
||||||
|
context: context,
|
||||||
|
removeTop: true,
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: _fetchPosts,
|
||||||
|
child: InfiniteList(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
itemCount: _posts.length,
|
||||||
|
isLoading: _isBusy,
|
||||||
|
hasReachedMax:
|
||||||
|
_totalCount != null && _posts.length >= _totalCount!,
|
||||||
|
onFetchData: _fetchPosts,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final post = _posts[idx];
|
||||||
|
return OpenablePostItem(
|
||||||
|
data: post,
|
||||||
|
maxWidth: 640,
|
||||||
|
onChanged: (data) {
|
||||||
|
setState(() => _posts[idx] = data);
|
||||||
|
},
|
||||||
|
onDeleted: () {
|
||||||
|
setState(() => _posts.removeAt(idx));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) =>
|
||||||
|
const Divider().padding(vertical: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -318,7 +318,7 @@ class _RealmPostListWidgetState extends State<_RealmPostListWidget> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (_, __) => const Gap(8),
|
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).padding(top: 8);
|
).padding(top: 8);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
'/cgi/im/channels/${widget.realm.alias}/$channel/members',
|
||||||
|
data: {
|
||||||
'related': ua.user?.name,
|
'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(
|
||||||
@@ -323,19 +326,43 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
secondary: const Icon(Symbols.left_panel_close),
|
secondary: const Icon(Symbols.hide),
|
||||||
title: Text('settingsDrawerPreferCollapse').tr(),
|
title: Text('settingsHideBottomNav').tr(),
|
||||||
subtitle:
|
subtitle: Text('settingsHideBottomNavDescription').tr(),
|
||||||
Text('settingsDrawerPreferCollapseDescription').tr(),
|
|
||||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
|
value: _prefs.getBool(kAppHideBottomNav) ?? false,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_prefs.setBool(kAppDrawerPreferCollapse, value ?? false);
|
_prefs.setBool(kAppHideBottomNav, value ?? false);
|
||||||
final cfg = context.read<ConfigProvider>();
|
final cfg = context.read<ConfigProvider>();
|
||||||
cfg.calcDrawerSize(context);
|
cfg.calcDrawerSize(context);
|
||||||
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(),
|
||||||
@@ -728,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,15 +173,29 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
|
|||||||
children: [
|
children: [
|
||||||
Text(ele.remark),
|
Text(ele.remark),
|
||||||
const Gap(2),
|
const Gap(2),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'walletTransactionType${ele.currency.capitalize()}'
|
||||||
|
.tr(),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
Text(' · ')
|
||||||
|
.textStyle(Theme.of(context).textTheme.labelSmall!)
|
||||||
|
.padding(right: 4),
|
||||||
Text(
|
Text(
|
||||||
DateFormat(
|
DateFormat(
|
||||||
null,
|
null,
|
||||||
EasyLocalization.of(context)!.currentLocale.toString(),
|
EasyLocalization.of(context)!
|
||||||
).format(ele.createdAt),
|
.currentLocale
|
||||||
|
.toString())
|
||||||
|
.format(ele.createdAt),
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ Future<ThemeData> createAppTheme(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false),
|
progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false),
|
||||||
|
sliderTheme: SliderThemeData(year2023: false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ abstract class SnAttachment with _$SnAttachment {
|
|||||||
required String hash,
|
required String hash,
|
||||||
required int destination,
|
required int destination,
|
||||||
required int refCount,
|
required int refCount,
|
||||||
|
String? refUrl,
|
||||||
@Default(0) int contentRating,
|
@Default(0) int contentRating,
|
||||||
@Default(0) int qualityRating,
|
@Default(0) int qualityRating,
|
||||||
required DateTime? cleanedAt,
|
required DateTime? cleanedAt,
|
||||||
@@ -39,6 +41,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 +52,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 +89,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 +114,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 +128,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 +146,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 +166,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 +184,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 +196,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ mixin _$SnAttachment {
|
|||||||
String get hash;
|
String get hash;
|
||||||
int get destination;
|
int get destination;
|
||||||
int get refCount;
|
int get refCount;
|
||||||
|
String? get refUrl;
|
||||||
int get contentRating;
|
int get contentRating;
|
||||||
int get qualityRating;
|
int get qualityRating;
|
||||||
DateTime? get cleanedAt;
|
DateTime? get cleanedAt;
|
||||||
@@ -38,6 +39,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;
|
||||||
@@ -82,6 +84,7 @@ mixin _$SnAttachment {
|
|||||||
other.destination == destination) &&
|
other.destination == destination) &&
|
||||||
(identical(other.refCount, refCount) ||
|
(identical(other.refCount, refCount) ||
|
||||||
other.refCount == refCount) &&
|
other.refCount == refCount) &&
|
||||||
|
(identical(other.refUrl, refUrl) || other.refUrl == refUrl) &&
|
||||||
(identical(other.contentRating, contentRating) ||
|
(identical(other.contentRating, contentRating) ||
|
||||||
other.contentRating == contentRating) &&
|
other.contentRating == contentRating) &&
|
||||||
(identical(other.qualityRating, qualityRating) ||
|
(identical(other.qualityRating, qualityRating) ||
|
||||||
@@ -98,6 +101,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) ||
|
||||||
@@ -130,6 +134,7 @@ mixin _$SnAttachment {
|
|||||||
hash,
|
hash,
|
||||||
destination,
|
destination,
|
||||||
refCount,
|
refCount,
|
||||||
|
refUrl,
|
||||||
contentRating,
|
contentRating,
|
||||||
qualityRating,
|
qualityRating,
|
||||||
cleanedAt,
|
cleanedAt,
|
||||||
@@ -140,6 +145,7 @@ mixin _$SnAttachment {
|
|||||||
refId,
|
refId,
|
||||||
pool,
|
pool,
|
||||||
poolId,
|
poolId,
|
||||||
|
account,
|
||||||
accountId,
|
accountId,
|
||||||
thumbnailId,
|
thumbnailId,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
@@ -152,7 +158,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, refUrl: $refUrl, 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)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +182,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
|
|||||||
String hash,
|
String hash,
|
||||||
int destination,
|
int destination,
|
||||||
int refCount,
|
int refCount,
|
||||||
|
String? refUrl,
|
||||||
int contentRating,
|
int contentRating,
|
||||||
int qualityRating,
|
int qualityRating,
|
||||||
DateTime? cleanedAt,
|
DateTime? cleanedAt,
|
||||||
@@ -186,6 +193,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 +205,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;
|
||||||
}
|
}
|
||||||
@@ -226,6 +235,7 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
|||||||
Object? hash = null,
|
Object? hash = null,
|
||||||
Object? destination = null,
|
Object? destination = null,
|
||||||
Object? refCount = null,
|
Object? refCount = null,
|
||||||
|
Object? refUrl = freezed,
|
||||||
Object? contentRating = null,
|
Object? contentRating = null,
|
||||||
Object? qualityRating = null,
|
Object? qualityRating = null,
|
||||||
Object? cleanedAt = freezed,
|
Object? cleanedAt = freezed,
|
||||||
@@ -236,6 +246,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,
|
||||||
@@ -298,6 +309,10 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
|||||||
? _self.refCount
|
? _self.refCount
|
||||||
: refCount // ignore: cast_nullable_to_non_nullable
|
: refCount // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
refUrl: freezed == refUrl
|
||||||
|
? _self.refUrl
|
||||||
|
: refUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
contentRating: null == contentRating
|
contentRating: null == contentRating
|
||||||
? _self.contentRating
|
? _self.contentRating
|
||||||
: contentRating // ignore: cast_nullable_to_non_nullable
|
: contentRating // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -338,6 +353,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 +420,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
|
||||||
@@ -447,6 +480,7 @@ class _SnAttachment extends SnAttachment {
|
|||||||
required this.hash,
|
required this.hash,
|
||||||
required this.destination,
|
required this.destination,
|
||||||
required this.refCount,
|
required this.refCount,
|
||||||
|
this.refUrl,
|
||||||
this.contentRating = 0,
|
this.contentRating = 0,
|
||||||
this.qualityRating = 0,
|
this.qualityRating = 0,
|
||||||
required this.cleanedAt,
|
required this.cleanedAt,
|
||||||
@@ -457,6 +491,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,
|
||||||
@@ -499,6 +534,8 @@ class _SnAttachment extends SnAttachment {
|
|||||||
@override
|
@override
|
||||||
final int refCount;
|
final int refCount;
|
||||||
@override
|
@override
|
||||||
|
final String? refUrl;
|
||||||
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final int contentRating;
|
final int contentRating;
|
||||||
@override
|
@override
|
||||||
@@ -521,6 +558,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;
|
||||||
@@ -596,6 +635,7 @@ class _SnAttachment extends SnAttachment {
|
|||||||
other.destination == destination) &&
|
other.destination == destination) &&
|
||||||
(identical(other.refCount, refCount) ||
|
(identical(other.refCount, refCount) ||
|
||||||
other.refCount == refCount) &&
|
other.refCount == refCount) &&
|
||||||
|
(identical(other.refUrl, refUrl) || other.refUrl == refUrl) &&
|
||||||
(identical(other.contentRating, contentRating) ||
|
(identical(other.contentRating, contentRating) ||
|
||||||
other.contentRating == contentRating) &&
|
other.contentRating == contentRating) &&
|
||||||
(identical(other.qualityRating, qualityRating) ||
|
(identical(other.qualityRating, qualityRating) ||
|
||||||
@@ -612,6 +652,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) ||
|
||||||
@@ -644,6 +685,7 @@ class _SnAttachment extends SnAttachment {
|
|||||||
hash,
|
hash,
|
||||||
destination,
|
destination,
|
||||||
refCount,
|
refCount,
|
||||||
|
refUrl,
|
||||||
contentRating,
|
contentRating,
|
||||||
qualityRating,
|
qualityRating,
|
||||||
cleanedAt,
|
cleanedAt,
|
||||||
@@ -654,6 +696,7 @@ class _SnAttachment extends SnAttachment {
|
|||||||
refId,
|
refId,
|
||||||
pool,
|
pool,
|
||||||
poolId,
|
poolId,
|
||||||
|
account,
|
||||||
accountId,
|
accountId,
|
||||||
thumbnailId,
|
thumbnailId,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
@@ -666,7 +709,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, refUrl: $refUrl, 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)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,6 +735,7 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
|
|||||||
String hash,
|
String hash,
|
||||||
int destination,
|
int destination,
|
||||||
int refCount,
|
int refCount,
|
||||||
|
String? refUrl,
|
||||||
int contentRating,
|
int contentRating,
|
||||||
int qualityRating,
|
int qualityRating,
|
||||||
DateTime? cleanedAt,
|
DateTime? cleanedAt,
|
||||||
@@ -702,6 +746,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 +761,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;
|
||||||
@@ -747,6 +794,7 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
|||||||
Object? hash = null,
|
Object? hash = null,
|
||||||
Object? destination = null,
|
Object? destination = null,
|
||||||
Object? refCount = null,
|
Object? refCount = null,
|
||||||
|
Object? refUrl = freezed,
|
||||||
Object? contentRating = null,
|
Object? contentRating = null,
|
||||||
Object? qualityRating = null,
|
Object? qualityRating = null,
|
||||||
Object? cleanedAt = freezed,
|
Object? cleanedAt = freezed,
|
||||||
@@ -757,6 +805,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,
|
||||||
@@ -819,6 +868,10 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
|||||||
? _self.refCount
|
? _self.refCount
|
||||||
: refCount // ignore: cast_nullable_to_non_nullable
|
: refCount // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,
|
||||||
|
refUrl: freezed == refUrl
|
||||||
|
? _self.refUrl
|
||||||
|
: refUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
contentRating: null == contentRating
|
contentRating: null == contentRating
|
||||||
? _self.contentRating
|
? _self.contentRating
|
||||||
: contentRating // ignore: cast_nullable_to_non_nullable
|
: contentRating // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -859,6 +912,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 +979,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
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ _SnAttachment _$SnAttachmentFromJson(Map<String, dynamic> json) =>
|
|||||||
hash: json['hash'] as String,
|
hash: json['hash'] as String,
|
||||||
destination: (json['destination'] as num).toInt(),
|
destination: (json['destination'] as num).toInt(),
|
||||||
refCount: (json['ref_count'] as num).toInt(),
|
refCount: (json['ref_count'] as num).toInt(),
|
||||||
|
refUrl: json['ref_url'] as String?,
|
||||||
contentRating: (json['content_rating'] as num?)?.toInt() ?? 0,
|
contentRating: (json['content_rating'] as num?)?.toInt() ?? 0,
|
||||||
qualityRating: (json['quality_rating'] as num?)?.toInt() ?? 0,
|
qualityRating: (json['quality_rating'] as num?)?.toInt() ?? 0,
|
||||||
cleanedAt: json['cleaned_at'] == null
|
cleanedAt: json['cleaned_at'] == null
|
||||||
@@ -39,6 +40,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
|
||||||
@@ -72,6 +76,7 @@ Map<String, dynamic> _$SnAttachmentToJson(_SnAttachment instance) =>
|
|||||||
'hash': instance.hash,
|
'hash': instance.hash,
|
||||||
'destination': instance.destination,
|
'destination': instance.destination,
|
||||||
'ref_count': instance.refCount,
|
'ref_count': instance.refCount,
|
||||||
|
'ref_url': instance.refUrl,
|
||||||
'content_rating': instance.contentRating,
|
'content_rating': instance.contentRating,
|
||||||
'quality_rating': instance.qualityRating,
|
'quality_rating': instance.qualityRating,
|
||||||
'cleaned_at': instance.cleanedAt?.toIso8601String(),
|
'cleaned_at': instance.cleanedAt?.toIso8601String(),
|
||||||
@@ -82,6 +87,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,5 +1,4 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
@@ -116,24 +115,3 @@ abstract class SnChatCall with _$SnChatCall {
|
|||||||
factory SnChatCall.fromJson(Map<String, dynamic> json) =>
|
factory SnChatCall.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnChatCallFromJson(json);
|
_$SnChatCallFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call stuff
|
|
||||||
|
|
||||||
enum ParticipantStatsType {
|
|
||||||
unknown,
|
|
||||||
localAudioSender,
|
|
||||||
localVideoSender,
|
|
||||||
remoteAudioReceiver,
|
|
||||||
remoteVideoReceiver,
|
|
||||||
}
|
|
||||||
|
|
||||||
class ParticipantTrack {
|
|
||||||
ParticipantTrack(
|
|
||||||
{required this.participant,
|
|
||||||
required this.videoTrack,
|
|
||||||
required this.isScreenShare});
|
|
||||||
|
|
||||||
VideoTrack? videoTrack;
|
|
||||||
Participant participant;
|
|
||||||
bool isScreenShare;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,35 +4,43 @@ part 'news.freezed.dart';
|
|||||||
part 'news.g.dart';
|
part 'news.g.dart';
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
abstract class SnNewsSource with _$SnNewsSource {
|
abstract class SnSubscriptionFeed with _$SnSubscriptionFeed {
|
||||||
const factory SnNewsSource({
|
const factory SnSubscriptionFeed({
|
||||||
required String id,
|
|
||||||
required String label,
|
|
||||||
required String type,
|
|
||||||
required String source,
|
|
||||||
required int depth,
|
|
||||||
required bool enabled,
|
|
||||||
}) = _SnNewsSource;
|
|
||||||
|
|
||||||
factory SnNewsSource.fromJson(Map<String, dynamic> json) => _$SnNewsSourceFromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
abstract class SnNewsArticle with _$SnNewsArticle {
|
|
||||||
const factory SnNewsArticle({
|
|
||||||
required int id,
|
required int id,
|
||||||
required DateTime createdAt,
|
required DateTime createdAt,
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
required dynamic deletedAt,
|
required DateTime? deletedAt,
|
||||||
|
required String url,
|
||||||
|
required bool isEnabled,
|
||||||
|
required bool isFullContent,
|
||||||
|
required int pullInterval,
|
||||||
|
required String adapter,
|
||||||
|
required int? accountId,
|
||||||
|
required DateTime? lastFetchedAt,
|
||||||
|
}) = _SnSubscriptionFeed;
|
||||||
|
|
||||||
|
factory SnSubscriptionFeed.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnSubscriptionFeedFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class SnSubscriptionItem with _$SnSubscriptionItem {
|
||||||
|
const factory SnSubscriptionItem({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
required String thumbnail,
|
required String thumbnail,
|
||||||
required String title,
|
required String title,
|
||||||
required String description,
|
required String description,
|
||||||
required String content,
|
required String content,
|
||||||
required String url,
|
required String url,
|
||||||
required String hash,
|
required String hash,
|
||||||
required String source,
|
required int feedId,
|
||||||
|
required SnSubscriptionFeed feed,
|
||||||
required DateTime? publishedAt,
|
required DateTime? publishedAt,
|
||||||
}) = _SnNewsArticle;
|
}) = _SnSubscriptionItem;
|
||||||
|
|
||||||
factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json);
|
factory SnSubscriptionItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnSubscriptionItemFromJson(json);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,149 +14,224 @@ part of 'news.dart';
|
|||||||
T _$identity<T>(T value) => value;
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnNewsSource {
|
mixin _$SnSubscriptionFeed {
|
||||||
String get id;
|
int get id;
|
||||||
String get label;
|
DateTime get createdAt;
|
||||||
String get type;
|
DateTime get updatedAt;
|
||||||
String get source;
|
DateTime? get deletedAt;
|
||||||
int get depth;
|
String get url;
|
||||||
bool get enabled;
|
bool get isEnabled;
|
||||||
|
bool get isFullContent;
|
||||||
|
int get pullInterval;
|
||||||
|
String get adapter;
|
||||||
|
int? get accountId;
|
||||||
|
DateTime? get lastFetchedAt;
|
||||||
|
|
||||||
/// Create a copy of SnNewsSource
|
/// Create a copy of SnSubscriptionFeed
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
$SnNewsSourceCopyWith<SnNewsSource> get copyWith =>
|
$SnSubscriptionFeedCopyWith<SnSubscriptionFeed> get copyWith =>
|
||||||
_$SnNewsSourceCopyWithImpl<SnNewsSource>(
|
_$SnSubscriptionFeedCopyWithImpl<SnSubscriptionFeed>(
|
||||||
this as SnNewsSource, _$identity);
|
this as SnSubscriptionFeed, _$identity);
|
||||||
|
|
||||||
/// Serializes this SnNewsSource to a JSON map.
|
/// Serializes this SnSubscriptionFeed to a JSON map.
|
||||||
Map<String, dynamic> toJson();
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other.runtimeType == runtimeType &&
|
(other.runtimeType == runtimeType &&
|
||||||
other is SnNewsSource &&
|
other is SnSubscriptionFeed &&
|
||||||
(identical(other.id, id) || other.id == id) &&
|
(identical(other.id, id) || other.id == id) &&
|
||||||
(identical(other.label, label) || other.label == label) &&
|
(identical(other.createdAt, createdAt) ||
|
||||||
(identical(other.type, type) || other.type == type) &&
|
other.createdAt == createdAt) &&
|
||||||
(identical(other.source, source) || other.source == source) &&
|
(identical(other.updatedAt, updatedAt) ||
|
||||||
(identical(other.depth, depth) || other.depth == depth) &&
|
other.updatedAt == updatedAt) &&
|
||||||
(identical(other.enabled, enabled) || other.enabled == enabled));
|
(identical(other.deletedAt, deletedAt) ||
|
||||||
|
other.deletedAt == deletedAt) &&
|
||||||
|
(identical(other.url, url) || other.url == url) &&
|
||||||
|
(identical(other.isEnabled, isEnabled) ||
|
||||||
|
other.isEnabled == isEnabled) &&
|
||||||
|
(identical(other.isFullContent, isFullContent) ||
|
||||||
|
other.isFullContent == isFullContent) &&
|
||||||
|
(identical(other.pullInterval, pullInterval) ||
|
||||||
|
other.pullInterval == pullInterval) &&
|
||||||
|
(identical(other.adapter, adapter) || other.adapter == adapter) &&
|
||||||
|
(identical(other.accountId, accountId) ||
|
||||||
|
other.accountId == accountId) &&
|
||||||
|
(identical(other.lastFetchedAt, lastFetchedAt) ||
|
||||||
|
other.lastFetchedAt == lastFetchedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode => Object.hash(
|
||||||
Object.hash(runtimeType, id, label, type, source, depth, enabled);
|
runtimeType,
|
||||||
|
id,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
deletedAt,
|
||||||
|
url,
|
||||||
|
isEnabled,
|
||||||
|
isFullContent,
|
||||||
|
pullInterval,
|
||||||
|
adapter,
|
||||||
|
accountId,
|
||||||
|
lastFetchedAt);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)';
|
return 'SnSubscriptionFeed(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, url: $url, isEnabled: $isEnabled, isFullContent: $isFullContent, pullInterval: $pullInterval, adapter: $adapter, accountId: $accountId, lastFetchedAt: $lastFetchedAt)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
abstract mixin class $SnNewsSourceCopyWith<$Res> {
|
abstract mixin class $SnSubscriptionFeedCopyWith<$Res> {
|
||||||
factory $SnNewsSourceCopyWith(
|
factory $SnSubscriptionFeedCopyWith(
|
||||||
SnNewsSource value, $Res Function(SnNewsSource) _then) =
|
SnSubscriptionFeed value, $Res Function(SnSubscriptionFeed) _then) =
|
||||||
_$SnNewsSourceCopyWithImpl;
|
_$SnSubscriptionFeedCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{String id,
|
{int id,
|
||||||
String label,
|
DateTime createdAt,
|
||||||
String type,
|
DateTime updatedAt,
|
||||||
String source,
|
DateTime? deletedAt,
|
||||||
int depth,
|
String url,
|
||||||
bool enabled});
|
bool isEnabled,
|
||||||
|
bool isFullContent,
|
||||||
|
int pullInterval,
|
||||||
|
String adapter,
|
||||||
|
int? accountId,
|
||||||
|
DateTime? lastFetchedAt});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
class _$SnNewsSourceCopyWithImpl<$Res> implements $SnNewsSourceCopyWith<$Res> {
|
class _$SnSubscriptionFeedCopyWithImpl<$Res>
|
||||||
_$SnNewsSourceCopyWithImpl(this._self, this._then);
|
implements $SnSubscriptionFeedCopyWith<$Res> {
|
||||||
|
_$SnSubscriptionFeedCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
final SnNewsSource _self;
|
final SnSubscriptionFeed _self;
|
||||||
final $Res Function(SnNewsSource) _then;
|
final $Res Function(SnSubscriptionFeed) _then;
|
||||||
|
|
||||||
/// Create a copy of SnNewsSource
|
/// Create a copy of SnSubscriptionFeed
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? id = null,
|
Object? id = null,
|
||||||
Object? label = null,
|
Object? createdAt = null,
|
||||||
Object? type = null,
|
Object? updatedAt = null,
|
||||||
Object? source = null,
|
Object? deletedAt = freezed,
|
||||||
Object? depth = null,
|
Object? url = null,
|
||||||
Object? enabled = null,
|
Object? isEnabled = null,
|
||||||
|
Object? isFullContent = null,
|
||||||
|
Object? pullInterval = null,
|
||||||
|
Object? adapter = null,
|
||||||
|
Object? accountId = freezed,
|
||||||
|
Object? lastFetchedAt = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
id: null == id
|
id: null == id
|
||||||
? _self.id
|
? _self.id
|
||||||
: id // ignore: cast_nullable_to_non_nullable
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
|
||||||
label: null == label
|
|
||||||
? _self.label
|
|
||||||
: label // ignore: cast_nullable_to_non_nullable
|
|
||||||
as String,
|
|
||||||
type: null == type
|
|
||||||
? _self.type
|
|
||||||
: type // ignore: cast_nullable_to_non_nullable
|
|
||||||
as String,
|
|
||||||
source: null == source
|
|
||||||
? _self.source
|
|
||||||
: source // ignore: cast_nullable_to_non_nullable
|
|
||||||
as String,
|
|
||||||
depth: null == depth
|
|
||||||
? _self.depth
|
|
||||||
: depth // ignore: cast_nullable_to_non_nullable
|
|
||||||
as int,
|
as int,
|
||||||
enabled: null == enabled
|
createdAt: null == createdAt
|
||||||
? _self.enabled
|
? _self.createdAt
|
||||||
: enabled // ignore: cast_nullable_to_non_nullable
|
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
updatedAt: null == updatedAt
|
||||||
|
? _self.updatedAt
|
||||||
|
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
deletedAt: freezed == deletedAt
|
||||||
|
? _self.deletedAt
|
||||||
|
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
url: null == url
|
||||||
|
? _self.url
|
||||||
|
: url // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
isEnabled: null == isEnabled
|
||||||
|
? _self.isEnabled
|
||||||
|
: isEnabled // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
|
isFullContent: null == isFullContent
|
||||||
|
? _self.isFullContent
|
||||||
|
: isFullContent // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
pullInterval: null == pullInterval
|
||||||
|
? _self.pullInterval
|
||||||
|
: pullInterval // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
adapter: null == adapter
|
||||||
|
? _self.adapter
|
||||||
|
: adapter // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
accountId: freezed == accountId
|
||||||
|
? _self.accountId
|
||||||
|
: accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,
|
||||||
|
lastFetchedAt: freezed == lastFetchedAt
|
||||||
|
? _self.lastFetchedAt
|
||||||
|
: lastFetchedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _SnNewsSource implements SnNewsSource {
|
class _SnSubscriptionFeed implements SnSubscriptionFeed {
|
||||||
const _SnNewsSource(
|
const _SnSubscriptionFeed(
|
||||||
{required this.id,
|
{required this.id,
|
||||||
required this.label,
|
required this.createdAt,
|
||||||
required this.type,
|
required this.updatedAt,
|
||||||
required this.source,
|
required this.deletedAt,
|
||||||
required this.depth,
|
required this.url,
|
||||||
required this.enabled});
|
required this.isEnabled,
|
||||||
factory _SnNewsSource.fromJson(Map<String, dynamic> json) =>
|
required this.isFullContent,
|
||||||
_$SnNewsSourceFromJson(json);
|
required this.pullInterval,
|
||||||
|
required this.adapter,
|
||||||
|
required this.accountId,
|
||||||
|
required this.lastFetchedAt});
|
||||||
|
factory _SnSubscriptionFeed.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnSubscriptionFeedFromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final String id;
|
final int id;
|
||||||
@override
|
@override
|
||||||
final String label;
|
final DateTime createdAt;
|
||||||
@override
|
@override
|
||||||
final String type;
|
final DateTime updatedAt;
|
||||||
@override
|
@override
|
||||||
final String source;
|
final DateTime? deletedAt;
|
||||||
@override
|
@override
|
||||||
final int depth;
|
final String url;
|
||||||
@override
|
@override
|
||||||
final bool enabled;
|
final bool isEnabled;
|
||||||
|
@override
|
||||||
|
final bool isFullContent;
|
||||||
|
@override
|
||||||
|
final int pullInterval;
|
||||||
|
@override
|
||||||
|
final String adapter;
|
||||||
|
@override
|
||||||
|
final int? accountId;
|
||||||
|
@override
|
||||||
|
final DateTime? lastFetchedAt;
|
||||||
|
|
||||||
/// Create a copy of SnNewsSource
|
/// Create a copy of SnSubscriptionFeed
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
_$SnNewsSourceCopyWith<_SnNewsSource> get copyWith =>
|
_$SnSubscriptionFeedCopyWith<_SnSubscriptionFeed> get copyWith =>
|
||||||
__$SnNewsSourceCopyWithImpl<_SnNewsSource>(this, _$identity);
|
__$SnSubscriptionFeedCopyWithImpl<_SnSubscriptionFeed>(this, _$identity);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return _$SnNewsSourceToJson(
|
return _$SnSubscriptionFeedToJson(
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -165,129 +240,185 @@ class _SnNewsSource implements SnNewsSource {
|
|||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other.runtimeType == runtimeType &&
|
(other.runtimeType == runtimeType &&
|
||||||
other is _SnNewsSource &&
|
other is _SnSubscriptionFeed &&
|
||||||
(identical(other.id, id) || other.id == id) &&
|
(identical(other.id, id) || other.id == id) &&
|
||||||
(identical(other.label, label) || other.label == label) &&
|
(identical(other.createdAt, createdAt) ||
|
||||||
(identical(other.type, type) || other.type == type) &&
|
other.createdAt == createdAt) &&
|
||||||
(identical(other.source, source) || other.source == source) &&
|
(identical(other.updatedAt, updatedAt) ||
|
||||||
(identical(other.depth, depth) || other.depth == depth) &&
|
other.updatedAt == updatedAt) &&
|
||||||
(identical(other.enabled, enabled) || other.enabled == enabled));
|
(identical(other.deletedAt, deletedAt) ||
|
||||||
|
other.deletedAt == deletedAt) &&
|
||||||
|
(identical(other.url, url) || other.url == url) &&
|
||||||
|
(identical(other.isEnabled, isEnabled) ||
|
||||||
|
other.isEnabled == isEnabled) &&
|
||||||
|
(identical(other.isFullContent, isFullContent) ||
|
||||||
|
other.isFullContent == isFullContent) &&
|
||||||
|
(identical(other.pullInterval, pullInterval) ||
|
||||||
|
other.pullInterval == pullInterval) &&
|
||||||
|
(identical(other.adapter, adapter) || other.adapter == adapter) &&
|
||||||
|
(identical(other.accountId, accountId) ||
|
||||||
|
other.accountId == accountId) &&
|
||||||
|
(identical(other.lastFetchedAt, lastFetchedAt) ||
|
||||||
|
other.lastFetchedAt == lastFetchedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode => Object.hash(
|
||||||
Object.hash(runtimeType, id, label, type, source, depth, enabled);
|
runtimeType,
|
||||||
|
id,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
deletedAt,
|
||||||
|
url,
|
||||||
|
isEnabled,
|
||||||
|
isFullContent,
|
||||||
|
pullInterval,
|
||||||
|
adapter,
|
||||||
|
accountId,
|
||||||
|
lastFetchedAt);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)';
|
return 'SnSubscriptionFeed(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, url: $url, isEnabled: $isEnabled, isFullContent: $isFullContent, pullInterval: $pullInterval, adapter: $adapter, accountId: $accountId, lastFetchedAt: $lastFetchedAt)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
abstract mixin class _$SnNewsSourceCopyWith<$Res>
|
abstract mixin class _$SnSubscriptionFeedCopyWith<$Res>
|
||||||
implements $SnNewsSourceCopyWith<$Res> {
|
implements $SnSubscriptionFeedCopyWith<$Res> {
|
||||||
factory _$SnNewsSourceCopyWith(
|
factory _$SnSubscriptionFeedCopyWith(
|
||||||
_SnNewsSource value, $Res Function(_SnNewsSource) _then) =
|
_SnSubscriptionFeed value, $Res Function(_SnSubscriptionFeed) _then) =
|
||||||
__$SnNewsSourceCopyWithImpl;
|
__$SnSubscriptionFeedCopyWithImpl;
|
||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{String id,
|
{int id,
|
||||||
String label,
|
DateTime createdAt,
|
||||||
String type,
|
DateTime updatedAt,
|
||||||
String source,
|
DateTime? deletedAt,
|
||||||
int depth,
|
String url,
|
||||||
bool enabled});
|
bool isEnabled,
|
||||||
|
bool isFullContent,
|
||||||
|
int pullInterval,
|
||||||
|
String adapter,
|
||||||
|
int? accountId,
|
||||||
|
DateTime? lastFetchedAt});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
class __$SnNewsSourceCopyWithImpl<$Res>
|
class __$SnSubscriptionFeedCopyWithImpl<$Res>
|
||||||
implements _$SnNewsSourceCopyWith<$Res> {
|
implements _$SnSubscriptionFeedCopyWith<$Res> {
|
||||||
__$SnNewsSourceCopyWithImpl(this._self, this._then);
|
__$SnSubscriptionFeedCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
final _SnNewsSource _self;
|
final _SnSubscriptionFeed _self;
|
||||||
final $Res Function(_SnNewsSource) _then;
|
final $Res Function(_SnSubscriptionFeed) _then;
|
||||||
|
|
||||||
/// Create a copy of SnNewsSource
|
/// Create a copy of SnSubscriptionFeed
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? id = null,
|
Object? id = null,
|
||||||
Object? label = null,
|
Object? createdAt = null,
|
||||||
Object? type = null,
|
Object? updatedAt = null,
|
||||||
Object? source = null,
|
Object? deletedAt = freezed,
|
||||||
Object? depth = null,
|
Object? url = null,
|
||||||
Object? enabled = null,
|
Object? isEnabled = null,
|
||||||
|
Object? isFullContent = null,
|
||||||
|
Object? pullInterval = null,
|
||||||
|
Object? adapter = null,
|
||||||
|
Object? accountId = freezed,
|
||||||
|
Object? lastFetchedAt = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_SnNewsSource(
|
return _then(_SnSubscriptionFeed(
|
||||||
id: null == id
|
id: null == id
|
||||||
? _self.id
|
? _self.id
|
||||||
: id // ignore: cast_nullable_to_non_nullable
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
|
||||||
label: null == label
|
|
||||||
? _self.label
|
|
||||||
: label // ignore: cast_nullable_to_non_nullable
|
|
||||||
as String,
|
|
||||||
type: null == type
|
|
||||||
? _self.type
|
|
||||||
: type // ignore: cast_nullable_to_non_nullable
|
|
||||||
as String,
|
|
||||||
source: null == source
|
|
||||||
? _self.source
|
|
||||||
: source // ignore: cast_nullable_to_non_nullable
|
|
||||||
as String,
|
|
||||||
depth: null == depth
|
|
||||||
? _self.depth
|
|
||||||
: depth // ignore: cast_nullable_to_non_nullable
|
|
||||||
as int,
|
as int,
|
||||||
enabled: null == enabled
|
createdAt: null == createdAt
|
||||||
? _self.enabled
|
? _self.createdAt
|
||||||
: enabled // ignore: cast_nullable_to_non_nullable
|
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
updatedAt: null == updatedAt
|
||||||
|
? _self.updatedAt
|
||||||
|
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
deletedAt: freezed == deletedAt
|
||||||
|
? _self.deletedAt
|
||||||
|
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
url: null == url
|
||||||
|
? _self.url
|
||||||
|
: url // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
isEnabled: null == isEnabled
|
||||||
|
? _self.isEnabled
|
||||||
|
: isEnabled // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
|
isFullContent: null == isFullContent
|
||||||
|
? _self.isFullContent
|
||||||
|
: isFullContent // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
pullInterval: null == pullInterval
|
||||||
|
? _self.pullInterval
|
||||||
|
: pullInterval // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
adapter: null == adapter
|
||||||
|
? _self.adapter
|
||||||
|
: adapter // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
accountId: freezed == accountId
|
||||||
|
? _self.accountId
|
||||||
|
: accountId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,
|
||||||
|
lastFetchedAt: freezed == lastFetchedAt
|
||||||
|
? _self.lastFetchedAt
|
||||||
|
: lastFetchedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnNewsArticle {
|
mixin _$SnSubscriptionItem {
|
||||||
int get id;
|
int get id;
|
||||||
DateTime get createdAt;
|
DateTime get createdAt;
|
||||||
DateTime get updatedAt;
|
DateTime get updatedAt;
|
||||||
dynamic get deletedAt;
|
DateTime? get deletedAt;
|
||||||
String get thumbnail;
|
String get thumbnail;
|
||||||
String get title;
|
String get title;
|
||||||
String get description;
|
String get description;
|
||||||
String get content;
|
String get content;
|
||||||
String get url;
|
String get url;
|
||||||
String get hash;
|
String get hash;
|
||||||
String get source;
|
int get feedId;
|
||||||
|
SnSubscriptionFeed get feed;
|
||||||
DateTime? get publishedAt;
|
DateTime? get publishedAt;
|
||||||
|
|
||||||
/// Create a copy of SnNewsArticle
|
/// Create a copy of SnSubscriptionItem
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
$SnNewsArticleCopyWith<SnNewsArticle> get copyWith =>
|
$SnSubscriptionItemCopyWith<SnSubscriptionItem> get copyWith =>
|
||||||
_$SnNewsArticleCopyWithImpl<SnNewsArticle>(
|
_$SnSubscriptionItemCopyWithImpl<SnSubscriptionItem>(
|
||||||
this as SnNewsArticle, _$identity);
|
this as SnSubscriptionItem, _$identity);
|
||||||
|
|
||||||
/// Serializes this SnNewsArticle to a JSON map.
|
/// Serializes this SnSubscriptionItem to a JSON map.
|
||||||
Map<String, dynamic> toJson();
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other.runtimeType == runtimeType &&
|
(other.runtimeType == runtimeType &&
|
||||||
other is SnNewsArticle &&
|
other is SnSubscriptionItem &&
|
||||||
(identical(other.id, id) || other.id == id) &&
|
(identical(other.id, id) || other.id == id) &&
|
||||||
(identical(other.createdAt, createdAt) ||
|
(identical(other.createdAt, createdAt) ||
|
||||||
other.createdAt == createdAt) &&
|
other.createdAt == createdAt) &&
|
||||||
(identical(other.updatedAt, updatedAt) ||
|
(identical(other.updatedAt, updatedAt) ||
|
||||||
other.updatedAt == updatedAt) &&
|
other.updatedAt == updatedAt) &&
|
||||||
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
(identical(other.deletedAt, deletedAt) ||
|
||||||
|
other.deletedAt == deletedAt) &&
|
||||||
(identical(other.thumbnail, thumbnail) ||
|
(identical(other.thumbnail, thumbnail) ||
|
||||||
other.thumbnail == thumbnail) &&
|
other.thumbnail == thumbnail) &&
|
||||||
(identical(other.title, title) || other.title == title) &&
|
(identical(other.title, title) || other.title == title) &&
|
||||||
@@ -296,7 +427,8 @@ mixin _$SnNewsArticle {
|
|||||||
(identical(other.content, content) || other.content == content) &&
|
(identical(other.content, content) || other.content == content) &&
|
||||||
(identical(other.url, url) || other.url == url) &&
|
(identical(other.url, url) || other.url == url) &&
|
||||||
(identical(other.hash, hash) || other.hash == hash) &&
|
(identical(other.hash, hash) || other.hash == hash) &&
|
||||||
(identical(other.source, source) || other.source == source) &&
|
(identical(other.feedId, feedId) || other.feedId == feedId) &&
|
||||||
|
(identical(other.feed, feed) || other.feed == feed) &&
|
||||||
(identical(other.publishedAt, publishedAt) ||
|
(identical(other.publishedAt, publishedAt) ||
|
||||||
other.publishedAt == publishedAt));
|
other.publishedAt == publishedAt));
|
||||||
}
|
}
|
||||||
@@ -308,52 +440,56 @@ mixin _$SnNewsArticle {
|
|||||||
id,
|
id,
|
||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
const DeepCollectionEquality().hash(deletedAt),
|
deletedAt,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
content,
|
content,
|
||||||
url,
|
url,
|
||||||
hash,
|
hash,
|
||||||
source,
|
feedId,
|
||||||
|
feed,
|
||||||
publishedAt);
|
publishedAt);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnNewsArticle(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, source: $source, publishedAt: $publishedAt)';
|
return 'SnSubscriptionItem(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, feedId: $feedId, feed: $feed, publishedAt: $publishedAt)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
abstract mixin class $SnNewsArticleCopyWith<$Res> {
|
abstract mixin class $SnSubscriptionItemCopyWith<$Res> {
|
||||||
factory $SnNewsArticleCopyWith(
|
factory $SnSubscriptionItemCopyWith(
|
||||||
SnNewsArticle value, $Res Function(SnNewsArticle) _then) =
|
SnSubscriptionItem value, $Res Function(SnSubscriptionItem) _then) =
|
||||||
_$SnNewsArticleCopyWithImpl;
|
_$SnSubscriptionItemCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{int id,
|
{int id,
|
||||||
DateTime createdAt,
|
DateTime createdAt,
|
||||||
DateTime updatedAt,
|
DateTime updatedAt,
|
||||||
dynamic deletedAt,
|
DateTime? deletedAt,
|
||||||
String thumbnail,
|
String thumbnail,
|
||||||
String title,
|
String title,
|
||||||
String description,
|
String description,
|
||||||
String content,
|
String content,
|
||||||
String url,
|
String url,
|
||||||
String hash,
|
String hash,
|
||||||
String source,
|
int feedId,
|
||||||
|
SnSubscriptionFeed feed,
|
||||||
DateTime? publishedAt});
|
DateTime? publishedAt});
|
||||||
|
|
||||||
|
$SnSubscriptionFeedCopyWith<$Res> get feed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
class _$SnNewsArticleCopyWithImpl<$Res>
|
class _$SnSubscriptionItemCopyWithImpl<$Res>
|
||||||
implements $SnNewsArticleCopyWith<$Res> {
|
implements $SnSubscriptionItemCopyWith<$Res> {
|
||||||
_$SnNewsArticleCopyWithImpl(this._self, this._then);
|
_$SnSubscriptionItemCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
final SnNewsArticle _self;
|
final SnSubscriptionItem _self;
|
||||||
final $Res Function(SnNewsArticle) _then;
|
final $Res Function(SnSubscriptionItem) _then;
|
||||||
|
|
||||||
/// Create a copy of SnNewsArticle
|
/// Create a copy of SnSubscriptionItem
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
@@ -368,7 +504,8 @@ class _$SnNewsArticleCopyWithImpl<$Res>
|
|||||||
Object? content = null,
|
Object? content = null,
|
||||||
Object? url = null,
|
Object? url = null,
|
||||||
Object? hash = null,
|
Object? hash = null,
|
||||||
Object? source = null,
|
Object? feedId = null,
|
||||||
|
Object? feed = null,
|
||||||
Object? publishedAt = freezed,
|
Object? publishedAt = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
@@ -387,7 +524,7 @@ class _$SnNewsArticleCopyWithImpl<$Res>
|
|||||||
deletedAt: freezed == deletedAt
|
deletedAt: freezed == deletedAt
|
||||||
? _self.deletedAt
|
? _self.deletedAt
|
||||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as DateTime?,
|
||||||
thumbnail: null == thumbnail
|
thumbnail: null == thumbnail
|
||||||
? _self.thumbnail
|
? _self.thumbnail
|
||||||
: thumbnail // ignore: cast_nullable_to_non_nullable
|
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -412,22 +549,36 @@ class _$SnNewsArticleCopyWithImpl<$Res>
|
|||||||
? _self.hash
|
? _self.hash
|
||||||
: hash // ignore: cast_nullable_to_non_nullable
|
: hash // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
source: null == source
|
feedId: null == feedId
|
||||||
? _self.source
|
? _self.feedId
|
||||||
: source // ignore: cast_nullable_to_non_nullable
|
: feedId // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as int,
|
||||||
|
feed: null == feed
|
||||||
|
? _self.feed
|
||||||
|
: feed // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnSubscriptionFeed,
|
||||||
publishedAt: freezed == publishedAt
|
publishedAt: freezed == publishedAt
|
||||||
? _self.publishedAt
|
? _self.publishedAt
|
||||||
: publishedAt // ignore: cast_nullable_to_non_nullable
|
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnSubscriptionItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnSubscriptionFeedCopyWith<$Res> get feed {
|
||||||
|
return $SnSubscriptionFeedCopyWith<$Res>(_self.feed, (value) {
|
||||||
|
return _then(_self.copyWith(feed: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _SnNewsArticle implements SnNewsArticle {
|
class _SnSubscriptionItem implements SnSubscriptionItem {
|
||||||
const _SnNewsArticle(
|
const _SnSubscriptionItem(
|
||||||
{required this.id,
|
{required this.id,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
@@ -438,10 +589,11 @@ class _SnNewsArticle implements SnNewsArticle {
|
|||||||
required this.content,
|
required this.content,
|
||||||
required this.url,
|
required this.url,
|
||||||
required this.hash,
|
required this.hash,
|
||||||
required this.source,
|
required this.feedId,
|
||||||
|
required this.feed,
|
||||||
required this.publishedAt});
|
required this.publishedAt});
|
||||||
factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =>
|
factory _SnSubscriptionItem.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnNewsArticleFromJson(json);
|
_$SnSubscriptionItemFromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final int id;
|
final int id;
|
||||||
@@ -450,7 +602,7 @@ class _SnNewsArticle implements SnNewsArticle {
|
|||||||
@override
|
@override
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
@override
|
@override
|
||||||
final dynamic deletedAt;
|
final DateTime? deletedAt;
|
||||||
@override
|
@override
|
||||||
final String thumbnail;
|
final String thumbnail;
|
||||||
@override
|
@override
|
||||||
@@ -464,21 +616,23 @@ class _SnNewsArticle implements SnNewsArticle {
|
|||||||
@override
|
@override
|
||||||
final String hash;
|
final String hash;
|
||||||
@override
|
@override
|
||||||
final String source;
|
final int feedId;
|
||||||
|
@override
|
||||||
|
final SnSubscriptionFeed feed;
|
||||||
@override
|
@override
|
||||||
final DateTime? publishedAt;
|
final DateTime? publishedAt;
|
||||||
|
|
||||||
/// Create a copy of SnNewsArticle
|
/// Create a copy of SnSubscriptionItem
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
_$SnNewsArticleCopyWith<_SnNewsArticle> get copyWith =>
|
_$SnSubscriptionItemCopyWith<_SnSubscriptionItem> get copyWith =>
|
||||||
__$SnNewsArticleCopyWithImpl<_SnNewsArticle>(this, _$identity);
|
__$SnSubscriptionItemCopyWithImpl<_SnSubscriptionItem>(this, _$identity);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return _$SnNewsArticleToJson(
|
return _$SnSubscriptionItemToJson(
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -487,13 +641,14 @@ class _SnNewsArticle implements SnNewsArticle {
|
|||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other.runtimeType == runtimeType &&
|
(other.runtimeType == runtimeType &&
|
||||||
other is _SnNewsArticle &&
|
other is _SnSubscriptionItem &&
|
||||||
(identical(other.id, id) || other.id == id) &&
|
(identical(other.id, id) || other.id == id) &&
|
||||||
(identical(other.createdAt, createdAt) ||
|
(identical(other.createdAt, createdAt) ||
|
||||||
other.createdAt == createdAt) &&
|
other.createdAt == createdAt) &&
|
||||||
(identical(other.updatedAt, updatedAt) ||
|
(identical(other.updatedAt, updatedAt) ||
|
||||||
other.updatedAt == updatedAt) &&
|
other.updatedAt == updatedAt) &&
|
||||||
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
(identical(other.deletedAt, deletedAt) ||
|
||||||
|
other.deletedAt == deletedAt) &&
|
||||||
(identical(other.thumbnail, thumbnail) ||
|
(identical(other.thumbnail, thumbnail) ||
|
||||||
other.thumbnail == thumbnail) &&
|
other.thumbnail == thumbnail) &&
|
||||||
(identical(other.title, title) || other.title == title) &&
|
(identical(other.title, title) || other.title == title) &&
|
||||||
@@ -502,7 +657,8 @@ class _SnNewsArticle implements SnNewsArticle {
|
|||||||
(identical(other.content, content) || other.content == content) &&
|
(identical(other.content, content) || other.content == content) &&
|
||||||
(identical(other.url, url) || other.url == url) &&
|
(identical(other.url, url) || other.url == url) &&
|
||||||
(identical(other.hash, hash) || other.hash == hash) &&
|
(identical(other.hash, hash) || other.hash == hash) &&
|
||||||
(identical(other.source, source) || other.source == source) &&
|
(identical(other.feedId, feedId) || other.feedId == feedId) &&
|
||||||
|
(identical(other.feed, feed) || other.feed == feed) &&
|
||||||
(identical(other.publishedAt, publishedAt) ||
|
(identical(other.publishedAt, publishedAt) ||
|
||||||
other.publishedAt == publishedAt));
|
other.publishedAt == publishedAt));
|
||||||
}
|
}
|
||||||
@@ -514,54 +670,59 @@ class _SnNewsArticle implements SnNewsArticle {
|
|||||||
id,
|
id,
|
||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
const DeepCollectionEquality().hash(deletedAt),
|
deletedAt,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
content,
|
content,
|
||||||
url,
|
url,
|
||||||
hash,
|
hash,
|
||||||
source,
|
feedId,
|
||||||
|
feed,
|
||||||
publishedAt);
|
publishedAt);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnNewsArticle(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, source: $source, publishedAt: $publishedAt)';
|
return 'SnSubscriptionItem(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, feedId: $feedId, feed: $feed, publishedAt: $publishedAt)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
abstract mixin class _$SnNewsArticleCopyWith<$Res>
|
abstract mixin class _$SnSubscriptionItemCopyWith<$Res>
|
||||||
implements $SnNewsArticleCopyWith<$Res> {
|
implements $SnSubscriptionItemCopyWith<$Res> {
|
||||||
factory _$SnNewsArticleCopyWith(
|
factory _$SnSubscriptionItemCopyWith(
|
||||||
_SnNewsArticle value, $Res Function(_SnNewsArticle) _then) =
|
_SnSubscriptionItem value, $Res Function(_SnSubscriptionItem) _then) =
|
||||||
__$SnNewsArticleCopyWithImpl;
|
__$SnSubscriptionItemCopyWithImpl;
|
||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{int id,
|
{int id,
|
||||||
DateTime createdAt,
|
DateTime createdAt,
|
||||||
DateTime updatedAt,
|
DateTime updatedAt,
|
||||||
dynamic deletedAt,
|
DateTime? deletedAt,
|
||||||
String thumbnail,
|
String thumbnail,
|
||||||
String title,
|
String title,
|
||||||
String description,
|
String description,
|
||||||
String content,
|
String content,
|
||||||
String url,
|
String url,
|
||||||
String hash,
|
String hash,
|
||||||
String source,
|
int feedId,
|
||||||
|
SnSubscriptionFeed feed,
|
||||||
DateTime? publishedAt});
|
DateTime? publishedAt});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$SnSubscriptionFeedCopyWith<$Res> get feed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
class __$SnNewsArticleCopyWithImpl<$Res>
|
class __$SnSubscriptionItemCopyWithImpl<$Res>
|
||||||
implements _$SnNewsArticleCopyWith<$Res> {
|
implements _$SnSubscriptionItemCopyWith<$Res> {
|
||||||
__$SnNewsArticleCopyWithImpl(this._self, this._then);
|
__$SnSubscriptionItemCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
final _SnNewsArticle _self;
|
final _SnSubscriptionItem _self;
|
||||||
final $Res Function(_SnNewsArticle) _then;
|
final $Res Function(_SnSubscriptionItem) _then;
|
||||||
|
|
||||||
/// Create a copy of SnNewsArticle
|
/// Create a copy of SnSubscriptionItem
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@@ -576,10 +737,11 @@ class __$SnNewsArticleCopyWithImpl<$Res>
|
|||||||
Object? content = null,
|
Object? content = null,
|
||||||
Object? url = null,
|
Object? url = null,
|
||||||
Object? hash = null,
|
Object? hash = null,
|
||||||
Object? source = null,
|
Object? feedId = null,
|
||||||
|
Object? feed = null,
|
||||||
Object? publishedAt = freezed,
|
Object? publishedAt = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_SnNewsArticle(
|
return _then(_SnSubscriptionItem(
|
||||||
id: null == id
|
id: null == id
|
||||||
? _self.id
|
? _self.id
|
||||||
: id // ignore: cast_nullable_to_non_nullable
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -595,7 +757,7 @@ class __$SnNewsArticleCopyWithImpl<$Res>
|
|||||||
deletedAt: freezed == deletedAt
|
deletedAt: freezed == deletedAt
|
||||||
? _self.deletedAt
|
? _self.deletedAt
|
||||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as dynamic,
|
as DateTime?,
|
||||||
thumbnail: null == thumbnail
|
thumbnail: null == thumbnail
|
||||||
? _self.thumbnail
|
? _self.thumbnail
|
||||||
: thumbnail // ignore: cast_nullable_to_non_nullable
|
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -620,16 +782,30 @@ class __$SnNewsArticleCopyWithImpl<$Res>
|
|||||||
? _self.hash
|
? _self.hash
|
||||||
: hash // ignore: cast_nullable_to_non_nullable
|
: hash // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
source: null == source
|
feedId: null == feedId
|
||||||
? _self.source
|
? _self.feedId
|
||||||
: source // ignore: cast_nullable_to_non_nullable
|
: feedId // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as int,
|
||||||
|
feed: null == feed
|
||||||
|
? _self.feed
|
||||||
|
: feed // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SnSubscriptionFeed,
|
||||||
publishedAt: freezed == publishedAt
|
publishedAt: freezed == publishedAt
|
||||||
? _self.publishedAt
|
? _self.publishedAt
|
||||||
: publishedAt // ignore: cast_nullable_to_non_nullable
|
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnSubscriptionItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnSubscriptionFeedCopyWith<$Res> get feed {
|
||||||
|
return $SnSubscriptionFeedCopyWith<$Res>(_self.feed, (value) {
|
||||||
|
return _then(_self.copyWith(feed: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dart format on
|
// dart format on
|
||||||
|
|||||||
@@ -6,56 +6,74 @@ part of 'news.dart';
|
|||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
_SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) =>
|
_SnSubscriptionFeed _$SnSubscriptionFeedFromJson(Map<String, dynamic> json) =>
|
||||||
_SnNewsSource(
|
_SnSubscriptionFeed(
|
||||||
id: json['id'] as String,
|
|
||||||
label: json['label'] as String,
|
|
||||||
type: json['type'] as String,
|
|
||||||
source: json['source'] as String,
|
|
||||||
depth: (json['depth'] as num).toInt(),
|
|
||||||
enabled: json['enabled'] as bool,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SnNewsSourceToJson(_SnNewsSource instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'label': instance.label,
|
|
||||||
'type': instance.type,
|
|
||||||
'source': instance.source,
|
|
||||||
'depth': instance.depth,
|
|
||||||
'enabled': instance.enabled,
|
|
||||||
};
|
|
||||||
|
|
||||||
_SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) =>
|
|
||||||
_SnNewsArticle(
|
|
||||||
id: (json['id'] as num).toInt(),
|
id: (json['id'] as num).toInt(),
|
||||||
createdAt: DateTime.parse(json['created_at'] as String),
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
deletedAt: json['deleted_at'],
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
url: json['url'] as String,
|
||||||
|
isEnabled: json['is_enabled'] as bool,
|
||||||
|
isFullContent: json['is_full_content'] as bool,
|
||||||
|
pullInterval: (json['pull_interval'] as num).toInt(),
|
||||||
|
adapter: json['adapter'] as String,
|
||||||
|
accountId: (json['account_id'] as num?)?.toInt(),
|
||||||
|
lastFetchedAt: json['last_fetched_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['last_fetched_at'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SnSubscriptionFeedToJson(_SnSubscriptionFeed instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'url': instance.url,
|
||||||
|
'is_enabled': instance.isEnabled,
|
||||||
|
'is_full_content': instance.isFullContent,
|
||||||
|
'pull_interval': instance.pullInterval,
|
||||||
|
'adapter': instance.adapter,
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'last_fetched_at': instance.lastFetchedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_SnSubscriptionItem _$SnSubscriptionItemFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SnSubscriptionItem(
|
||||||
|
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),
|
||||||
thumbnail: json['thumbnail'] as String,
|
thumbnail: json['thumbnail'] as String,
|
||||||
title: json['title'] as String,
|
title: json['title'] as String,
|
||||||
description: json['description'] as String,
|
description: json['description'] as String,
|
||||||
content: json['content'] as String,
|
content: json['content'] as String,
|
||||||
url: json['url'] as String,
|
url: json['url'] as String,
|
||||||
hash: json['hash'] as String,
|
hash: json['hash'] as String,
|
||||||
source: json['source'] as String,
|
feedId: (json['feed_id'] as num).toInt(),
|
||||||
|
feed: SnSubscriptionFeed.fromJson(json['feed'] as Map<String, dynamic>),
|
||||||
publishedAt: json['published_at'] == null
|
publishedAt: json['published_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['published_at'] as String),
|
: DateTime.parse(json['published_at'] as String),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnNewsArticleToJson(_SnNewsArticle instance) =>
|
Map<String, dynamic> _$SnSubscriptionItemToJson(_SnSubscriptionItem instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'created_at': instance.createdAt.toIso8601String(),
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
'deleted_at': instance.deletedAt,
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
'thumbnail': instance.thumbnail,
|
'thumbnail': instance.thumbnail,
|
||||||
'title': instance.title,
|
'title': instance.title,
|
||||||
'description': instance.description,
|
'description': instance.description,
|
||||||
'content': instance.content,
|
'content': instance.content,
|
||||||
'url': instance.url,
|
'url': instance.url,
|
||||||
'hash': instance.hash,
|
'hash': instance.hash,
|
||||||
'source': instance.source,
|
'feed_id': instance.feedId,
|
||||||
|
'feed': instance.feed.toJson(),
|
||||||
'published_at': instance.publishedAt?.toIso8601String(),
|
'published_at': instance.publishedAt?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) =>
|
||||||
@@ -178,41 +181,3 @@ abstract class SnFeedEntry with _$SnFeedEntry {
|
|||||||
factory SnFeedEntry.fromJson(Map<String, dynamic> json) =>
|
factory SnFeedEntry.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnFeedEntryFromJson(json);
|
_$SnFeedEntryFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
|
||||||
abstract class SnFediversePost with _$SnFediversePost {
|
|
||||||
const factory SnFediversePost({
|
|
||||||
required int id,
|
|
||||||
required DateTime createdAt,
|
|
||||||
required DateTime updatedAt,
|
|
||||||
required DateTime? deletedAt,
|
|
||||||
required String identifier,
|
|
||||||
required String origin,
|
|
||||||
required String content,
|
|
||||||
required String language,
|
|
||||||
required List<String> images,
|
|
||||||
required SnFediverseUser user,
|
|
||||||
required int userId,
|
|
||||||
}) = _SnFediversePost;
|
|
||||||
|
|
||||||
factory SnFediversePost.fromJson(Map<String, Object?> json) =>
|
|
||||||
_$SnFediversePostFromJson(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
abstract class SnFediverseUser with _$SnFediverseUser {
|
|
||||||
const factory SnFediverseUser({
|
|
||||||
required int id,
|
|
||||||
required DateTime createdAt,
|
|
||||||
required DateTime updatedAt,
|
|
||||||
required DateTime? deletedAt,
|
|
||||||
required String identifier,
|
|
||||||
required String origin,
|
|
||||||
required String avatar,
|
|
||||||
required String name,
|
|
||||||
required String nick,
|
|
||||||
}) = _SnFediverseUser;
|
|
||||||
|
|
||||||
factory SnFediverseUser.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SnFediverseUserFromJson(json);
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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) =>
|
||||||
@@ -295,64 +303,3 @@ Map<String, dynamic> _$SnFeedEntryToJson(_SnFeedEntry instance) =>
|
|||||||
'data': instance.data,
|
'data': instance.data,
|
||||||
'created_at': instance.createdAt.toIso8601String(),
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
_SnFediversePost _$SnFediversePostFromJson(Map<String, dynamic> json) =>
|
|
||||||
_SnFediversePost(
|
|
||||||
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),
|
|
||||||
identifier: json['identifier'] as String,
|
|
||||||
origin: json['origin'] as String,
|
|
||||||
content: json['content'] as String,
|
|
||||||
language: json['language'] as String,
|
|
||||||
images:
|
|
||||||
(json['images'] as List<dynamic>).map((e) => e as String).toList(),
|
|
||||||
user: SnFediverseUser.fromJson(json['user'] as Map<String, dynamic>),
|
|
||||||
userId: (json['user_id'] as num).toInt(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SnFediversePostToJson(_SnFediversePost instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'created_at': instance.createdAt.toIso8601String(),
|
|
||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
|
||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
|
||||||
'identifier': instance.identifier,
|
|
||||||
'origin': instance.origin,
|
|
||||||
'content': instance.content,
|
|
||||||
'language': instance.language,
|
|
||||||
'images': instance.images,
|
|
||||||
'user': instance.user.toJson(),
|
|
||||||
'user_id': instance.userId,
|
|
||||||
};
|
|
||||||
|
|
||||||
_SnFediverseUser _$SnFediverseUserFromJson(Map<String, dynamic> json) =>
|
|
||||||
_SnFediverseUser(
|
|
||||||
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),
|
|
||||||
identifier: json['identifier'] as String,
|
|
||||||
origin: json['origin'] as String,
|
|
||||||
avatar: json['avatar'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
nick: json['nick'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SnFediverseUserToJson(_SnFediverseUser instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'created_at': instance.createdAt.toIso8601String(),
|
|
||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
|
||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
|
||||||
'identifier': instance.identifier,
|
|
||||||
'origin': instance.origin,
|
|
||||||
'avatar': instance.avatar,
|
|
||||||
'name': instance.name,
|
|
||||||
'nick': instance.nick,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user