Compare commits
53 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a3aa694076 | ||
|
e98ee562ef | ||
63ff6df93a | |||
f95eadd3e6 | |||
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 | ||
|
ddd0a4c3d3 | ||
|
99e07de243 | ||
|
10bf0883e5 |
16
.github/workflows/nightly.yml
vendored
16
.github/workflows/nightly.yml
vendored
@ -48,7 +48,6 @@ jobs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
cache: true
|
||||
- run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev
|
||||
@ -65,3 +64,18 @@ jobs:
|
||||
with:
|
||||
name: build-output-linux
|
||||
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
|
||||
|
||||
@ -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 Wallet Service: [Wallet](https://github.com/Solsynth/HyperNet.Wallet)
|
||||
- The Crawler: [Reader](https://github.com/Solsynth/HyperNet.Reader)
|
||||
- Some others may not be listed, you can search in the organization with `HyperNet.` the prefix of all HyperNet projects.
|
||||
- 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
|
||||
|
||||
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.
|
||||
|
||||
-----
|
||||
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.
|
2
android/.gitignore
vendored
2
android/.gitignore
vendored
@ -11,3 +11,5 @@ GeneratedPluginRegistrant.java
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
||||
app/.cxx
|
@ -10,10 +10,15 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// implementation('org.jitsi.react:jitsi-meet-sdk:11.1.1') { transitive = true }
|
||||
// implementation 'com.facebook.fresco:webpsupport:2.6.0'
|
||||
// implementation 'com.facebook.fresco:animated-webp:2.6.0'
|
||||
// implementation 'com.facebook.react:react-android:0.75.5'
|
||||
// implementation 'com.facebook.react:hermes-android:0.75.5'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.glance:glance:1.1.1'
|
||||
implementation 'androidx.glance:glance-appwidget:1.1.1'
|
||||
implementation 'androidx.compose.foundation:foundation-layout-android:1.7.6'
|
||||
implementation 'androidx.compose.foundation:foundation-layout-android:1.7.8'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'io.coil-kt.coil3:coil-compose:3.0.4'
|
||||
@ -73,8 +78,10 @@ android {
|
||||
}
|
||||
release {
|
||||
signingConfig = signingConfigs.release
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
96
android/app/proguard-rules.pro
vendored
Normal file
96
android/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,96 @@
|
||||
-keepclassmembers class kotlin.Metadata { *; }
|
||||
-keep class dev.solsynth.solian.** { *; }
|
||||
-keep public class dev.solsynth.solian.data.** { public *; }
|
||||
-keepclassmembers class dev.solsynth.solian.data.** { *; }
|
||||
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes Signature
|
||||
-keepattributes EnclosingMethod
|
||||
|
||||
-keep class com.google.gson.** { *; }
|
||||
|
||||
-keepclassmembers class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
-dontwarn com.facebook.imagepipeline.nativecode.WebpTranscoder
|
||||
|
||||
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
|
||||
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
|
||||
|
||||
# Do not strip any method/class that is annotated with @DoNotStrip
|
||||
-keep @com.facebook.proguard.annotations.DoNotStrip class *
|
||||
-keepclassmembers class * {
|
||||
@com.facebook.proguard.annotations.DoNotStrip *;
|
||||
}
|
||||
|
||||
-keep @com.facebook.proguard.annotations.DoNotStripAny class * {
|
||||
*;
|
||||
}
|
||||
|
||||
-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
|
||||
void set*(***);
|
||||
*** get*();
|
||||
}
|
||||
|
||||
-keep class * implements com.facebook.react.bridge.JavaScriptModule { *; }
|
||||
-keep class * implements com.facebook.react.bridge.NativeModule { *; }
|
||||
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
|
||||
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
|
||||
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
|
||||
|
||||
-dontwarn com.facebook.react.**
|
||||
-keep,includedescriptorclasses class com.facebook.react.bridge.** { *; }
|
||||
-keep,includedescriptorclasses class com.facebook.react.turbomodule.core.** { *; }
|
||||
|
||||
# hermes
|
||||
-keep class com.facebook.jni.** { *; }
|
||||
|
||||
# okio
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
-dontwarn java.nio.file.*
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
-dontwarn okio.**
|
||||
|
||||
# yoga
|
||||
-keep,allowobfuscation @interface com.facebook.yoga.annotations.DoNotStrip
|
||||
-keep @com.facebook.yoga.annotations.DoNotStrip class *
|
||||
-keepclassmembers class * {
|
||||
@com.facebook.yoga.annotations.DoNotStrip *;
|
||||
}
|
||||
|
||||
# WebRTC
|
||||
|
||||
-keep class org.webrtc.** { *; }
|
||||
-dontwarn org.chromium.build.BuildHooksAndroid
|
||||
|
||||
# Jisti Meet SDK
|
||||
|
||||
-keep class org.jitsi.meet.** { *; }
|
||||
-keep class org.jitsi.meet.sdk.** { *; }
|
||||
|
||||
# We added the following when we switched minifyEnabled on. Probably because we
|
||||
# ran the app and hit problems...
|
||||
|
||||
-keep class com.facebook.react.bridge.CatalystInstanceImpl { *; }
|
||||
-keep class com.facebook.react.bridge.ExecutorToken { *; }
|
||||
-keep class com.facebook.react.bridge.JavaScriptExecutor { *; }
|
||||
-keep class com.facebook.react.bridge.ModuleRegistryHolder { *; }
|
||||
-keep class com.facebook.react.bridge.ReadableType { *; }
|
||||
-keep class com.facebook.react.bridge.queue.NativeRunnable { *; }
|
||||
-keep class com.facebook.react.devsupport.** { *; }
|
||||
|
||||
-dontwarn com.facebook.react.devsupport.**
|
||||
-dontwarn com.google.appengine.**
|
||||
-dontwarn com.squareup.okhttp.**
|
||||
-dontwarn javax.servlet.**
|
||||
|
||||
# ^^^ We added the above when we switched minifyEnabled on.
|
||||
|
||||
# Rule to avoid build errors related to SVGs.
|
||||
-keep public class com.horcrux.svg.** {*;}
|
||||
|
||||
# https://github.com/facebook/fresco/issues/2638
|
||||
-keep public class com.facebook.imageutils.** {
|
||||
public *;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@ -9,11 +9,13 @@
|
||||
<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_ADMIN" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
tools:replace="android:label"
|
||||
android:label="Solian"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
14
android/app/src/proguard-rules.pro
vendored
14
android/app/src/proguard-rules.pro
vendored
@ -1,14 +0,0 @@
|
||||
-keepclassmembers class kotlin.Metadata { *; }
|
||||
-keep class dev.solsynth.solian.** { *; }
|
||||
-keep public class dev.solsynth.solian.data.** { public *; }
|
||||
-keepclassmembers class dev.solsynth.solian.data.** { *; }
|
||||
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes Signature
|
||||
-keepattributes EnclosingMethod
|
||||
|
||||
-keep class com.google.gson.** { *; }
|
||||
|
||||
-keepclassmembers class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
|
@ -10,18 +10,22 @@ pluginManagement {
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version '8.7.3' apply false
|
||||
id "com.android.application" version '8.9.1' apply false
|
||||
// START: FlutterFire Configuration
|
||||
id "com.google.gms.google-services" version "4.3.15" apply false
|
||||
id "com.google.firebase.crashlytics" version "2.8.1" apply false
|
||||
id "com.google.gms.google-services" version "4.4.2" apply false
|
||||
id "com.google.firebase.crashlytics" version "3.0.3" apply false
|
||||
// END: FlutterFire Configuration
|
||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||
}
|
||||
|
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 {
|
||||
{
|
||||
"sources": ["taiwan-pts"],
|
||||
"eager": true
|
||||
}
|
||||
{}
|
||||
}
|
||||
|
@ -543,6 +543,7 @@
|
||||
"attachmentSaved": "Saved to album",
|
||||
"attachmentSavedDesktop": "Saved to Downloads folder",
|
||||
"openInAlbum": "Open in album",
|
||||
"openInBrowser": "Open in browser",
|
||||
"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.",
|
||||
"abuseReport": "Abuse Report",
|
||||
@ -941,5 +942,28 @@
|
||||
"settingsResetMemorizedWindowSize": "Reset Window Size",
|
||||
"settingsResetMemorizedWindowSizeDescription": "Reset the memorized window size, and set it to the default size.",
|
||||
"chatDirect": "Direct Messages",
|
||||
"back": "返回"
|
||||
"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",
|
||||
"fieldPostVideoUrl": "Video URL",
|
||||
"fieldPostVideoUrlDescription": "The URL of the video content, it can be a webpage, and will be rendered by iFrame / WebView."
|
||||
}
|
||||
|
@ -541,6 +541,7 @@
|
||||
"attachmentSaved": "已保存到相册",
|
||||
"attachmentSavedDesktop": "已保存到下载目录",
|
||||
"openInAlbum": "在相册中打开",
|
||||
"openInBrowser": "在浏览器中打开",
|
||||
"postAbuseReport": "检举帖子",
|
||||
"postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
|
||||
"abuseReport": "检举",
|
||||
@ -938,5 +939,28 @@
|
||||
"settingsResetMemorizedWindowSize": "重置窗口大小",
|
||||
"settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。",
|
||||
"chatDirect": "私信",
|
||||
"back": "返回"
|
||||
"back": "返回",
|
||||
"badgeProgramDeveloper": "开发者计划成员",
|
||||
"badgeProgramStellar": "一颗恒星",
|
||||
"badgeProgramModerator": "社区管理员",
|
||||
"postEditedHint": "已编辑",
|
||||
"splashScreenServer": "服务器",
|
||||
"splashScreenServerName": "土豆",
|
||||
"splashScreenCaption": "正在尝试与 HyperNet™ 取得太阳链连接",
|
||||
"attachmentEditor": "附件编辑器",
|
||||
"attachmentEditorUnUploadHint": "该附件未上传,元数据编辑不可用,同时你可以裁剪本附件。",
|
||||
"attachmentEditorUploadHint": "该附件已上传。",
|
||||
"attachmentRating": "评级",
|
||||
"fieldAttachmentRating": "内容分级",
|
||||
"fieldAttachmentQuality": "质量评分",
|
||||
"attachmentReferenceLink": "引用外部附件",
|
||||
"fieldAttachmentReferenceLink": "引用连接",
|
||||
"attachmentReferenceLinkDescription": "作为附件的源文件。需要链接允许跨域访问。",
|
||||
"fieldAttachmentMimetype": "文件类型",
|
||||
"postVideoLive": "直播",
|
||||
"postVideoLiveDescription": "这是一条直播影片,允许用户自行嵌入源站。",
|
||||
"postVideoRendererWeb": "网页渲染器",
|
||||
"postVideoRendererWebDescription": "使用 WebView 渲染内容。",
|
||||
"fieldPostVideoUrl": "视频流地址",
|
||||
"fieldPostVideoUrlDescription": "视频内容的地址,可以为网页,将会使用 iFrame / WebView 渲染。"
|
||||
}
|
||||
|
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
|
||||
platform :ios, '13.0'
|
||||
platform :ios, '15.1'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
145
ios/Podfile.lock
145
ios/Podfile.lock
@ -46,58 +46,58 @@ PODS:
|
||||
- Flutter
|
||||
- file_saver (0.0.1):
|
||||
- Flutter
|
||||
- Firebase/Analytics (11.8.0):
|
||||
- Firebase/Analytics (11.10.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (11.8.0):
|
||||
- Firebase/Core (11.10.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 11.8.0)
|
||||
- Firebase/CoreOnly (11.8.0):
|
||||
- FirebaseCore (~> 11.8.0)
|
||||
- Firebase/Messaging (11.8.0):
|
||||
- FirebaseAnalytics (~> 11.10.0)
|
||||
- Firebase/CoreOnly (11.10.0):
|
||||
- FirebaseCore (~> 11.10.0)
|
||||
- Firebase/Messaging (11.10.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.8.0)
|
||||
- firebase_analytics (11.4.4):
|
||||
- Firebase/Analytics (= 11.8.0)
|
||||
- FirebaseMessaging (~> 11.10.0)
|
||||
- firebase_analytics (11.4.5):
|
||||
- Firebase/Analytics (= 11.10.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_core (3.12.1):
|
||||
- Firebase/CoreOnly (= 11.8.0)
|
||||
- firebase_core (3.13.0):
|
||||
- Firebase/CoreOnly (= 11.10.0)
|
||||
- Flutter
|
||||
- firebase_messaging (15.2.4):
|
||||
- Firebase/Messaging (= 11.8.0)
|
||||
- firebase_messaging (15.2.5):
|
||||
- Firebase/Messaging (= 11.10.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseAnalytics (11.8.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.8.0)
|
||||
- FirebaseCore (~> 11.8.0)
|
||||
- FirebaseAnalytics (11.10.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.10.0)
|
||||
- FirebaseCore (~> 11.10.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.8.0):
|
||||
- FirebaseCore (~> 11.8.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.10.0):
|
||||
- FirebaseCore (~> 11.10.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleAppMeasurement (= 11.8.0)
|
||||
- GoogleAppMeasurement (= 11.10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (11.8.1):
|
||||
- FirebaseCoreInternal (~> 11.8.0)
|
||||
- FirebaseCore (11.10.0):
|
||||
- FirebaseCoreInternal (~> 11.10.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- FirebaseCoreInternal (11.8.0):
|
||||
- FirebaseCoreInternal (11.10.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseInstallations (11.8.0):
|
||||
- FirebaseCore (~> 11.8.0)
|
||||
- FirebaseInstallations (11.10.0):
|
||||
- FirebaseCore (~> 11.10.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (11.8.0):
|
||||
- FirebaseCore (~> 11.8.0)
|
||||
- FirebaseMessaging (11.10.0):
|
||||
- FirebaseCore (~> 11.10.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -122,27 +122,26 @@ PODS:
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- flutter_webrtc (0.12.6):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleAppMeasurement (11.8.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.8.0)
|
||||
- Giphy (2.2.12):
|
||||
- libwebp
|
||||
- GoogleAppMeasurement (11.10.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.8.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.8.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.10.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.8.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.10.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
@ -184,11 +183,26 @@ PODS:
|
||||
- Flutter
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- Kingfisher (8.2.0)
|
||||
- livekit_client (2.4.1):
|
||||
- jitsi_meet_flutter_sdk (11.1.1):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
- JitsiMeetSDK (= 11.1.1)
|
||||
- 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):
|
||||
- Flutter
|
||||
- media_kit_video (0.0.1):
|
||||
@ -212,9 +226,9 @@ PODS:
|
||||
- receive_sharing_intent (1.8.1):
|
||||
- Flutter
|
||||
- SAMKeychain (1.5.3)
|
||||
- SDWebImage (5.20.1):
|
||||
- SDWebImage/Core (= 5.20.1)
|
||||
- SDWebImage/Core (5.20.1)
|
||||
- SDWebImage (5.21.0):
|
||||
- SDWebImage/Core (= 5.21.0)
|
||||
- SDWebImage/Core (5.21.0)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@ -254,7 +268,6 @@ PODS:
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- WebRTC-SDK (125.6422.06)
|
||||
- workmanager (0.0.1):
|
||||
- Flutter
|
||||
|
||||
@ -276,13 +289,12 @@ DEPENDENCIES:
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/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)
|
||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
@ -311,10 +323,14 @@ SPEC REPOS:
|
||||
- FirebaseCoreInternal
|
||||
- FirebaseInstallations
|
||||
- FirebaseMessaging
|
||||
- Giphy
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- JitsiMeetSDK
|
||||
- JitsiWebRTC
|
||||
- Kingfisher
|
||||
- libwebp
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
@ -322,7 +338,6 @@ SPEC REPOS:
|
||||
- SDWebImage
|
||||
- sqlite3
|
||||
- SwiftyGif
|
||||
- WebRTC-SDK
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
audioplayers_darwin:
|
||||
@ -357,8 +372,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_timezone/ios"
|
||||
flutter_udid:
|
||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||
flutter_webrtc:
|
||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||
gal:
|
||||
:path: ".symlinks/plugins/gal/darwin"
|
||||
home_widget:
|
||||
@ -367,8 +380,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
in_app_review:
|
||||
:path: ".symlinks/plugins/in_app_review/ios"
|
||||
livekit_client:
|
||||
:path: ".symlinks/plugins/livekit_client/ios"
|
||||
jitsi_meet_flutter_sdk:
|
||||
:path: ".symlinks/plugins/jitsi_meet_flutter_sdk/ios"
|
||||
media_kit_libs_ios_video:
|
||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||
media_kit_video:
|
||||
@ -413,31 +426,34 @@ SPEC CHECKSUMS:
|
||||
fast_rsa: d99f8e1809a4a312fa9216d830186869b2e9eb65
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
||||
firebase_analytics: 4e93dbe66872104d28ae9750fec1800e1fd66858
|
||||
firebase_core: 8d552814f6c01ccde5d88939fced4ec26f2f5510
|
||||
firebase_messaging: 8b96a4f09841c15a16b96973ef5c3dcfc1a064e4
|
||||
FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b
|
||||
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
||||
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
||||
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
||||
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
|
||||
Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
|
||||
firebase_analytics: 1998960b8fa16fd0cd9e77a6f9fd35a2009ad65e
|
||||
firebase_core: 2d4534e7b489907dcede540c835b48981d890943
|
||||
firebase_messaging: 75bc93a4df25faccad67f6662ae872ac9ae69b64
|
||||
FirebaseAnalytics: 4e42333f02cf78ed93703a5c36f36dd518aebdef
|
||||
FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7
|
||||
FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679
|
||||
FirebaseInstallations: 9980995bdd06ec8081dfb6ab364162bdd64245c3
|
||||
FirebaseMessaging: 2b9f56aa4ed286e1f0ce2ee1d413aabb8f9f5cb9
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||
flutter_webrtc: 57f32415b8744e806f9c2a96ccdb60c6a627ba33
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
|
||||
Giphy: 83628960ed04e1c3428ff1b4fb2b027f65e82f50
|
||||
GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457
|
||||
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
|
||||
livekit_client: 08755cabfa4da4ed455642f460cfbb39bc518070
|
||||
jitsi_meet_flutter_sdk: 0283a60730922d608fbad9872e07afdd5bb3578a
|
||||
JitsiMeetSDK: 4e1c269aaaed8f2cb7b0fff2d3c00f08359b170e
|
||||
JitsiWebRTC: b47805ab5668be38e7ee60e2258f49badfe8e1d0
|
||||
Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
@ -449,7 +465,7 @@ SPEC CHECKSUMS:
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SDWebImage: 33d0f23bddeb5d209ae959153883247be6703713
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
@ -460,9 +476,8 @@ SPEC CHECKSUMS:
|
||||
video_compress: f2133a07762889d67f0711ac831faa26f956980e
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
||||
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
|
||||
|
||||
PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc
|
||||
PODFILE CHECKSUM: d278ce52a331dda323590121247d2046cd085ae7
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
@ -961,7 +961,7 @@
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -1521,7 +1521,7 @@
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -1549,7 +1549,7 @@
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@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">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
||||
<key>AppGroupId</key>
|
||||
<string>group.solsynth.solian</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
|
@ -219,6 +219,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||
DateTime? publishedAt, publishedUntil;
|
||||
SnAttachment? videoAttachment;
|
||||
String videoUrl = '';
|
||||
bool videoLive = false;
|
||||
SnPoll? poll;
|
||||
|
||||
Future<void> fetchRelatedPost(
|
||||
@ -241,7 +243,13 @@ class PostWriteController extends ChangeNotifier {
|
||||
contentController.text = post.body['content'] ?? '';
|
||||
aliasController.text = post.alias ?? '';
|
||||
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;
|
||||
publishedUntil = post.publishedUntil;
|
||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||
@ -252,17 +260,23 @@ class PostWriteController extends ChangeNotifier {
|
||||
categories =
|
||||
List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||
attachments.addAll(
|
||||
post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||
poll = post.preload?.poll;
|
||||
post.body['attachments']
|
||||
?.map((ele) => SnAttachment.fromJson(ele))
|
||||
?.map((ele) => PostWriteMedia(ele))
|
||||
?.cast<PostWriteMedia>() ??
|
||||
[],
|
||||
);
|
||||
poll = post.poll;
|
||||
|
||||
videoLive = post.body['is_live'] ?? false;
|
||||
editingDraft = post.isDraft;
|
||||
|
||||
if (post.preload?.thumbnail != null &&
|
||||
(post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||
thumbnail = PostWriteMedia(post.preload!.thumbnail);
|
||||
if (post.body['thumbnail'] != null) {
|
||||
thumbnail =
|
||||
PostWriteMedia(SnAttachment.fromJson(post.body['thumbnail']));
|
||||
}
|
||||
if (post.preload?.realm != null) {
|
||||
realm = post.preload!.realm!;
|
||||
if (post.realm != null) {
|
||||
realm = post.realm!;
|
||||
}
|
||||
|
||||
editingPost = post;
|
||||
@ -438,8 +452,9 @@ class PostWriteController extends ChangeNotifier {
|
||||
titleController.text = data['title'] ?? '';
|
||||
descriptionController.text = data['description'] ?? '';
|
||||
rewardController.text = data['reward']?.toString() ?? '';
|
||||
if (data['thumbnail'] != null)
|
||||
if (data['thumbnail'] != null) {
|
||||
thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||
}
|
||||
attachments.addAll(data['attachments']
|
||||
.map((ele) => PostWriteMedia(SnAttachment.fromJson(ele)))
|
||||
.cast<PostWriteMedia>());
|
||||
@ -448,10 +463,12 @@ class PostWriteController extends ChangeNotifier {
|
||||
visibility = data['visibility'];
|
||||
visibleUsers = List.from(data['visible_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();
|
||||
if (data['published_until'] != null)
|
||||
}
|
||||
if (data['published_until'] != null) {
|
||||
publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||
}
|
||||
replyingPost =
|
||||
data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||
repostingPost =
|
||||
@ -588,9 +605,11 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
||||
if (reward != null) 'reward': reward,
|
||||
if (videoAttachment != null) 'video': videoAttachment!.rid,
|
||||
if (videoAttachment != null || videoUrl.isNotEmpty)
|
||||
'video': videoUrl.isNotEmpty ? videoUrl : videoAttachment!.rid,
|
||||
if (poll != null) 'poll': poll!.id,
|
||||
if (realm != null) 'realm': realm!.id,
|
||||
if (videoLive) 'is_live': videoLive,
|
||||
'is_draft': saveAsDraft,
|
||||
},
|
||||
onSendProgress: (count, total) {
|
||||
@ -731,6 +750,16 @@ class PostWriteController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVideoUrl(String value) {
|
||||
videoUrl = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVideoLive(bool value) {
|
||||
videoLive = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPoll(SnPoll? value) {
|
||||
poll = value;
|
||||
notifyListeners();
|
||||
|
255
lib/main.dart
255
lib/main.dart
@ -15,6 +15,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:package_info_plus/package_info_plus.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/logger.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/database.dart';
|
||||
import 'package:surface/providers/keypair.dart';
|
||||
@ -49,6 +49,7 @@ import 'package:surface/router.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||
import 'package:surface/widgets/dialog.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:tray_manager/tray_manager.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_platform_interface/image_picker_platform_interface.dart';
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void appBackgroundDispatcher() {
|
||||
@ -195,7 +197,6 @@ class SolianApp extends StatelessWidget {
|
||||
Provider(create: (ctx) => KeyPairProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
|
||||
Provider(create: (ctx) => SnTranslator()),
|
||||
|
||||
// Additional helper layer
|
||||
@ -257,6 +258,7 @@ class _AppSplashScreen extends StatefulWidget {
|
||||
|
||||
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
bool _isBusy = false;
|
||||
double _initPercentage = 0;
|
||||
String _phaseText = 'appInitStarting';
|
||||
|
||||
void _tryRequestRating() async {
|
||||
@ -331,20 +333,24 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
// 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 also save initialize the Config, so it not need to be initialized again
|
||||
_initPercentage = 0.1;
|
||||
_setPhaseText('network');
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.initializeUserAgent();
|
||||
await sn.setConfigWithNative();
|
||||
if (!mounted) return;
|
||||
_initPercentage = 0.2;
|
||||
_setPhaseText('userdata');
|
||||
final ua = context.read<UserProvider>();
|
||||
await ua.initialize();
|
||||
if (!mounted) return;
|
||||
_initPercentage = 0.3;
|
||||
_setPhaseText('websocket');
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
await ws.tryConnect();
|
||||
try {
|
||||
if (!mounted) return;
|
||||
_initPercentage = 0.9;
|
||||
_setPhaseText('keyPair');
|
||||
final kp = context.read<KeyPairProvider>();
|
||||
kp.reloadActive();
|
||||
@ -357,7 +363,6 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
notify.listen();
|
||||
try {
|
||||
notify.registerPushNotifications();
|
||||
} catch (_) {}
|
||||
if (!mounted) return;
|
||||
_setPhaseText('stickers');
|
||||
final sticker = context.read<SnStickerProvider>();
|
||||
@ -374,7 +379,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
_setPhaseText('chat');
|
||||
final ct = context.read<ChatChannelProvider>();
|
||||
await ct.refreshAvailableChannels();
|
||||
_initPercentage = 1;
|
||||
_setPhaseText('done');
|
||||
} catch (_) {}
|
||||
_playIntro();
|
||||
}
|
||||
} catch (err) {
|
||||
@ -396,8 +403,22 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
if (!cfg.soundEffects) return;
|
||||
|
||||
final date = DateTime.now();
|
||||
final player = AudioPlayer(playerId: 'launch-done-player');
|
||||
await player.play(AssetSource('audio/sfx/launch-done.mp3'), volume: 0.8);
|
||||
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();
|
||||
});
|
||||
@ -456,6 +477,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
AppLifecycleListener(onExitRequested: _onExitRequested);
|
||||
}
|
||||
|
||||
try {
|
||||
_trayInitialization();
|
||||
_hotkeyInitialization();
|
||||
_notifyInitialization();
|
||||
@ -464,7 +486,13 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
_tryRequestRating();
|
||||
_checkForUpdate();
|
||||
setState(() => _isBusy = false);
|
||||
}).catchError((err) {
|
||||
logging.error('[Bootstrap] Unable to initialize app', err);
|
||||
setState(() => _isBusy = false);
|
||||
});
|
||||
} catch (err) {
|
||||
logging.error('[Bootstrap] Unable to initialize (pre-stage) app', err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<AppExitResponse> _onExitRequested() async {
|
||||
@ -555,8 +583,205 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
});
|
||||
return SizeChangedLayoutNotifier(
|
||||
child: _isBusy
|
||||
? Material(
|
||||
key: Key('app-splash-screen-$_isBusy'),
|
||||
? _AppLoadingScreen(
|
||||
isBusy: _isBusy,
|
||||
initPercentage: _initPercentage,
|
||||
phaseText: _phaseText,
|
||||
)
|
||||
: widget.child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppLoadingScreen extends StatelessWidget {
|
||||
const _AppLoadingScreen({
|
||||
required this.isBusy,
|
||||
required this.initPercentage,
|
||||
required this.phaseText,
|
||||
});
|
||||
|
||||
final bool isBusy;
|
||||
final double initPercentage;
|
||||
final String phaseText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (ResponsiveScaffold.getIsExpand(context)) {
|
||||
return Material(
|
||||
key: Key('app-splash-screen-$isBusy'),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/icon/kanban-1st.jpg'),
|
||||
fit: BoxFit.cover,
|
||||
opacity: 0.1,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
backgroundBlendMode: BlendMode.darken,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: initPercentage),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${(value * 100).toStringAsFixed(0)}%')
|
||||
.padding(left: 32, bottom: 4),
|
||||
LinearProgressIndicator(
|
||||
value: value,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(0),
|
||||
),
|
||||
stopIndicatorColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
const Gap(24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: initPercentage),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('${(value * 100).toStringAsFixed(0)}%')
|
||||
.padding(right: 32, bottom: 4),
|
||||
Transform.flip(
|
||||
flipX: true,
|
||||
child: LinearProgressIndicator(
|
||||
value: value,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(0),
|
||||
),
|
||||
stopIndicatorColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 240, minWidth: 160),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surface.withOpacity(0.85),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 3,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'splashScreenServer',
|
||||
style: GoogleFonts.notoSerifHk(height: 1, fontSize: 11),
|
||||
textAlign: TextAlign.center,
|
||||
).tr().opacity(0.85),
|
||||
Text(
|
||||
'splashScreenServerName',
|
||||
style: GoogleFonts.notoSerifHk(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
).tr().opacity(0.85),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: '#',
|
||||
style: GoogleFonts.notoSerifHk(),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '0',
|
||||
style: GoogleFonts.notoSerifHk(
|
||||
fontSize: 80,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
).padding(vertical: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: MediaQuery.of(context).size.height * 0.2,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
phaseText,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
AnimateWidgetExtensions(Text(
|
||||
'splashScreenCaption',
|
||||
textAlign: TextAlign.center,
|
||||
).tr())
|
||||
.animate(onPlay: (e) => e.repeat())
|
||||
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
|
||||
.then()
|
||||
.fadeOut(
|
||||
duration: 500.ms,
|
||||
delay: 1000.ms,
|
||||
curve: Curves.easeIn,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/icon/icon.png',
|
||||
width: 40,
|
||||
height: 40,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
).padding(all: 4),
|
||||
const Gap(4),
|
||||
Text('Solar Network').bold(),
|
||||
Expanded(child: const SizedBox()),
|
||||
AppVersionLabel(),
|
||||
const Gap(12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Material(
|
||||
key: Key('app-splash-screen-$isBusy'),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
@ -580,27 +805,25 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
'assets/icon/icon.png',
|
||||
width: 64,
|
||||
height: 64,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
Text('Solar Network').bold(),
|
||||
AppVersionLabel(),
|
||||
Gap(8),
|
||||
Text(_phaseText, textAlign: TextAlign.center),
|
||||
Text(phaseText, textAlign: TextAlign.center),
|
||||
Gap(16),
|
||||
const LinearProgressIndicator(),
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: initPercentage),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) =>
|
||||
LinearProgressIndicator(value: value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: widget.child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -152,7 +152,7 @@ class KeyPairProvider {
|
||||
|
||||
Future<SnKeyPair?> reloadActive({bool autoEnroll = true}) async {
|
||||
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.isActive.equals(true))
|
||||
..limit(1))
|
||||
|
@ -64,11 +64,6 @@ class NavigationProvider extends ChangeNotifier {
|
||||
screen: 'realm',
|
||||
label: 'screenRealm',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
|
||||
screen: 'news',
|
||||
label: 'screenNews',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.settings, weight: 400, opticalSize: 20),
|
||||
screen: 'settings',
|
||||
|
@ -106,6 +106,14 @@ class NotificationProvider extends ChangeNotifier {
|
||||
_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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,144 +1,31 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_realm.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
|
||||
class SnPostContentProvider {
|
||||
late final SnNetworkProvider _sn;
|
||||
late final UserDirectoryProvider _ud;
|
||||
late final SnAttachmentProvider _attach;
|
||||
late final SnRealmProvider _realm;
|
||||
|
||||
SnPostContentProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
_ud = context.read<UserDirectoryProvider>();
|
||||
_attach = context.read<SnAttachmentProvider>();
|
||||
_realm = context.read<SnRealmProvider>();
|
||||
}
|
||||
|
||||
Future<SnPoll> _fetchPoll(int id) async {
|
||||
final resp = await _sn.client.get('/cgi/co/polls/$id');
|
||||
return SnPoll.fromJson(resp.data);
|
||||
}
|
||||
|
||||
Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
|
||||
Set<String> rids = {};
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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(
|
||||
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 {
|
||||
final resp =
|
||||
await _sn.client.get('/cgi/co/recommendations/feed', queryParameters: {
|
||||
final resp = await _sn.client.get(
|
||||
'/cgi/co/recommendations/feed',
|
||||
queryParameters: {
|
||||
'take': take,
|
||||
if (cursor != null) 'cursor': cursor.toUtc().millisecondsSinceEpoch,
|
||||
});
|
||||
},
|
||||
options: Options(headers: {'X-API-Version': '2'}),
|
||||
);
|
||||
final List<SnFeedEntry> out =
|
||||
List.from(resp.data.map((ele) => SnFeedEntry.fromJson(ele)));
|
||||
|
||||
@ -202,6 +92,9 @@ class SnPostContentProvider {
|
||||
if (realm != null) 'realm': realm,
|
||||
if (channel != null) 'channel': channel,
|
||||
},
|
||||
options: Options(headers: {
|
||||
'X-API-Version': '2',
|
||||
}),
|
||||
);
|
||||
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||
@ -215,11 +108,16 @@ class SnPostContentProvider {
|
||||
int take = 10,
|
||||
int offset = 0,
|
||||
}) async {
|
||||
final resp = await _sn.client
|
||||
.get('/cgi/co/posts/$parentId/replies', queryParameters: {
|
||||
final resp = await _sn.client.get(
|
||||
'/cgi/co/posts/$parentId/replies',
|
||||
queryParameters: {
|
||||
'take': take,
|
||||
'offset': offset,
|
||||
});
|
||||
},
|
||||
options: Options(headers: {
|
||||
'X-API-Version': '2',
|
||||
}),
|
||||
);
|
||||
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||
);
|
||||
@ -234,13 +132,20 @@ class SnPostContentProvider {
|
||||
Iterable<String>? tags,
|
||||
Iterable<String>? categories,
|
||||
}) 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,
|
||||
'offset': offset,
|
||||
'probe': searchTerm,
|
||||
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(
|
||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||
);
|
||||
@ -249,7 +154,12 @@ class SnPostContentProvider {
|
||||
}
|
||||
|
||||
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(
|
||||
SnPost.fromJson(resp.data),
|
||||
);
|
||||
|
@ -120,6 +120,25 @@ class SnAttachmentProvider {
|
||||
'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(
|
||||
Uint8List data,
|
||||
String filename,
|
||||
@ -311,6 +330,23 @@ class SnAttachmentProvider {
|
||||
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 {
|
||||
for (final ele in out) {
|
||||
if (!ele.isAnalyzed || ele.destination == 0) continue;
|
||||
|
@ -249,8 +249,11 @@ class SnNetworkProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
String getAttachmentUrl(String ky) {
|
||||
String getAttachmentUrl(String ky, {bool preview = true}) {
|
||||
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';
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:surface/screens/abuse_report.dart';
|
||||
@ -23,7 +22,6 @@ import 'package:surface/screens/album.dart';
|
||||
import 'package:surface/screens/auth/login.dart';
|
||||
import 'package:surface/screens/auth/register.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/manage.dart';
|
||||
import 'package:surface/screens/chat/room.dart';
|
||||
@ -31,8 +29,7 @@ import 'package:surface/screens/explore.dart';
|
||||
import 'package:surface/screens/friend.dart';
|
||||
import 'package:surface/screens/home.dart';
|
||||
import 'package:surface/screens/logging.dart';
|
||||
import 'package:surface/screens/news/news_detail.dart';
|
||||
import 'package:surface/screens/news/news_list.dart';
|
||||
import 'package:surface/screens/feed/feed_detail.dart';
|
||||
import 'package:surface/screens/notification.dart';
|
||||
import 'package:surface/screens/post/post_detail.dart';
|
||||
import 'package:surface/screens/post/post_draft.dart';
|
||||
@ -54,16 +51,6 @@ import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/about.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
Widget _fadeThroughTransition(BuildContext context, Animation<double> animation,
|
||||
Animation<double> secondaryAnimation, Widget child) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
final _appRoutes = [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
@ -137,6 +124,13 @@ final _appRoutes = [
|
||||
preload: state.extra as SnPost?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/pages/:id',
|
||||
name: 'readerFeedDetail',
|
||||
builder: (context, state) => ReaderPageScreen(
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/:name',
|
||||
name: 'postPublisher',
|
||||
@ -275,14 +269,6 @@ final _appRoutes = [
|
||||
extra: state.extra as ChatRoomScreenExtra?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:scope/:alias/call',
|
||||
name: 'chatCallRoom',
|
||||
builder: (context, state) => CallRoomScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:scope/:alias/detail',
|
||||
name: 'channelDetail',
|
||||
@ -305,10 +291,7 @@ final _appRoutes = [
|
||||
GoRoute(
|
||||
path: '/realm',
|
||||
name: 'realm',
|
||||
pageBuilder: (context, state) => CustomTransitionPage(
|
||||
transitionsBuilder: _fadeThroughTransition,
|
||||
child: const RealmScreen(),
|
||||
),
|
||||
builder: (context, state) => const RealmScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:alias/community',
|
||||
@ -337,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(
|
||||
path: '/stickers',
|
||||
name: 'stickers',
|
||||
@ -421,4 +390,10 @@ final appRouter = GoRouter(
|
||||
),
|
||||
),
|
||||
],
|
||||
onException: (context, state, router) {
|
||||
if (state.error is GoException) {
|
||||
router.goNamed('/');
|
||||
}
|
||||
},
|
||||
navigatorKey: GlobalKey(),
|
||||
);
|
||||
|
@ -15,7 +15,6 @@ import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/account/account_image.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/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
@ -110,9 +109,9 @@ class AccountScreen extends StatelessWidget {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
leading: const PageBackButton(),
|
||||
title: Text("screenAccount").tr(),
|
||||
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
||||
? Stack(
|
||||
@ -295,12 +294,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
|
||||
leading: const Icon(Symbols.login),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('authLogin').then((value) {
|
||||
if (value == true && context.mounted) {
|
||||
final ua = context.read<UserProvider>();
|
||||
ua.refreshUser();
|
||||
}
|
||||
});
|
||||
GoRouter.of(context).pushNamed('authLogin');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
@ -59,7 +59,7 @@ class _ActionEventScreenState extends State<ActionEventScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountActionEvent').tr(),
|
||||
|
@ -91,7 +91,7 @@ class _AccountAuthTicketState extends State<AccountAuthTicket> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountAuthTickets').tr(),
|
||||
|
@ -70,7 +70,7 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: Text('screenAccountBadges').tr(),
|
||||
),
|
||||
|
@ -69,7 +69,7 @@ class _AccountContactMethodState extends State<AccountContactMethod> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountContactMethods').tr(),
|
||||
|
@ -62,7 +62,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenFactorSettings').tr(),
|
||||
|
@ -37,7 +37,7 @@ class _KeyPairScreenState extends State<KeyPairScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: Text('screenKeyPairs').tr(),
|
||||
),
|
||||
|
@ -75,7 +75,7 @@ class _AccountNotifyPrefsScreenState extends State<AccountNotifyPrefsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountSettingsNotify').tr(),
|
||||
|
@ -70,7 +70,7 @@ class _AccountSecurityPrefsScreenState
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountSettingsSecurity').tr(),
|
||||
|
@ -244,7 +244,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountProfileEdit').tr()),
|
||||
@ -263,7 +263,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
aspectRatio: 16 / 7,
|
||||
child: Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
|
@ -15,6 +15,7 @@ import 'package:surface/providers/experience.dart';
|
||||
import 'package:surface/providers/relationship.dart';
|
||||
import 'package:surface/providers/sn_network.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/check_in.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
@ -61,6 +62,21 @@ final Map<String, (String, IconData, Color)> kBadgesMeta = {
|
||||
Symbols.thumb_up,
|
||||
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 {
|
||||
@ -442,7 +458,7 @@ class _UserScreenState extends State<UserScreen>
|
||||
],
|
||||
).padding(right: 8),
|
||||
if (_account!.profile!.description.isNotEmpty)
|
||||
const Gap(12)
|
||||
const Gap(4)
|
||||
else
|
||||
const Gap(8),
|
||||
if (_account!.profile!.description.isNotEmpty)
|
||||
@ -488,7 +504,8 @@ class _UserScreenState extends State<UserScreen>
|
||||
],
|
||||
).padding(vertical: 8, horizontal: 12),
|
||||
),
|
||||
const Gap(8),
|
||||
if (_account!.badges.isNotEmpty) const Gap(8),
|
||||
if (_account!.badges.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
@ -604,6 +621,17 @@ class _UserScreenState extends State<UserScreen>
|
||||
],
|
||||
).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)
|
||||
SliverToBoxAdapter(child: const Divider()),
|
||||
if (_account?.profile?.links.isNotEmpty ?? false)
|
||||
|
@ -70,7 +70,7 @@ class _AccountProgramScreenState extends State<AccountProgramScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: Text('accountProgram').tr(),
|
||||
),
|
||||
|
@ -196,7 +196,7 @@ class _AccountPublisherEditScreenState
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountPublisherEdit').tr()),
|
||||
@ -214,7 +214,7 @@ class _AccountPublisherEditScreenState
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
aspectRatio: 16 / 7,
|
||||
child: Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
|
@ -26,7 +26,7 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountPublisherNew').tr(),
|
||||
|
@ -82,7 +82,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountPublishers').tr(),
|
||||
|
@ -55,7 +55,7 @@ class _PunishmentsScreenState extends State<PunishmentsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: Text('accountPunishments').tr(),
|
||||
leading: PageBackButton(),
|
||||
@ -107,6 +107,28 @@ class _PunishmentsScreenState extends State<PunishmentsScreen> {
|
||||
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(
|
||||
@ -117,10 +139,8 @@ class _PunishmentsScreenState extends State<PunishmentsScreen> {
|
||||
Icon(kPunishmentIcons[ele.type], size: 20),
|
||||
const Gap(6),
|
||||
Expanded(
|
||||
child: Text('punishmentType${ele.type}')
|
||||
.tr()
|
||||
.fontSize(16)
|
||||
.bold(),
|
||||
child:
|
||||
Text('punishmentType${ele.type}').tr().fontSize(16).bold(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -175,13 +195,5 @@ class _PunishmentsScreenState extends State<PunishmentsScreen> {
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ class AccountSettingsScreen extends StatelessWidget {
|
||||
final ua = context.watch<UserProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountSettings').tr(),
|
||||
|
@ -1,20 +1,21 @@
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:path/path.dart' show withoutExtension;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_zoom.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/navigation/app_scaffold.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class AlbumScreen extends StatefulWidget {
|
||||
const AlbumScreen({super.key});
|
||||
@ -49,23 +50,23 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
Future<void> _fetchAttachments() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
const uuid = Uuid();
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _attachments.length,
|
||||
'author': ua.user?.name,
|
||||
});
|
||||
final attachments = List<SnAttachment>.from(
|
||||
resp.data['data']?.map((e) => SnAttachment.fromJson(e)) ?? [],
|
||||
).where((e) => e.mimetype.startsWith('image')).toList();
|
||||
);
|
||||
_attachments.addAll(attachments);
|
||||
_heroTags.addAll(_attachments.map((_) => uuid.v4()));
|
||||
|
||||
await ud.listAccount(attachments.map((e) => e.accountId).toSet());
|
||||
|
||||
_totalCount = resp.data['count'] as int?;
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
@ -101,15 +102,14 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAlbum').tr(),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Card(
|
||||
body: Column(
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
@ -129,8 +129,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
children: [
|
||||
Text('attachmentBillingUploaded').tr().bold(),
|
||||
Text(
|
||||
(_billing?.currentBytes ?? 0)
|
||||
.formatBytes(decimals: 4),
|
||||
(_billing?.currentBytes ?? 0).formatBytes(decimals: 4),
|
||||
style: GoogleFonts.robotoMono(),
|
||||
),
|
||||
Text('attachmentBillingDiscount').tr().bold(),
|
||||
@ -150,47 +149,82 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 8),
|
||||
),
|
||||
),
|
||||
SliverMasonryGrid.extent(
|
||||
childCount: _attachments.length,
|
||||
maxCrossAxisExtent: 320,
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisSpacing: 4,
|
||||
itemBuilder: (context, idx) {
|
||||
final attachment = _attachments[idx];
|
||||
return GestureDetector(
|
||||
child: ClipRRect(
|
||||
).padding(horizontal: 8, top: 8),
|
||||
Expanded(
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
itemCount: _attachments.length,
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax:
|
||||
_totalCount != null && _attachments.length >= _totalCount!,
|
||||
onFetchData: _fetchAttachments,
|
||||
itemBuilder: (context, index) {
|
||||
final ele = _attachments[index];
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
child: AspectRatio(
|
||||
aspectRatio: attachment.metadata['ratio']?.toDouble() ?? 1,
|
||||
aspectRatio: (ele.data['ratio'] ?? 1).toDouble(),
|
||||
child: AttachmentItem(
|
||||
data: attachment,
|
||||
heroTag: _heroTags[idx],
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
data: ele,
|
||||
heroTag: _heroTags[index],
|
||||
onZoom: () {
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: [attachment],
|
||||
heroTags: [_heroTags[idx]],
|
||||
data: [ele],
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
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);
|
||||
if (!mounted) return;
|
||||
final user = context.read<UserProvider>();
|
||||
user.isAuthorized = true;
|
||||
await user.refreshUser();
|
||||
if (!mounted) return;
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
|
@ -41,7 +41,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
||||
final captchaTk = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CaptchaScreen(),
|
||||
),
|
||||
|
@ -1,3 +1 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
|
||||
export 'captcha_native.dart' if (kIsWeb) 'captcha_web.dart';
|
||||
export 'captcha_native.dart' if (dart.library.html) 'captcha_web.dart';
|
||||
|
@ -1,3 +1,4 @@
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:html' as html;
|
||||
import 'dart:ui_web' as ui;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -32,7 +33,7 @@ class _CaptchaScreenState extends State<CaptchaScreen> {
|
||||
});
|
||||
|
||||
final iframe = html.IFrameElement()
|
||||
..src = '${context.read<ConfigProvider>().serverUrl}/captcha?redirect_uri=web'
|
||||
..src = '${context.read<ConfigProvider>().serverUrl}/captcha'
|
||||
..style.border = 'none'
|
||||
..width = '100%'
|
||||
..height = '100%';
|
||||
|
@ -171,7 +171,18 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
void _onTapChannel(SnChannel channel) {
|
||||
setState(() => _unreadCounts?[channel.id] = 0);
|
||||
setState(() {
|
||||
_unreadCounts?[channel.id] = 0;
|
||||
if (channel.realmId != null) {
|
||||
_unreadCountsGrouped?[channel.realmId!] =
|
||||
(_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',
|
||||
@ -180,9 +191,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
'alias': channel.alias,
|
||||
},
|
||||
).then((value) {
|
||||
if (mounted) {
|
||||
setState(() => _unreadCounts?[channel.id] = 0);
|
||||
_refreshChannels(noRemote: true);
|
||||
if (mounted && value == true) {
|
||||
_refreshChannels();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -193,9 +203,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
'alias': channel.alias,
|
||||
},
|
||||
).then((value) {
|
||||
if (mounted) {
|
||||
setState(() => _unreadCounts?[channel.id] = 0);
|
||||
_refreshChannels(noRemote: true);
|
||||
if (mounted && value == true) {
|
||||
_refreshChannels();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -223,7 +232,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenChat').tr(),
|
||||
@ -389,7 +398,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
children: [
|
||||
if (_focusedRealm!.banner != null)
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
aspectRatio: 16 / 7,
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(
|
||||
_focusedRealm!.banner!,
|
||||
|
@ -1,322 +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(
|
||||
noBackground: true,
|
||||
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,7 +220,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
|
||||
),
|
||||
|
@ -141,7 +141,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: widget.editingChannelAlias != null
|
||||
? Text('screenChatManage').tr()
|
||||
|
@ -1,19 +1,20 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.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:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/chat_message_controller.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/providers/notification.dart';
|
||||
import 'package:surface/providers/sn_network.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/types/chat.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_input.dart';
|
||||
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.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';
|
||||
|
||||
class ChatRoomScreenExtra {
|
||||
@ -51,13 +52,11 @@ class ChatRoomScreen extends StatefulWidget {
|
||||
|
||||
class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
bool _isBusy = false;
|
||||
bool _isCalling = false;
|
||||
bool _isJoining = false;
|
||||
|
||||
SnChannel? _channel;
|
||||
SnChannelMember? _currentMember;
|
||||
SnChannelMember? _otherMember;
|
||||
SnChatCall? _ongoingCall;
|
||||
|
||||
final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey();
|
||||
late final ChatMessageController _messageController;
|
||||
@ -139,88 +138,35 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchOngoingCall() async {
|
||||
setState(() => _isCalling = true);
|
||||
|
||||
try {
|
||||
Future<void> _joinCall() async {
|
||||
if (kIsWeb || !(Platform.isIOS || Platform.isAndroid)) {
|
||||
return await _joinCallWeb();
|
||||
}
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get(
|
||||
'/cgi/im/channels/${_messageController.channel!.keyPath}/calls/ongoing',
|
||||
options: Options(
|
||||
validateStatus: (status) => status != null && status < 500,
|
||||
receiveTimeout: const Duration(seconds: 60),
|
||||
sendTimeout: const Duration(seconds: 60),
|
||||
),
|
||||
);
|
||||
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,
|
||||
final ua = context.read<UserProvider>();
|
||||
final meet = JitsiMeet();
|
||||
final confOpts = JitsiMeetConferenceOptions(
|
||||
room: 'sn-chat-${_channel!.alias}-${_channel!.id}',
|
||||
serverURL: 'https://meet.element.io',
|
||||
configOverrides: {
|
||||
"subject": _channel!.name,
|
||||
},
|
||||
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) {
|
||||
@ -248,10 +194,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
_messageController.checkUpdate(),
|
||||
_fetchOngoingCall(),
|
||||
]);
|
||||
await _messageController.checkUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
@ -260,23 +203,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
super.initState();
|
||||
_messageController = ChatMessageController(context);
|
||||
_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
|
||||
@ -300,11 +226,10 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final call = context.watch<ChatCallProvider>();
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_channel?.type == 1
|
||||
@ -324,14 +249,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
),
|
||||
if (_currentMember != null)
|
||||
IconButton(
|
||||
icon: _ongoingCall == null
|
||||
? const Icon(Symbols.call)
|
||||
: const Icon(Symbols.call_end),
|
||||
onPressed: _isCalling
|
||||
? null
|
||||
: _ongoingCall == null
|
||||
? _makeCall
|
||||
: _endCall,
|
||||
icon: const Icon(Symbols.video_call),
|
||||
onPressed: _joinCall,
|
||||
onLongPress: _joinCallWeb,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.more_vert),
|
||||
@ -359,28 +279,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
LoadingIndicator(
|
||||
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)
|
||||
Expanded(
|
||||
child: Center(
|
||||
|
@ -17,10 +17,9 @@ import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/feed/feed_news.dart';
|
||||
import 'package:surface/widgets/feed/feed_reader.dart';
|
||||
import 'package:surface/widgets/feed/feed_unknown.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:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
@ -157,7 +156,7 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: ExpandableFab(
|
||||
key: _fabKey,
|
||||
@ -466,7 +465,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
final pt = context.read<SnPostContentProvider>();
|
||||
final result = await pt.getFeed(
|
||||
cursor: _feed
|
||||
.where((ele) => !['reader.news'].contains(ele.type))
|
||||
.where((ele) => !['reader.feed'].contains(ele.type))
|
||||
.lastOrNull
|
||||
?.createdAt,
|
||||
);
|
||||
@ -549,12 +548,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
refreshPosts();
|
||||
},
|
||||
);
|
||||
case 'fediverse.post':
|
||||
return FediversePostWidget(
|
||||
data: SnFediversePost.fromJson(ele.data),
|
||||
maxWidth: 640,
|
||||
);
|
||||
case 'reader.news':
|
||||
case 'reader.feed':
|
||||
return Center(
|
||||
child: Container(
|
||||
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:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class NewsDetailScreen extends StatefulWidget {
|
||||
final String hash;
|
||||
class ReaderPageScreen extends StatefulWidget {
|
||||
final String id;
|
||||
|
||||
const NewsDetailScreen({super.key, required this.hash});
|
||||
const ReaderPageScreen({super.key, required this.id});
|
||||
|
||||
@override
|
||||
State<NewsDetailScreen> createState() => _NewsDetailScreenState();
|
||||
State<ReaderPageScreen> createState() => _ReaderPageScreenState();
|
||||
}
|
||||
|
||||
class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
SnNewsArticle? _article;
|
||||
class _ReaderPageScreenState extends State<ReaderPageScreen> {
|
||||
SnSubscriptionItem? _article;
|
||||
|
||||
Future<void> _fetchArticle() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
|
||||
_article = SnNewsArticle.fromJson(resp.data);
|
||||
final resp = await sn.client.get('/cgi/re/subscriptions/${widget.id}');
|
||||
_article = SnSubscriptionItem.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err).then((_) {
|
@ -518,7 +518,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
}
|
||||
|
||||
Future<void> _doCheckIn() async {
|
||||
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
||||
final captchaTk = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CaptchaScreen(),
|
||||
),
|
||||
|
@ -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/sn_network.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/loading_indicator.dart';
|
||||
import 'package:surface/widgets/markdown_content.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 '../providers/userinfo.dart';
|
||||
@ -158,7 +155,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenNotification').tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
@ -219,34 +216,24 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
'interactive.subscription',
|
||||
].contains(nty.topic) &&
|
||||
nty.metadata['related_post'] != null)
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1),
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.zero,
|
||||
),
|
||||
child: PostItem(
|
||||
data: SnPost.fromJson(
|
||||
nty.metadata['related_post']!),
|
||||
showComments: false,
|
||||
showReactions: false,
|
||||
showMenu: false,
|
||||
).padding(vertical: 4),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
onTap: () {
|
||||
child: Text('postReadMore').tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {
|
||||
'slug': nty
|
||||
.metadata['related_post']!['id']
|
||||
.toString()
|
||||
'slug': nty.metadata['related_post']['id']
|
||||
.toString(),
|
||||
},
|
||||
);
|
||||
},
|
||||
).padding(top: 8),
|
||||
),
|
||||
const Gap(8),
|
||||
Row(
|
||||
children: [
|
||||
|
@ -66,7 +66,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
|
@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
@ -16,7 +15,6 @@ import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.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_realm.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/attachment/attachment_input.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_boost.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_actions.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
@ -346,9 +343,15 @@ class _PostEditorScreenState extends State<PostEditorScreen>
|
||||
children: [
|
||||
const Icon(Icons.edit, size: 16),
|
||||
const Gap(10),
|
||||
Text('postEditingNotice').tr(args: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'postEditingNotice',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(args: [
|
||||
'@${_writeController.editingPost!.publisher.name}'
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -452,7 +455,9 @@ class _PostEditorScreenState extends State<PostEditorScreen>
|
||||
isBusy: _writeController.isBusy,
|
||||
onUpload: (int idx) async {
|
||||
await _writeController.uploadSingleAttachment(
|
||||
context, idx);
|
||||
context,
|
||||
idx,
|
||||
);
|
||||
},
|
||||
onInsertLink: (int idx) async {
|
||||
_writeController.contentController.text +=
|
||||
@ -1100,7 +1105,7 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PostVideoEditor extends StatelessWidget {
|
||||
class _PostVideoEditor extends StatefulWidget {
|
||||
final PostWriteController controller;
|
||||
final Function? onTapPublisher;
|
||||
final Function? onTapRealm;
|
||||
@ -1108,7 +1113,14 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
const _PostVideoEditor(
|
||||
{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?>(
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
@ -1119,78 +1131,25 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (video == null) return;
|
||||
controller.setVideoAttachment(video);
|
||||
widget.controller.setVideoAttachment(video);
|
||||
}
|
||||
|
||||
void _setAlt(BuildContext context) async {
|
||||
if (controller.videoAttachment == null) return;
|
||||
|
||||
final result = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentAltDialog(
|
||||
media: PostWriteMedia(controller.videoAttachment)),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
controller.setVideoAttachment(result);
|
||||
@override
|
||||
void initState() {
|
||||
_streamUrlController.addListener(() {
|
||||
if (_streamUrlController.text.isEmpty) {
|
||||
widget.controller.setVideoUrl('');
|
||||
} else {
|
||||
widget.controller.setVideoUrl(_streamUrlController.text);
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> _createBoost(BuildContext context) async {
|
||||
if (controller.videoAttachment == null) return;
|
||||
|
||||
final result = await showDialog<SnAttachmentBoost?>(
|
||||
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
|
||||
void dispose() {
|
||||
_streamUrlController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1208,10 +1167,10 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onTapPublisher?.call();
|
||||
widget.onTapPublisher?.call();
|
||||
},
|
||||
child: AccountImage(
|
||||
content: controller.publisher?.avatar,
|
||||
content: widget.controller.publisher?.avatar,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -1221,10 +1180,10 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onTapRealm?.call();
|
||||
widget.onTapRealm?.call();
|
||||
},
|
||||
child: AccountImage(
|
||||
content: controller.realm?.avatar,
|
||||
content: widget.controller.realm?.avatar,
|
||||
fallbackWidget: const Icon(Symbols.globe, size: 20),
|
||||
radius: 14,
|
||||
),
|
||||
@ -1237,7 +1196,7 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
children: [
|
||||
const Gap(6),
|
||||
TextField(
|
||||
controller: controller.titleController,
|
||||
controller: widget.controller.titleController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostTitle'.tr(),
|
||||
border: InputBorder.none,
|
||||
@ -1248,7 +1207,7 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: controller.descriptionController,
|
||||
controller: widget.controller.descriptionController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostDescription'.tr(),
|
||||
border: InputBorder.none,
|
||||
@ -1260,66 +1219,52 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(12),
|
||||
if (widget.controller.videoLive ||
|
||||
widget.controller.videoAttachment == null)
|
||||
TextField(
|
||||
controller: _streamUrlController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPostVideoUrl'.tr(),
|
||||
helperText: 'fieldPostVideoUrlDescription'.tr(),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16, bottom: 12, top: 2),
|
||||
if (!widget.controller.videoLive &&
|
||||
_streamUrlController.text.isEmpty)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
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(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: controller.videoAttachment == null
|
||||
? () => _selectVideo(context)
|
||||
: null,
|
||||
onTap: widget.controller.videoAttachment == null
|
||||
? () => _selectVideo()
|
||||
: () {
|
||||
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(
|
||||
aspectRatio: 16 / 9,
|
||||
child: controller.videoAttachment == null
|
||||
child: widget.controller.videoAttachment == null
|
||||
? Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -1335,13 +1280,21 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AttachmentItem(
|
||||
data: controller.videoAttachment!,
|
||||
data: widget.controller.videoAttachment!,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -79,6 +79,7 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
|
||||
key: ValueKey(ele),
|
||||
data: ele,
|
||||
maxWidth: 640,
|
||||
useReplace: true,
|
||||
onChanged: (ele) {
|
||||
_posts[idx] = ele;
|
||||
setState(() {});
|
||||
|
@ -286,7 +286,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
body: NestedScrollView(
|
||||
controller: _scrollController,
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
@ -303,6 +303,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
|
||||
),
|
||||
child: SliverAppBar(
|
||||
expandedHeight: _appBarHeight,
|
||||
leading: const PageBackButton(),
|
||||
title: _publisher == null
|
||||
? Text('loading').tr()
|
||||
: RichText(
|
||||
|
@ -45,7 +45,7 @@ class _WalletScreenState extends State<WalletScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: true,
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(), title: Text('screenAccountWallet').tr()),
|
||||
body: Column(
|
||||
|
@ -22,6 +22,7 @@ abstract class SnAccount with _$SnAccount {
|
||||
required String language,
|
||||
required SnAccountProfile? profile,
|
||||
@Default([]) List<SnAccountBadge> badges,
|
||||
@Default([]) List<SnPunishment> punishments,
|
||||
required DateTime? suspendedAt,
|
||||
required int? affiliatedId,
|
||||
required int? affiliatedTo,
|
||||
|
@ -29,6 +29,7 @@ mixin _$SnAccount {
|
||||
String get language;
|
||||
SnAccountProfile? get profile;
|
||||
List<SnAccountBadge> get badges;
|
||||
List<SnPunishment> get punishments;
|
||||
DateTime? get suspendedAt;
|
||||
int? get affiliatedId;
|
||||
int? get affiliatedTo;
|
||||
@ -69,6 +70,8 @@ mixin _$SnAccount {
|
||||
other.language == language) &&
|
||||
(identical(other.profile, profile) || other.profile == profile) &&
|
||||
const DeepCollectionEquality().equals(other.badges, badges) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.punishments, punishments) &&
|
||||
(identical(other.suspendedAt, suspendedAt) ||
|
||||
other.suspendedAt == suspendedAt) &&
|
||||
(identical(other.affiliatedId, affiliatedId) ||
|
||||
@ -99,6 +102,7 @@ mixin _$SnAccount {
|
||||
language,
|
||||
profile,
|
||||
const DeepCollectionEquality().hash(badges),
|
||||
const DeepCollectionEquality().hash(punishments),
|
||||
suspendedAt,
|
||||
affiliatedId,
|
||||
affiliatedTo,
|
||||
@ -108,7 +112,7 @@ mixin _$SnAccount {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, punishments: $punishments, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,6 +136,7 @@ abstract mixin class $SnAccountCopyWith<$Res> {
|
||||
String language,
|
||||
SnAccountProfile? profile,
|
||||
List<SnAccountBadge> badges,
|
||||
List<SnPunishment> punishments,
|
||||
DateTime? suspendedAt,
|
||||
int? affiliatedId,
|
||||
int? affiliatedTo,
|
||||
@ -167,6 +172,7 @@ class _$SnAccountCopyWithImpl<$Res> implements $SnAccountCopyWith<$Res> {
|
||||
Object? language = null,
|
||||
Object? profile = freezed,
|
||||
Object? badges = null,
|
||||
Object? punishments = null,
|
||||
Object? suspendedAt = freezed,
|
||||
Object? affiliatedId = freezed,
|
||||
Object? affiliatedTo = freezed,
|
||||
@ -230,6 +236,10 @@ class _$SnAccountCopyWithImpl<$Res> implements $SnAccountCopyWith<$Res> {
|
||||
? _self.badges
|
||||
: badges // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnAccountBadge>,
|
||||
punishments: null == punishments
|
||||
? _self.punishments
|
||||
: punishments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPunishment>,
|
||||
suspendedAt: freezed == suspendedAt
|
||||
? _self.suspendedAt
|
||||
: suspendedAt // ignore: cast_nullable_to_non_nullable
|
||||
@ -286,6 +296,7 @@ class _SnAccount extends SnAccount {
|
||||
required this.language,
|
||||
required this.profile,
|
||||
final List<SnAccountBadge> badges = const [],
|
||||
final List<SnPunishment> punishments = const [],
|
||||
required this.suspendedAt,
|
||||
required this.affiliatedId,
|
||||
required this.affiliatedTo,
|
||||
@ -294,6 +305,7 @@ class _SnAccount extends SnAccount {
|
||||
: _contacts = contacts,
|
||||
_permNodes = permNodes,
|
||||
_badges = badges,
|
||||
_punishments = punishments,
|
||||
super._();
|
||||
factory _SnAccount.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnAccountFromJson(json);
|
||||
@ -350,6 +362,15 @@ class _SnAccount extends SnAccount {
|
||||
return EqualUnmodifiableListView(_badges);
|
||||
}
|
||||
|
||||
final List<SnPunishment> _punishments;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<SnPunishment> get punishments {
|
||||
if (_punishments is EqualUnmodifiableListView) return _punishments;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_punishments);
|
||||
}
|
||||
|
||||
@override
|
||||
final DateTime? suspendedAt;
|
||||
@override
|
||||
@ -401,6 +422,8 @@ class _SnAccount extends SnAccount {
|
||||
other.language == language) &&
|
||||
(identical(other.profile, profile) || other.profile == profile) &&
|
||||
const DeepCollectionEquality().equals(other._badges, _badges) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._punishments, _punishments) &&
|
||||
(identical(other.suspendedAt, suspendedAt) ||
|
||||
other.suspendedAt == suspendedAt) &&
|
||||
(identical(other.affiliatedId, affiliatedId) ||
|
||||
@ -431,6 +454,7 @@ class _SnAccount extends SnAccount {
|
||||
language,
|
||||
profile,
|
||||
const DeepCollectionEquality().hash(_badges),
|
||||
const DeepCollectionEquality().hash(_punishments),
|
||||
suspendedAt,
|
||||
affiliatedId,
|
||||
affiliatedTo,
|
||||
@ -440,7 +464,7 @@ class _SnAccount extends SnAccount {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, punishments: $punishments, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -467,6 +491,7 @@ abstract mixin class _$SnAccountCopyWith<$Res>
|
||||
String language,
|
||||
SnAccountProfile? profile,
|
||||
List<SnAccountBadge> badges,
|
||||
List<SnPunishment> punishments,
|
||||
DateTime? suspendedAt,
|
||||
int? affiliatedId,
|
||||
int? affiliatedTo,
|
||||
@ -503,6 +528,7 @@ class __$SnAccountCopyWithImpl<$Res> implements _$SnAccountCopyWith<$Res> {
|
||||
Object? language = null,
|
||||
Object? profile = freezed,
|
||||
Object? badges = null,
|
||||
Object? punishments = null,
|
||||
Object? suspendedAt = freezed,
|
||||
Object? affiliatedId = freezed,
|
||||
Object? affiliatedTo = freezed,
|
||||
@ -566,6 +592,10 @@ class __$SnAccountCopyWithImpl<$Res> implements _$SnAccountCopyWith<$Res> {
|
||||
? _self._badges
|
||||
: badges // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnAccountBadge>,
|
||||
punishments: null == punishments
|
||||
? _self._punishments
|
||||
: punishments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPunishment>,
|
||||
suspendedAt: freezed == suspendedAt
|
||||
? _self.suspendedAt
|
||||
: suspendedAt // ignore: cast_nullable_to_non_nullable
|
||||
|
@ -32,6 +32,10 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
|
||||
?.map((e) => SnAccountBadge.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
punishments: (json['punishments'] as List<dynamic>?)
|
||||
?.map((e) => SnPunishment.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
suspendedAt: json['suspended_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['suspended_at'] as String),
|
||||
@ -57,6 +61,7 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
|
||||
'language': instance.language,
|
||||
'profile': instance.profile?.toJson(),
|
||||
'badges': instance.badges.map((e) => e.toJson()).toList(),
|
||||
'punishments': instance.punishments.map((e) => e.toJson()).toList(),
|
||||
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
||||
'affiliated_id': instance.affiliatedId,
|
||||
'affiliated_to': instance.affiliatedTo,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
|
||||
part 'attachment.freezed.dart';
|
||||
|
||||
@ -29,6 +30,7 @@ abstract class SnAttachment with _$SnAttachment {
|
||||
required String hash,
|
||||
required int destination,
|
||||
required int refCount,
|
||||
String? refUrl,
|
||||
@Default(0) int contentRating,
|
||||
@Default(0) int qualityRating,
|
||||
required DateTime? cleanedAt,
|
||||
@ -39,6 +41,7 @@ abstract class SnAttachment with _$SnAttachment {
|
||||
required int? refId,
|
||||
required SnAttachmentPool? pool,
|
||||
required int? poolId,
|
||||
required SnAccount? account,
|
||||
required int accountId,
|
||||
int? thumbnailId,
|
||||
SnAttachment? thumbnail,
|
||||
@ -49,7 +52,8 @@ abstract class SnAttachment with _$SnAttachment {
|
||||
@Default({}) Map<String, dynamic> metadata,
|
||||
}) = _SnAttachment;
|
||||
|
||||
factory SnAttachment.fromJson(Map<String, Object?> json) => _$SnAttachmentFromJson(json);
|
||||
factory SnAttachment.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAttachmentFromJson(json);
|
||||
|
||||
Map<String, dynamic> get data => {
|
||||
...metadata,
|
||||
@ -85,7 +89,8 @@ abstract class SnAttachmentFragment with _$SnAttachmentFragment {
|
||||
@Default([]) List<String> fileChunksMissing,
|
||||
}) = _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) {
|
||||
'image' => SnMediaType.image,
|
||||
@ -109,7 +114,8 @@ abstract class SnAttachmentPool with _$SnAttachmentPool {
|
||||
required int? accountId,
|
||||
}) = _SnAttachmentPool;
|
||||
|
||||
factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json);
|
||||
factory SnAttachmentPool.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAttachmentPoolFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -122,7 +128,8 @@ abstract class SnAttachmentDestination with _$SnAttachmentDestination {
|
||||
required bool isBoost,
|
||||
}) = _SnAttachmentDestination;
|
||||
|
||||
factory SnAttachmentDestination.fromJson(Map<String, Object?> json) => _$SnAttachmentDestinationFromJson(json);
|
||||
factory SnAttachmentDestination.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAttachmentDestinationFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -139,7 +146,8 @@ abstract class SnAttachmentBoost with _$SnAttachmentBoost {
|
||||
required int account,
|
||||
}) = _SnAttachmentBoost;
|
||||
|
||||
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json);
|
||||
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAttachmentBoostFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -158,7 +166,8 @@ abstract class SnSticker with _$SnSticker {
|
||||
required int accountId,
|
||||
}) = _SnSticker;
|
||||
|
||||
factory SnSticker.fromJson(Map<String, Object?> json) => _$SnStickerFromJson(json);
|
||||
factory SnSticker.fromJson(Map<String, Object?> json) =>
|
||||
_$SnStickerFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -175,7 +184,8 @@ abstract class SnStickerPack with _$SnStickerPack {
|
||||
required int accountId,
|
||||
}) = _SnStickerPack;
|
||||
|
||||
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json);
|
||||
factory SnStickerPack.fromJson(Map<String, Object?> json) =>
|
||||
_$SnStickerPackFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -186,5 +196,6 @@ abstract class SnAttachmentBilling with _$SnAttachmentBilling {
|
||||
required double includedRatio,
|
||||
}) = _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;
|
||||
int get destination;
|
||||
int get refCount;
|
||||
String? get refUrl;
|
||||
int get contentRating;
|
||||
int get qualityRating;
|
||||
DateTime? get cleanedAt;
|
||||
@ -38,6 +39,7 @@ mixin _$SnAttachment {
|
||||
int? get refId;
|
||||
SnAttachmentPool? get pool;
|
||||
int? get poolId;
|
||||
SnAccount? get account;
|
||||
int get accountId;
|
||||
int? get thumbnailId;
|
||||
SnAttachment? get thumbnail;
|
||||
@ -82,6 +84,7 @@ mixin _$SnAttachment {
|
||||
other.destination == destination) &&
|
||||
(identical(other.refCount, refCount) ||
|
||||
other.refCount == refCount) &&
|
||||
(identical(other.refUrl, refUrl) || other.refUrl == refUrl) &&
|
||||
(identical(other.contentRating, contentRating) ||
|
||||
other.contentRating == contentRating) &&
|
||||
(identical(other.qualityRating, qualityRating) ||
|
||||
@ -98,6 +101,7 @@ mixin _$SnAttachment {
|
||||
(identical(other.refId, refId) || other.refId == refId) &&
|
||||
(identical(other.pool, pool) || other.pool == pool) &&
|
||||
(identical(other.poolId, poolId) || other.poolId == poolId) &&
|
||||
(identical(other.account, account) || other.account == account) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId) &&
|
||||
(identical(other.thumbnailId, thumbnailId) ||
|
||||
@ -130,6 +134,7 @@ mixin _$SnAttachment {
|
||||
hash,
|
||||
destination,
|
||||
refCount,
|
||||
refUrl,
|
||||
contentRating,
|
||||
qualityRating,
|
||||
cleanedAt,
|
||||
@ -140,6 +145,7 @@ mixin _$SnAttachment {
|
||||
refId,
|
||||
pool,
|
||||
poolId,
|
||||
account,
|
||||
accountId,
|
||||
thumbnailId,
|
||||
thumbnail,
|
||||
@ -152,7 +158,7 @@ mixin _$SnAttachment {
|
||||
|
||||
@override
|
||||
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,
|
||||
int destination,
|
||||
int refCount,
|
||||
String? refUrl,
|
||||
int contentRating,
|
||||
int qualityRating,
|
||||
DateTime? cleanedAt,
|
||||
@ -186,6 +193,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
|
||||
int? refId,
|
||||
SnAttachmentPool? pool,
|
||||
int? poolId,
|
||||
SnAccount? account,
|
||||
int accountId,
|
||||
int? thumbnailId,
|
||||
SnAttachment? thumbnail,
|
||||
@ -197,6 +205,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
|
||||
|
||||
$SnAttachmentCopyWith<$Res>? get ref;
|
||||
$SnAttachmentPoolCopyWith<$Res>? get pool;
|
||||
$SnAccountCopyWith<$Res>? get account;
|
||||
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
||||
$SnAttachmentCopyWith<$Res>? get compressed;
|
||||
}
|
||||
@ -226,6 +235,7 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
||||
Object? hash = null,
|
||||
Object? destination = null,
|
||||
Object? refCount = null,
|
||||
Object? refUrl = freezed,
|
||||
Object? contentRating = null,
|
||||
Object? qualityRating = null,
|
||||
Object? cleanedAt = freezed,
|
||||
@ -236,6 +246,7 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
||||
Object? refId = freezed,
|
||||
Object? pool = freezed,
|
||||
Object? poolId = freezed,
|
||||
Object? account = freezed,
|
||||
Object? accountId = null,
|
||||
Object? thumbnailId = freezed,
|
||||
Object? thumbnail = freezed,
|
||||
@ -298,6 +309,10 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
||||
? _self.refCount
|
||||
: refCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
refUrl: freezed == refUrl
|
||||
? _self.refUrl
|
||||
: refUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
contentRating: null == contentRating
|
||||
? _self.contentRating
|
||||
: contentRating // ignore: cast_nullable_to_non_nullable
|
||||
@ -338,6 +353,10 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
||||
? _self.poolId
|
||||
: poolId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
account: freezed == account
|
||||
? _self.account
|
||||
: account // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccount?,
|
||||
accountId: null == accountId
|
||||
? _self.accountId
|
||||
: 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
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@ -447,6 +480,7 @@ class _SnAttachment extends SnAttachment {
|
||||
required this.hash,
|
||||
required this.destination,
|
||||
required this.refCount,
|
||||
this.refUrl,
|
||||
this.contentRating = 0,
|
||||
this.qualityRating = 0,
|
||||
required this.cleanedAt,
|
||||
@ -457,6 +491,7 @@ class _SnAttachment extends SnAttachment {
|
||||
required this.refId,
|
||||
required this.pool,
|
||||
required this.poolId,
|
||||
required this.account,
|
||||
required this.accountId,
|
||||
this.thumbnailId,
|
||||
this.thumbnail,
|
||||
@ -499,6 +534,8 @@ class _SnAttachment extends SnAttachment {
|
||||
@override
|
||||
final int refCount;
|
||||
@override
|
||||
final String? refUrl;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int contentRating;
|
||||
@override
|
||||
@ -521,6 +558,8 @@ class _SnAttachment extends SnAttachment {
|
||||
@override
|
||||
final int? poolId;
|
||||
@override
|
||||
final SnAccount? account;
|
||||
@override
|
||||
final int accountId;
|
||||
@override
|
||||
final int? thumbnailId;
|
||||
@ -596,6 +635,7 @@ class _SnAttachment extends SnAttachment {
|
||||
other.destination == destination) &&
|
||||
(identical(other.refCount, refCount) ||
|
||||
other.refCount == refCount) &&
|
||||
(identical(other.refUrl, refUrl) || other.refUrl == refUrl) &&
|
||||
(identical(other.contentRating, contentRating) ||
|
||||
other.contentRating == contentRating) &&
|
||||
(identical(other.qualityRating, qualityRating) ||
|
||||
@ -612,6 +652,7 @@ class _SnAttachment extends SnAttachment {
|
||||
(identical(other.refId, refId) || other.refId == refId) &&
|
||||
(identical(other.pool, pool) || other.pool == pool) &&
|
||||
(identical(other.poolId, poolId) || other.poolId == poolId) &&
|
||||
(identical(other.account, account) || other.account == account) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId) &&
|
||||
(identical(other.thumbnailId, thumbnailId) ||
|
||||
@ -644,6 +685,7 @@ class _SnAttachment extends SnAttachment {
|
||||
hash,
|
||||
destination,
|
||||
refCount,
|
||||
refUrl,
|
||||
contentRating,
|
||||
qualityRating,
|
||||
cleanedAt,
|
||||
@ -654,6 +696,7 @@ class _SnAttachment extends SnAttachment {
|
||||
refId,
|
||||
pool,
|
||||
poolId,
|
||||
account,
|
||||
accountId,
|
||||
thumbnailId,
|
||||
thumbnail,
|
||||
@ -666,7 +709,7 @@ class _SnAttachment extends SnAttachment {
|
||||
|
||||
@override
|
||||
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,
|
||||
int destination,
|
||||
int refCount,
|
||||
String? refUrl,
|
||||
int contentRating,
|
||||
int qualityRating,
|
||||
DateTime? cleanedAt,
|
||||
@ -702,6 +746,7 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
|
||||
int? refId,
|
||||
SnAttachmentPool? pool,
|
||||
int? poolId,
|
||||
SnAccount? account,
|
||||
int accountId,
|
||||
int? thumbnailId,
|
||||
SnAttachment? thumbnail,
|
||||
@ -716,6 +761,8 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
|
||||
@override
|
||||
$SnAttachmentPoolCopyWith<$Res>? get pool;
|
||||
@override
|
||||
$SnAccountCopyWith<$Res>? get account;
|
||||
@override
|
||||
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
||||
@override
|
||||
$SnAttachmentCopyWith<$Res>? get compressed;
|
||||
@ -747,6 +794,7 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
||||
Object? hash = null,
|
||||
Object? destination = null,
|
||||
Object? refCount = null,
|
||||
Object? refUrl = freezed,
|
||||
Object? contentRating = null,
|
||||
Object? qualityRating = null,
|
||||
Object? cleanedAt = freezed,
|
||||
@ -757,6 +805,7 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
||||
Object? refId = freezed,
|
||||
Object? pool = freezed,
|
||||
Object? poolId = freezed,
|
||||
Object? account = freezed,
|
||||
Object? accountId = null,
|
||||
Object? thumbnailId = freezed,
|
||||
Object? thumbnail = freezed,
|
||||
@ -819,6 +868,10 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
||||
? _self.refCount
|
||||
: refCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
refUrl: freezed == refUrl
|
||||
? _self.refUrl
|
||||
: refUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
contentRating: null == contentRating
|
||||
? _self.contentRating
|
||||
: contentRating // ignore: cast_nullable_to_non_nullable
|
||||
@ -859,6 +912,10 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
||||
? _self.poolId
|
||||
: poolId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
account: freezed == account
|
||||
? _self.account
|
||||
: account // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccount?,
|
||||
accountId: null == accountId
|
||||
? _self.accountId
|
||||
: 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
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
|
@ -23,6 +23,7 @@ _SnAttachment _$SnAttachmentFromJson(Map<String, dynamic> json) =>
|
||||
hash: json['hash'] as String,
|
||||
destination: (json['destination'] as num).toInt(),
|
||||
refCount: (json['ref_count'] as num).toInt(),
|
||||
refUrl: json['ref_url'] as String?,
|
||||
contentRating: (json['content_rating'] as num?)?.toInt() ?? 0,
|
||||
qualityRating: (json['quality_rating'] as num?)?.toInt() ?? 0,
|
||||
cleanedAt: json['cleaned_at'] == null
|
||||
@ -39,6 +40,9 @@ _SnAttachment _$SnAttachmentFromJson(Map<String, dynamic> json) =>
|
||||
? null
|
||||
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
|
||||
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(),
|
||||
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
|
||||
thumbnail: json['thumbnail'] == null
|
||||
@ -72,6 +76,7 @@ Map<String, dynamic> _$SnAttachmentToJson(_SnAttachment instance) =>
|
||||
'hash': instance.hash,
|
||||
'destination': instance.destination,
|
||||
'ref_count': instance.refCount,
|
||||
'ref_url': instance.refUrl,
|
||||
'content_rating': instance.contentRating,
|
||||
'quality_rating': instance.qualityRating,
|
||||
'cleaned_at': instance.cleanedAt?.toIso8601String(),
|
||||
@ -82,6 +87,7 @@ Map<String, dynamic> _$SnAttachmentToJson(_SnAttachment instance) =>
|
||||
'ref_id': instance.refId,
|
||||
'pool': instance.pool?.toJson(),
|
||||
'pool_id': instance.poolId,
|
||||
'account': instance.account?.toJson(),
|
||||
'account_id': instance.accountId,
|
||||
'thumbnail_id': instance.thumbnailId,
|
||||
'thumbnail': instance.thumbnail?.toJson(),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
@ -116,24 +115,3 @@ abstract class SnChatCall with _$SnChatCall {
|
||||
factory SnChatCall.fromJson(Map<String, dynamic> 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';
|
||||
|
||||
@freezed
|
||||
abstract class SnNewsSource with _$SnNewsSource {
|
||||
const factory SnNewsSource({
|
||||
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({
|
||||
abstract class SnSubscriptionFeed with _$SnSubscriptionFeed {
|
||||
const factory SnSubscriptionFeed({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
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 title,
|
||||
required String description,
|
||||
required String content,
|
||||
required String url,
|
||||
required String hash,
|
||||
required String source,
|
||||
required int feedId,
|
||||
required SnSubscriptionFeed feed,
|
||||
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;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnNewsSource {
|
||||
String get id;
|
||||
String get label;
|
||||
String get type;
|
||||
String get source;
|
||||
int get depth;
|
||||
bool get enabled;
|
||||
mixin _$SnSubscriptionFeed {
|
||||
int get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
DateTime? get deletedAt;
|
||||
String get url;
|
||||
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.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnNewsSourceCopyWith<SnNewsSource> get copyWith =>
|
||||
_$SnNewsSourceCopyWithImpl<SnNewsSource>(
|
||||
this as SnNewsSource, _$identity);
|
||||
$SnSubscriptionFeedCopyWith<SnSubscriptionFeed> get copyWith =>
|
||||
_$SnSubscriptionFeedCopyWithImpl<SnSubscriptionFeed>(
|
||||
this as SnSubscriptionFeed, _$identity);
|
||||
|
||||
/// Serializes this SnNewsSource to a JSON map.
|
||||
/// Serializes this SnSubscriptionFeed to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnNewsSource &&
|
||||
other is SnSubscriptionFeed &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.label, label) || other.label == label) &&
|
||||
(identical(other.type, type) || other.type == type) &&
|
||||
(identical(other.source, source) || other.source == source) &&
|
||||
(identical(other.depth, depth) || other.depth == depth) &&
|
||||
(identical(other.enabled, enabled) || other.enabled == enabled));
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(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)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, id, label, type, source, depth, enabled);
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
url,
|
||||
isEnabled,
|
||||
isFullContent,
|
||||
pullInterval,
|
||||
adapter,
|
||||
accountId,
|
||||
lastFetchedAt);
|
||||
|
||||
@override
|
||||
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
|
||||
abstract mixin class $SnNewsSourceCopyWith<$Res> {
|
||||
factory $SnNewsSourceCopyWith(
|
||||
SnNewsSource value, $Res Function(SnNewsSource) _then) =
|
||||
_$SnNewsSourceCopyWithImpl;
|
||||
abstract mixin class $SnSubscriptionFeedCopyWith<$Res> {
|
||||
factory $SnSubscriptionFeedCopyWith(
|
||||
SnSubscriptionFeed value, $Res Function(SnSubscriptionFeed) _then) =
|
||||
_$SnSubscriptionFeedCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String id,
|
||||
String label,
|
||||
String type,
|
||||
String source,
|
||||
int depth,
|
||||
bool enabled});
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String url,
|
||||
bool isEnabled,
|
||||
bool isFullContent,
|
||||
int pullInterval,
|
||||
String adapter,
|
||||
int? accountId,
|
||||
DateTime? lastFetchedAt});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnNewsSourceCopyWithImpl<$Res> implements $SnNewsSourceCopyWith<$Res> {
|
||||
_$SnNewsSourceCopyWithImpl(this._self, this._then);
|
||||
class _$SnSubscriptionFeedCopyWithImpl<$Res>
|
||||
implements $SnSubscriptionFeedCopyWith<$Res> {
|
||||
_$SnSubscriptionFeedCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnNewsSource _self;
|
||||
final $Res Function(SnNewsSource) _then;
|
||||
final SnSubscriptionFeed _self;
|
||||
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.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? label = null,
|
||||
Object? type = null,
|
||||
Object? source = null,
|
||||
Object? depth = null,
|
||||
Object? enabled = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? url = null,
|
||||
Object? isEnabled = null,
|
||||
Object? isFullContent = null,
|
||||
Object? pullInterval = null,
|
||||
Object? adapter = null,
|
||||
Object? accountId = freezed,
|
||||
Object? lastFetchedAt = freezed,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: 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,
|
||||
enabled: null == enabled
|
||||
? _self.enabled
|
||||
: enabled // ignore: cast_nullable_to_non_nullable
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: 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,
|
||||
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
|
||||
@JsonSerializable()
|
||||
class _SnNewsSource implements SnNewsSource {
|
||||
const _SnNewsSource(
|
||||
class _SnSubscriptionFeed implements SnSubscriptionFeed {
|
||||
const _SnSubscriptionFeed(
|
||||
{required this.id,
|
||||
required this.label,
|
||||
required this.type,
|
||||
required this.source,
|
||||
required this.depth,
|
||||
required this.enabled});
|
||||
factory _SnNewsSource.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnNewsSourceFromJson(json);
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.url,
|
||||
required this.isEnabled,
|
||||
required this.isFullContent,
|
||||
required this.pullInterval,
|
||||
required this.adapter,
|
||||
required this.accountId,
|
||||
required this.lastFetchedAt});
|
||||
factory _SnSubscriptionFeed.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnSubscriptionFeedFromJson(json);
|
||||
|
||||
@override
|
||||
final String id;
|
||||
final int id;
|
||||
@override
|
||||
final String label;
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final String type;
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final String source;
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final int depth;
|
||||
final String url;
|
||||
@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.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnNewsSourceCopyWith<_SnNewsSource> get copyWith =>
|
||||
__$SnNewsSourceCopyWithImpl<_SnNewsSource>(this, _$identity);
|
||||
_$SnSubscriptionFeedCopyWith<_SnSubscriptionFeed> get copyWith =>
|
||||
__$SnSubscriptionFeedCopyWithImpl<_SnSubscriptionFeed>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnNewsSourceToJson(
|
||||
return _$SnSubscriptionFeedToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
@ -165,129 +240,185 @@ class _SnNewsSource implements SnNewsSource {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnNewsSource &&
|
||||
other is _SnSubscriptionFeed &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.label, label) || other.label == label) &&
|
||||
(identical(other.type, type) || other.type == type) &&
|
||||
(identical(other.source, source) || other.source == source) &&
|
||||
(identical(other.depth, depth) || other.depth == depth) &&
|
||||
(identical(other.enabled, enabled) || other.enabled == enabled));
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(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)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, id, label, type, source, depth, enabled);
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
url,
|
||||
isEnabled,
|
||||
isFullContent,
|
||||
pullInterval,
|
||||
adapter,
|
||||
accountId,
|
||||
lastFetchedAt);
|
||||
|
||||
@override
|
||||
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
|
||||
abstract mixin class _$SnNewsSourceCopyWith<$Res>
|
||||
implements $SnNewsSourceCopyWith<$Res> {
|
||||
factory _$SnNewsSourceCopyWith(
|
||||
_SnNewsSource value, $Res Function(_SnNewsSource) _then) =
|
||||
__$SnNewsSourceCopyWithImpl;
|
||||
abstract mixin class _$SnSubscriptionFeedCopyWith<$Res>
|
||||
implements $SnSubscriptionFeedCopyWith<$Res> {
|
||||
factory _$SnSubscriptionFeedCopyWith(
|
||||
_SnSubscriptionFeed value, $Res Function(_SnSubscriptionFeed) _then) =
|
||||
__$SnSubscriptionFeedCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String id,
|
||||
String label,
|
||||
String type,
|
||||
String source,
|
||||
int depth,
|
||||
bool enabled});
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String url,
|
||||
bool isEnabled,
|
||||
bool isFullContent,
|
||||
int pullInterval,
|
||||
String adapter,
|
||||
int? accountId,
|
||||
DateTime? lastFetchedAt});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnNewsSourceCopyWithImpl<$Res>
|
||||
implements _$SnNewsSourceCopyWith<$Res> {
|
||||
__$SnNewsSourceCopyWithImpl(this._self, this._then);
|
||||
class __$SnSubscriptionFeedCopyWithImpl<$Res>
|
||||
implements _$SnSubscriptionFeedCopyWith<$Res> {
|
||||
__$SnSubscriptionFeedCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnNewsSource _self;
|
||||
final $Res Function(_SnNewsSource) _then;
|
||||
final _SnSubscriptionFeed _self;
|
||||
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.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? label = null,
|
||||
Object? type = null,
|
||||
Object? source = null,
|
||||
Object? depth = null,
|
||||
Object? enabled = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? url = 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
|
||||
? _self.id
|
||||
: 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,
|
||||
enabled: null == enabled
|
||||
? _self.enabled
|
||||
: enabled // ignore: cast_nullable_to_non_nullable
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: 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,
|
||||
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
|
||||
mixin _$SnNewsArticle {
|
||||
mixin _$SnSubscriptionItem {
|
||||
int get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
dynamic get deletedAt;
|
||||
DateTime? get deletedAt;
|
||||
String get thumbnail;
|
||||
String get title;
|
||||
String get description;
|
||||
String get content;
|
||||
String get url;
|
||||
String get hash;
|
||||
String get source;
|
||||
int get feedId;
|
||||
SnSubscriptionFeed get feed;
|
||||
DateTime? get publishedAt;
|
||||
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// Create a copy of SnSubscriptionItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnNewsArticleCopyWith<SnNewsArticle> get copyWith =>
|
||||
_$SnNewsArticleCopyWithImpl<SnNewsArticle>(
|
||||
this as SnNewsArticle, _$identity);
|
||||
$SnSubscriptionItemCopyWith<SnSubscriptionItem> get copyWith =>
|
||||
_$SnSubscriptionItemCopyWithImpl<SnSubscriptionItem>(
|
||||
this as SnSubscriptionItem, _$identity);
|
||||
|
||||
/// Serializes this SnNewsArticle to a JSON map.
|
||||
/// Serializes this SnSubscriptionItem to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnNewsArticle &&
|
||||
other is SnSubscriptionItem &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.thumbnail, thumbnail) ||
|
||||
other.thumbnail == thumbnail) &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
@ -296,7 +427,8 @@ mixin _$SnNewsArticle {
|
||||
(identical(other.content, content) || other.content == content) &&
|
||||
(identical(other.url, url) || other.url == url) &&
|
||||
(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) ||
|
||||
other.publishedAt == publishedAt));
|
||||
}
|
||||
@ -308,52 +440,56 @@ mixin _$SnNewsArticle {
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
const DeepCollectionEquality().hash(deletedAt),
|
||||
deletedAt,
|
||||
thumbnail,
|
||||
title,
|
||||
description,
|
||||
content,
|
||||
url,
|
||||
hash,
|
||||
source,
|
||||
feedId,
|
||||
feed,
|
||||
publishedAt);
|
||||
|
||||
@override
|
||||
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
|
||||
abstract mixin class $SnNewsArticleCopyWith<$Res> {
|
||||
factory $SnNewsArticleCopyWith(
|
||||
SnNewsArticle value, $Res Function(SnNewsArticle) _then) =
|
||||
_$SnNewsArticleCopyWithImpl;
|
||||
abstract mixin class $SnSubscriptionItemCopyWith<$Res> {
|
||||
factory $SnSubscriptionItemCopyWith(
|
||||
SnSubscriptionItem value, $Res Function(SnSubscriptionItem) _then) =
|
||||
_$SnSubscriptionItemCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
DateTime? deletedAt,
|
||||
String thumbnail,
|
||||
String title,
|
||||
String description,
|
||||
String content,
|
||||
String url,
|
||||
String hash,
|
||||
String source,
|
||||
int feedId,
|
||||
SnSubscriptionFeed feed,
|
||||
DateTime? publishedAt});
|
||||
|
||||
$SnSubscriptionFeedCopyWith<$Res> get feed;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnNewsArticleCopyWithImpl<$Res>
|
||||
implements $SnNewsArticleCopyWith<$Res> {
|
||||
_$SnNewsArticleCopyWithImpl(this._self, this._then);
|
||||
class _$SnSubscriptionItemCopyWithImpl<$Res>
|
||||
implements $SnSubscriptionItemCopyWith<$Res> {
|
||||
_$SnSubscriptionItemCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnNewsArticle _self;
|
||||
final $Res Function(SnNewsArticle) _then;
|
||||
final SnSubscriptionItem _self;
|
||||
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.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
@ -368,7 +504,8 @@ class _$SnNewsArticleCopyWithImpl<$Res>
|
||||
Object? content = null,
|
||||
Object? url = null,
|
||||
Object? hash = null,
|
||||
Object? source = null,
|
||||
Object? feedId = null,
|
||||
Object? feed = null,
|
||||
Object? publishedAt = freezed,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
@ -387,7 +524,7 @@ class _$SnNewsArticleCopyWithImpl<$Res>
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as DateTime?,
|
||||
thumbnail: null == thumbnail
|
||||
? _self.thumbnail
|
||||
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||
@ -412,22 +549,36 @@ class _$SnNewsArticleCopyWithImpl<$Res>
|
||||
? _self.hash
|
||||
: hash // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
source: null == source
|
||||
? _self.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
feedId: null == feedId
|
||||
? _self.feedId
|
||||
: feedId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
feed: null == feed
|
||||
? _self.feed
|
||||
: feed // ignore: cast_nullable_to_non_nullable
|
||||
as SnSubscriptionFeed,
|
||||
publishedAt: freezed == publishedAt
|
||||
? _self.publishedAt
|
||||
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||
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
|
||||
@JsonSerializable()
|
||||
class _SnNewsArticle implements SnNewsArticle {
|
||||
const _SnNewsArticle(
|
||||
class _SnSubscriptionItem implements SnSubscriptionItem {
|
||||
const _SnSubscriptionItem(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
@ -438,10 +589,11 @@ class _SnNewsArticle implements SnNewsArticle {
|
||||
required this.content,
|
||||
required this.url,
|
||||
required this.hash,
|
||||
required this.source,
|
||||
required this.feedId,
|
||||
required this.feed,
|
||||
required this.publishedAt});
|
||||
factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnNewsArticleFromJson(json);
|
||||
factory _SnSubscriptionItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnSubscriptionItemFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@ -450,7 +602,7 @@ class _SnNewsArticle implements SnNewsArticle {
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final dynamic deletedAt;
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final String thumbnail;
|
||||
@override
|
||||
@ -464,21 +616,23 @@ class _SnNewsArticle implements SnNewsArticle {
|
||||
@override
|
||||
final String hash;
|
||||
@override
|
||||
final String source;
|
||||
final int feedId;
|
||||
@override
|
||||
final SnSubscriptionFeed feed;
|
||||
@override
|
||||
final DateTime? publishedAt;
|
||||
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// Create a copy of SnSubscriptionItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnNewsArticleCopyWith<_SnNewsArticle> get copyWith =>
|
||||
__$SnNewsArticleCopyWithImpl<_SnNewsArticle>(this, _$identity);
|
||||
_$SnSubscriptionItemCopyWith<_SnSubscriptionItem> get copyWith =>
|
||||
__$SnSubscriptionItemCopyWithImpl<_SnSubscriptionItem>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnNewsArticleToJson(
|
||||
return _$SnSubscriptionItemToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
@ -487,13 +641,14 @@ class _SnNewsArticle implements SnNewsArticle {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnNewsArticle &&
|
||||
other is _SnSubscriptionItem &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.thumbnail, thumbnail) ||
|
||||
other.thumbnail == thumbnail) &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
@ -502,7 +657,8 @@ class _SnNewsArticle implements SnNewsArticle {
|
||||
(identical(other.content, content) || other.content == content) &&
|
||||
(identical(other.url, url) || other.url == url) &&
|
||||
(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) ||
|
||||
other.publishedAt == publishedAt));
|
||||
}
|
||||
@ -514,54 +670,59 @@ class _SnNewsArticle implements SnNewsArticle {
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
const DeepCollectionEquality().hash(deletedAt),
|
||||
deletedAt,
|
||||
thumbnail,
|
||||
title,
|
||||
description,
|
||||
content,
|
||||
url,
|
||||
hash,
|
||||
source,
|
||||
feedId,
|
||||
feed,
|
||||
publishedAt);
|
||||
|
||||
@override
|
||||
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
|
||||
abstract mixin class _$SnNewsArticleCopyWith<$Res>
|
||||
implements $SnNewsArticleCopyWith<$Res> {
|
||||
factory _$SnNewsArticleCopyWith(
|
||||
_SnNewsArticle value, $Res Function(_SnNewsArticle) _then) =
|
||||
__$SnNewsArticleCopyWithImpl;
|
||||
abstract mixin class _$SnSubscriptionItemCopyWith<$Res>
|
||||
implements $SnSubscriptionItemCopyWith<$Res> {
|
||||
factory _$SnSubscriptionItemCopyWith(
|
||||
_SnSubscriptionItem value, $Res Function(_SnSubscriptionItem) _then) =
|
||||
__$SnSubscriptionItemCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
DateTime? deletedAt,
|
||||
String thumbnail,
|
||||
String title,
|
||||
String description,
|
||||
String content,
|
||||
String url,
|
||||
String hash,
|
||||
String source,
|
||||
int feedId,
|
||||
SnSubscriptionFeed feed,
|
||||
DateTime? publishedAt});
|
||||
|
||||
@override
|
||||
$SnSubscriptionFeedCopyWith<$Res> get feed;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnNewsArticleCopyWithImpl<$Res>
|
||||
implements _$SnNewsArticleCopyWith<$Res> {
|
||||
__$SnNewsArticleCopyWithImpl(this._self, this._then);
|
||||
class __$SnSubscriptionItemCopyWithImpl<$Res>
|
||||
implements _$SnSubscriptionItemCopyWith<$Res> {
|
||||
__$SnSubscriptionItemCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnNewsArticle _self;
|
||||
final $Res Function(_SnNewsArticle) _then;
|
||||
final _SnSubscriptionItem _self;
|
||||
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.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
@ -576,10 +737,11 @@ class __$SnNewsArticleCopyWithImpl<$Res>
|
||||
Object? content = null,
|
||||
Object? url = null,
|
||||
Object? hash = null,
|
||||
Object? source = null,
|
||||
Object? feedId = null,
|
||||
Object? feed = null,
|
||||
Object? publishedAt = freezed,
|
||||
}) {
|
||||
return _then(_SnNewsArticle(
|
||||
return _then(_SnSubscriptionItem(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
@ -595,7 +757,7 @@ class __$SnNewsArticleCopyWithImpl<$Res>
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as DateTime?,
|
||||
thumbnail: null == thumbnail
|
||||
? _self.thumbnail
|
||||
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||
@ -620,16 +782,30 @@ class __$SnNewsArticleCopyWithImpl<$Res>
|
||||
? _self.hash
|
||||
: hash // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
source: null == source
|
||||
? _self.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
feedId: null == feedId
|
||||
? _self.feedId
|
||||
: feedId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
feed: null == feed
|
||||
? _self.feed
|
||||
: feed // ignore: cast_nullable_to_non_nullable
|
||||
as SnSubscriptionFeed,
|
||||
publishedAt: freezed == publishedAt
|
||||
? _self.publishedAt
|
||||
: publishedAt // ignore: cast_nullable_to_non_nullable
|
||||
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
|
||||
|
@ -6,56 +6,74 @@ part of 'news.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) =>
|
||||
_SnNewsSource(
|
||||
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(
|
||||
_SnSubscriptionFeed _$SnSubscriptionFeedFromJson(Map<String, dynamic> json) =>
|
||||
_SnSubscriptionFeed(
|
||||
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'],
|
||||
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,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
content: json['content'] as String,
|
||||
url: json['url'] 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
|
||||
? null
|
||||
: DateTime.parse(json['published_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnNewsArticleToJson(_SnNewsArticle instance) =>
|
||||
Map<String, dynamic> _$SnSubscriptionItemToJson(_SnSubscriptionItem instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt,
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'thumbnail': instance.thumbnail,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'content': instance.content,
|
||||
'url': instance.url,
|
||||
'hash': instance.hash,
|
||||
'source': instance.source,
|
||||
'feed_id': instance.feedId,
|
||||
'feed': instance.feed.toJson(),
|
||||
'published_at': instance.publishedAt?.toIso8601String(),
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
@ -26,6 +27,7 @@ abstract class SnPost with _$SnPost {
|
||||
required int? replyId,
|
||||
required int? repostId,
|
||||
required int? realmId,
|
||||
required SnRealm? realm,
|
||||
required SnPost? replyTo,
|
||||
required SnPost? repostTo,
|
||||
required List<int>? visibleUsersList,
|
||||
@ -43,9 +45,9 @@ abstract class SnPost with _$SnPost {
|
||||
@Default(0) int totalAggregatedViews,
|
||||
required int publisherId,
|
||||
required int? pollId,
|
||||
required SnPoll? poll,
|
||||
required SnPublisher publisher,
|
||||
required SnMetric metric,
|
||||
SnPostPreload? preload,
|
||||
}) = _SnPost;
|
||||
|
||||
factory SnPost.fromJson(Map<String, Object?> json) => _$SnPostFromJson(json);
|
||||
@ -146,6 +148,7 @@ abstract class SnPublisher with _$SnPublisher {
|
||||
required int totalDownvote,
|
||||
required int? realmId,
|
||||
required int accountId,
|
||||
required SnAccount? account,
|
||||
}) = _SnPublisher;
|
||||
|
||||
factory SnPublisher.fromJson(Map<String, Object?> json) =>
|
||||
@ -178,41 +181,3 @@ abstract class SnFeedEntry with _$SnFeedEntry {
|
||||
factory SnFeedEntry.fromJson(Map<String, dynamic> 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(),
|
||||
repostId: (json['repost_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
|
||||
? null
|
||||
: 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,
|
||||
publisherId: (json['publisher_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:
|
||||
SnPublisher.fromJson(json['publisher'] 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>{
|
||||
@ -92,6 +95,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
|
||||
'reply_id': instance.replyId,
|
||||
'repost_id': instance.repostId,
|
||||
'realm_id': instance.realmId,
|
||||
'realm': instance.realm?.toJson(),
|
||||
'reply_to': instance.replyTo?.toJson(),
|
||||
'repost_to': instance.repostTo?.toJson(),
|
||||
'visible_users_list': instance.visibleUsersList,
|
||||
@ -109,9 +113,9 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
|
||||
'total_aggregated_views': instance.totalAggregatedViews,
|
||||
'publisher_id': instance.publisherId,
|
||||
'poll_id': instance.pollId,
|
||||
'poll': instance.poll?.toJson(),
|
||||
'publisher': instance.publisher.toJson(),
|
||||
'metric': instance.metric.toJson(),
|
||||
'preload': instance.preload?.toJson(),
|
||||
};
|
||||
|
||||
_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(),
|
||||
realmId: (json['realm_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) =>
|
||||
@ -259,6 +266,7 @@ Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
|
||||
'total_downvote': instance.totalDownvote,
|
||||
'realm_id': instance.realmId,
|
||||
'account_id': instance.accountId,
|
||||
'account': instance.account?.toJson(),
|
||||
};
|
||||
|
||||
_SnSubscription _$SnSubscriptionFromJson(Map<String, dynamic> json) =>
|
||||
@ -295,64 +303,3 @@ Map<String, dynamic> _$SnFeedEntryToJson(_SnFeedEntry instance) =>
|
||||
'data': instance.data,
|
||||
'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),
|
||||
'joy': ReactInfo(icon: '🤣', attitude: 1),
|
||||
'pray': ReactInfo(icon: '🙏', attitude: 1),
|
||||
'heart': ReactInfo(icon: '❤️', attitude: 1),
|
||||
};
|
||||
|
@ -54,11 +54,15 @@ class AccountImage extends StatelessWidget {
|
||||
))
|
||||
.center(),
|
||||
)
|
||||
: AutoResizeUniversalImage(
|
||||
: UniversalImage(
|
||||
sn.getAttachmentUrl(url),
|
||||
filterQuality: filterQuality,
|
||||
key: Key('attachment-${content.hashCode}'),
|
||||
fit: BoxFit.cover,
|
||||
width: (radius != null ? radius! : 20) * 2,
|
||||
height: (radius != null ? radius! : 20) * 2,
|
||||
cacheWidth: (radius != null ? radius! : 20) * 2,
|
||||
cacheHeight: (radius != null ? radius! : 20) * 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -22,12 +22,14 @@ class AccountPopoverCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Column(
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (data.banner.isNotEmpty)
|
||||
Container(
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
@ -37,8 +39,10 @@ class AccountPopoverCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
).padding(all: 16)
|
||||
else
|
||||
const Gap(16),
|
||||
// Top padding
|
||||
Gap(16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -60,14 +64,15 @@ class AccountPopoverCard extends StatelessWidget {
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
GoRouter.of(context).pushNamed(
|
||||
GoRouter.of(context).pushReplacementNamed(
|
||||
'accountProfilePage',
|
||||
pathParameters: {'name': data.name},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.chevron_right),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
const Gap(8)
|
||||
],
|
||||
@ -86,7 +91,9 @@ class AccountPopoverCard extends StatelessWidget {
|
||||
data.profile?.description ?? '',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).padding(horizontal: 26, bottom: 8),
|
||||
).padding(horizontal: 26, bottom: 8)
|
||||
else
|
||||
const Gap(12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@ -104,7 +111,8 @@ class AccountPopoverCard extends StatelessWidget {
|
||||
child: LinearProgressIndicator(
|
||||
value: calcLevelUpProgress(data.profile?.experience ?? 0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
).alignment(Alignment.centerLeft),
|
||||
),
|
||||
],
|
||||
@ -156,8 +164,9 @@ class AccountPopoverCard extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
// Bottom padding
|
||||
const Gap(16),
|
||||
const Gap(64),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,9 @@ import 'package:surface/widgets/dialog.dart';
|
||||
class AttachmentInputDialog extends StatefulWidget {
|
||||
final String? title;
|
||||
final bool? analyzeNow;
|
||||
final bool canPickMedia;
|
||||
final bool canReferenceLink;
|
||||
final bool canRandomId;
|
||||
final SnMediaType? mediaType;
|
||||
final String pool;
|
||||
|
||||
@ -21,6 +24,9 @@ class AttachmentInputDialog extends StatefulWidget {
|
||||
required this.pool,
|
||||
this.analyzeNow = false,
|
||||
this.mediaType = SnMediaType.image,
|
||||
this.canPickMedia = true,
|
||||
this.canReferenceLink = true,
|
||||
this.canRandomId = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -29,6 +35,8 @@ class AttachmentInputDialog extends StatefulWidget {
|
||||
|
||||
class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
final _randomIdController = TextEditingController();
|
||||
final _referenceLinkController = TextEditingController();
|
||||
final _referenceMimetypeController = TextEditingController();
|
||||
|
||||
XFile? _file;
|
||||
double? _progress;
|
||||
@ -61,9 +69,26 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
} else if (_referenceLinkController.text.isNotEmpty) {
|
||||
try {
|
||||
final attachment = await attach.createWithReferenceLink(
|
||||
_referenceLinkController.text,
|
||||
widget.pool,
|
||||
null,
|
||||
mimetype: _referenceMimetypeController.text.isNotEmpty
|
||||
? _referenceMimetypeController.text
|
||||
: null,
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, attachment);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
} else if (_file != null) {
|
||||
try {
|
||||
final place = await attach.chunkedUploadInitialize(await _file!.length(), _file!.name, widget.pool, null);
|
||||
final place = await attach.chunkedUploadInitialize(
|
||||
await _file!.length(), _file!.name, widget.pool, null);
|
||||
|
||||
final attachment = await attach.chunkedUploadParts(
|
||||
_file!,
|
||||
@ -89,8 +114,15 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
return AlertDialog(
|
||||
title: Text(widget.title ?? 'attachmentInputDialog'.tr()),
|
||||
content: Column(
|
||||
spacing: 16,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_file == null &&
|
||||
_referenceLinkController.text.isEmpty &&
|
||||
widget.canRandomId)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('attachmentInputUseRandomId').tr().fontSize(14),
|
||||
const Gap(8),
|
||||
@ -98,22 +130,73 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
controller: _randomIdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldAttachmentRandomId'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(24),
|
||||
],
|
||||
),
|
||||
if (_file == null &&
|
||||
_referenceLinkController.text.isEmpty &&
|
||||
widget.canReferenceLink)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('attachmentReferenceLink').tr().fontSize(14),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: _referenceLinkController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldAttachmentReferenceLink'.tr(),
|
||||
helperText: 'attachmentReferenceLinkDescription'.tr(),
|
||||
helperMaxLines: 3,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: _referenceLinkController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldAttachmentMimetype'.tr(),
|
||||
helperText: 'class/type',
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_referenceLinkController.text.isEmpty &&
|
||||
_randomIdController.text.isEmpty &&
|
||||
widget.canPickMedia)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('attachmentInputNew').tr().fontSize(14),
|
||||
Card(
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
leading: const Icon(Symbols.add_photo_alternate),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('addAttachmentFromAlbum').tr(),
|
||||
subtitle: _file == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
|
||||
subtitle: _file == null
|
||||
? Text('unset').tr()
|
||||
: Text('waitingForUpload').tr(),
|
||||
onTap: () {
|
||||
_pickMedia();
|
||||
},
|
||||
@ -121,6 +204,8 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isBusy)
|
||||
LinearProgressIndicator(
|
||||
value: _progress,
|
||||
|
@ -15,6 +15,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
@ -25,6 +26,7 @@ class AttachmentItem extends StatelessWidget {
|
||||
final String? heroTag;
|
||||
final BoxFit fit;
|
||||
final FilterQuality? filterQuality;
|
||||
final Function? onZoom;
|
||||
|
||||
const AttachmentItem({
|
||||
super.key,
|
||||
@ -32,6 +34,7 @@ class AttachmentItem extends StatelessWidget {
|
||||
required this.data,
|
||||
required this.heroTag,
|
||||
this.filterQuality,
|
||||
this.onZoom,
|
||||
});
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
@ -94,7 +97,14 @@ class AttachmentItem extends StatelessWidget {
|
||||
});
|
||||
}
|
||||
|
||||
return _buildContent(context);
|
||||
return GestureDetector(
|
||||
child: _buildContent(context),
|
||||
onTap: () {
|
||||
if (data?.mimetype.startsWith('image') ?? false) {
|
||||
onZoom?.call();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,6 +229,7 @@ class _AttachmentItemContentVideoState
|
||||
setState(() => _showContent = true);
|
||||
MediaKit.ensureInitialized();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
final url = _showOriginal
|
||||
? sn.getAttachmentUrl(widget.data.rid)
|
||||
: sn.getAttachmentUrl(widget.data.compressed!.rid);
|
||||
@ -231,6 +242,7 @@ class _AttachmentItemContentVideoState
|
||||
logging.info('[MediaPlayer] Miss cache: $url');
|
||||
final fileStream = DefaultCacheManager().getFileStream(
|
||||
url,
|
||||
headers: {'Authorization': 'Bearer ${await ua.atk}'},
|
||||
withProgress: true,
|
||||
);
|
||||
await for (var fileInfo in fileStream) {
|
||||
@ -490,6 +502,7 @@ class _AttachmentItemContentAudioState
|
||||
setState(() => _showContent = true);
|
||||
MediaKit.ensureInitialized();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
final url = sn.getAttachmentUrl(widget.data.rid);
|
||||
_audioPlayer = Player();
|
||||
|
||||
@ -499,6 +512,7 @@ class _AttachmentItemContentAudioState
|
||||
logging.info('[MediaPlayer] Miss cache: $url');
|
||||
final fileStream = DefaultCacheManager().getFileStream(
|
||||
url,
|
||||
headers: {'Authorization': 'Bearer ${await ua.atk}'},
|
||||
withProgress: true,
|
||||
);
|
||||
await for (var fileInfo in fileStream) {
|
||||
|
@ -74,7 +74,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
return Container(
|
||||
padding: widget.padding ?? EdgeInsets.zero,
|
||||
constraints: constraints,
|
||||
child: GestureDetector(
|
||||
child: AspectRatio(
|
||||
aspectRatio: singleAspectRatio,
|
||||
child: Container(
|
||||
@ -90,14 +89,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
heroTag: heroTags[0],
|
||||
fit: widget.fit,
|
||||
filterQuality: widget.filterQuality,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) {
|
||||
return;
|
||||
}
|
||||
onZoom: () {
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data.where((ele) => ele != null).cast(),
|
||||
@ -109,6 +101,9 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -133,21 +128,14 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
mainAxisSpacing: 4,
|
||||
children: widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => GestureDetector(
|
||||
child: Container(
|
||||
(idx, ele) => Container(
|
||||
constraints: constraints,
|
||||
child: AttachmentItem(
|
||||
data: ele,
|
||||
heroTag: heroTags[idx],
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: widget.filterQuality,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.data[idx]!.mediaType !=
|
||||
SnMediaType.image) {
|
||||
return;
|
||||
}
|
||||
onZoom: () {
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data
|
||||
@ -161,6 +149,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
@ -181,8 +170,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
child: Column(
|
||||
children: widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => GestureDetector(
|
||||
child: AspectRatio(
|
||||
(idx, ele) => AspectRatio(
|
||||
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
@ -191,7 +179,21 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
heroTag: heroTags[idx],
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: widget.filterQuality,
|
||||
onZoom: () {
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data
|
||||
.where((ele) =>
|
||||
ele != null &&
|
||||
ele.mediaType == SnMediaType.image)
|
||||
.cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -222,26 +224,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
child: AspectRatio(
|
||||
aspectRatio:
|
||||
(widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.data[idx]?.mediaType !=
|
||||
SnMediaType.image) {
|
||||
return;
|
||||
}
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data
|
||||
.where((ele) =>
|
||||
ele != null &&
|
||||
ele.mediaType == SnMediaType.image)
|
||||
.cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
@ -260,6 +242,23 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
data: widget.data[idx],
|
||||
heroTag: heroTags[idx],
|
||||
filterQuality: widget.filterQuality,
|
||||
onZoom: () {
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data
|
||||
.where((ele) =>
|
||||
ele != null &&
|
||||
ele.mediaType ==
|
||||
SnMediaType.image)
|
||||
.cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
backgroundColor:
|
||||
Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -273,7 +272,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
|
@ -16,7 +16,6 @@ import 'package:photo_view/photo_view_gallery.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -65,7 +64,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
Future<void> _saveToAlbum(int idx) async {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final item = widget.data.elementAt(idx);
|
||||
final url = sn.getAttachmentUrl(item.rid);
|
||||
final url = sn.getAttachmentUrl(item.rid, preview: false);
|
||||
|
||||
if (kIsWeb || Platform.isLinux) {
|
||||
await launchUrlString(url);
|
||||
@ -182,7 +181,10 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
scaleState == PhotoViewScaleState.initial);
|
||||
},
|
||||
imageProvider: UniversalImage.provider(
|
||||
sn.getAttachmentUrl(widget.data.first.rid),
|
||||
sn.getAttachmentUrl(
|
||||
widget.data.first.rid,
|
||||
preview: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -200,7 +202,10 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
widget.heroTags?.elementAt(idx) ?? uuid.v4();
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: UniversalImage.provider(
|
||||
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
|
||||
sn.getAttachmentUrl(
|
||||
widget.data.elementAt(idx).rid,
|
||||
preview: false,
|
||||
),
|
||||
),
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
||||
@ -368,7 +373,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
_showDetail = true;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _AttachmentZoomDetailPopup(
|
||||
builder: (context) => AttachmentZoomDetailPopup(
|
||||
data: widget.data.elementAt(_page),
|
||||
),
|
||||
).then((_) {
|
||||
@ -398,7 +403,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
_showDetail = true;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _AttachmentZoomDetailPopup(
|
||||
builder: (context) => AttachmentZoomDetailPopup(
|
||||
data: widget.data.elementAt(_page),
|
||||
),
|
||||
).then((_) {
|
||||
@ -411,15 +416,14 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
}
|
||||
}
|
||||
|
||||
class _AttachmentZoomDetailPopup extends StatelessWidget {
|
||||
class AttachmentZoomDetailPopup extends StatelessWidget {
|
||||
final SnAttachment data;
|
||||
|
||||
const _AttachmentZoomDetailPopup({required this.data});
|
||||
const AttachmentZoomDetailPopup({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final account = ud.getFromCache(data.accountId);
|
||||
final account = data.account!;
|
||||
|
||||
const tableGap = TableRow(
|
||||
children: [
|
||||
@ -461,12 +465,12 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
||||
children: [
|
||||
if (data.accountId > 0)
|
||||
AccountImage(
|
||||
content: account?.avatar,
|
||||
content: account.avatar,
|
||||
radius: 8,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(data.accountId > 0
|
||||
? account?.nick ?? 'unknown'.tr()
|
||||
? account.nick
|
||||
: 'unknown'.tr()),
|
||||
const Gap(8),
|
||||
Text('#${data.accountId}',
|
||||
|
336
lib/widgets/attachment/pending_attachment_actions.dart
Normal file
336
lib/widgets/attachment/pending_attachment_actions.dart
Normal file
@ -0,0 +1,336 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:croppy/croppy.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_input.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_compress.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_rating.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
|
||||
class PendingAttachmentActionSheet extends StatefulWidget {
|
||||
final PostWriteMedia media;
|
||||
final bool canInsertLink;
|
||||
const PendingAttachmentActionSheet({
|
||||
super.key,
|
||||
required this.media,
|
||||
this.canInsertLink = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PendingAttachmentActionSheet> createState() =>
|
||||
_PendingAttachmentActionSheetState();
|
||||
}
|
||||
|
||||
class _PendingAttachmentActionSheetState
|
||||
extends State<PendingAttachmentActionSheet> {
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _cropImage() async {
|
||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||
? await showCupertinoImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
// ignore: use_build_context_synchronously
|
||||
imageProvider: widget.media.getImageProvider(context)!,
|
||||
)
|
||||
: await showMaterialImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
// ignore: use_build_context_synchronously
|
||||
imageProvider: widget.media.getImageProvider(context)!,
|
||||
);
|
||||
|
||||
if (result == null) return;
|
||||
|
||||
final rawBytes =
|
||||
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List();
|
||||
|
||||
if (!mounted) return;
|
||||
final updatedMedia = PostWriteMedia.fromBytes(
|
||||
rawBytes, widget.media.name, widget.media.type);
|
||||
Navigator.pop(context, updatedMedia);
|
||||
}
|
||||
|
||||
Future<void> _setThumbnail() async {
|
||||
final thumbnail = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
title: 'attachmentSetThumbnail'.tr(),
|
||||
pool: 'interactive',
|
||||
analyzeNow: true,
|
||||
),
|
||||
);
|
||||
if (thumbnail == null) return;
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
final newAttach = await attach.updateOne(widget.media.attachment!,
|
||||
thumbnailId: thumbnail.id);
|
||||
if (mounted) Navigator.pop(context, newAttach);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteAttachment() async {
|
||||
if (_isBusy) return;
|
||||
if (widget.media.attachment == null) return;
|
||||
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client
|
||||
.delete('/cgi/uc/attachments/${widget.media.attachment!.id}');
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, false);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createBoost() async {
|
||||
final result = await showDialog<SnAttachmentBoost?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentBoostDialog(
|
||||
media: widget.media,
|
||||
),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
final newAttach = widget.media.attachment!
|
||||
.copyWith(boosts: [...widget.media.attachment!.boosts, result]);
|
||||
final newMedia = PostWriteMedia(newAttach);
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, newMedia);
|
||||
}
|
||||
|
||||
Future<void> _compressVideo() async {
|
||||
final result = await showDialog<PostWriteMedia?>(
|
||||
context: context,
|
||||
builder: (context) => PendingVideoCompressDialog(media: widget.media),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, result);
|
||||
}
|
||||
|
||||
Future<void> _setAlt() async {
|
||||
final result = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentAltDialog(media: widget.media),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, PostWriteMedia(result));
|
||||
}
|
||||
|
||||
Future<void> _setRating() async {
|
||||
final result = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentRateDialog(media: widget.media),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, PostWriteMedia(result));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.edit, size: 24),
|
||||
const Gap(16),
|
||||
Text('attachmentEditor')
|
||||
.tr()
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
if (widget.media.attachment == null)
|
||||
Text('attachmentEditorUnUploadHint')
|
||||
.tr()
|
||||
.textStyle(Theme.of(context).textTheme.bodyMedium!)
|
||||
.padding(horizontal: 20, bottom: 8)
|
||||
.opacity(0.8)
|
||||
else
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.media.attachment!.alt),
|
||||
Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Text(widget.media.attachment!.size.formatBytes()),
|
||||
Text(
|
||||
widget.media.attachment!.mimetype,
|
||||
style: GoogleFonts.robotoMono(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text('attachmentEditorUploadHint')
|
||||
.tr()
|
||||
.textStyle(Theme.of(context).textTheme.bodyMedium!)
|
||||
.opacity(0.8),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 8),
|
||||
).padding(horizontal: 16, bottom: 8),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (widget.media.attachment == null)
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.upload),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('attachmentUpload').tr(),
|
||||
onTap: () => Navigator.pop(context, true),
|
||||
),
|
||||
if (widget.media.type == SnMediaType.video)
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.compress),
|
||||
title: Text('attachmentCompressVideo').tr(),
|
||||
onTap: () => _compressVideo(),
|
||||
),
|
||||
if (widget.media.type == SnMediaType.image)
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.crop),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('crop').tr(),
|
||||
onTap: () => _cropImage(),
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.delete),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('delete').tr(),
|
||||
onTap: () => Navigator.pop(context, false),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.preview),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('preview').tr(),
|
||||
onTap: () {
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(data: [widget.media.attachment!]),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.copy_all),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('attachmentCopyRandomId').tr(),
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: widget.media.attachment!.rid,
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
if (widget.canInsertLink)
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.add_link),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('attachmentInsertLink').tr(),
|
||||
onTap: () {
|
||||
Navigator.pop(context, 'link');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.bolt),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('attachmentBoost').tr(),
|
||||
onTap: () => _createBoost(),
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.thumbnail_bar),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('attachmentSetThumbnail').tr(),
|
||||
onTap: () => _setThumbnail(),
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.description),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('attachmentSetAlt').tr(),
|
||||
onTap: () => _setAlt(),
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.star),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('attachmentRating').tr(),
|
||||
onTap: () => _setRating(),
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.link_off),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('unlink').tr(),
|
||||
onTap: () => Navigator.pop(context, false),
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.delete),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('delete').tr(),
|
||||
onTap: () => _deleteAttachment(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -10,10 +10,12 @@ class PendingAttachmentAltDialog extends StatefulWidget {
|
||||
const PendingAttachmentAltDialog({super.key, required this.media});
|
||||
|
||||
@override
|
||||
State<PendingAttachmentAltDialog> createState() => _PendingAttachmentAltDialogState();
|
||||
State<PendingAttachmentAltDialog> createState() =>
|
||||
_PendingAttachmentAltDialogState();
|
||||
}
|
||||
|
||||
class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> {
|
||||
class _PendingAttachmentAltDialogState
|
||||
extends State<PendingAttachmentAltDialog> {
|
||||
final _contentController = TextEditingController();
|
||||
|
||||
@override
|
||||
@ -63,7 +65,7 @@ class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog>
|
||||
controller: _contentController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldAttachmentAlt'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
@ -71,7 +73,9 @@ class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog>
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () {
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('dialogDismiss'.tr()),
|
||||
|
@ -14,10 +14,12 @@ class PendingAttachmentBoostDialog extends StatefulWidget {
|
||||
const PendingAttachmentBoostDialog({super.key, required this.media});
|
||||
|
||||
@override
|
||||
State<PendingAttachmentBoostDialog> createState() => _PendingAttachmentBoostDialogState();
|
||||
State<PendingAttachmentBoostDialog> createState() =>
|
||||
_PendingAttachmentBoostDialogState();
|
||||
}
|
||||
|
||||
class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDialog> {
|
||||
class _PendingAttachmentBoostDialogState
|
||||
extends State<PendingAttachmentBoostDialog> {
|
||||
List<SnAttachmentDestination>? _regions;
|
||||
SnAttachmentDestination? _selectedRegion;
|
||||
|
||||
@ -84,17 +86,23 @@ class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDia
|
||||
children: _regions!.map(
|
||||
(ele) {
|
||||
return RadioListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
title: Text(ele.label).tr(),
|
||||
subtitle: Text(
|
||||
'attachmentDestinationRegion${ele.region}'.trExists()
|
||||
? 'attachmentDestinationRegion${ele.region}'.tr()
|
||||
? 'attachmentDestinationRegion${ele.region}'
|
||||
.tr()
|
||||
: ele.region,
|
||||
),
|
||||
selected: _selectedRegion == ele,
|
||||
value: ele,
|
||||
groupValue: _selectedRegion,
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _selectedRegion = value);
|
||||
if (value != null) {
|
||||
setState(() => _selectedRegion = value);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -105,7 +113,9 @@ class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDia
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () {
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('dialogDismiss'.tr()),
|
||||
|
108
lib/widgets/attachment/pending_attachment_rating.dart
Normal file
108
lib/widgets/attachment/pending_attachment_rating.dart
Normal file
@ -0,0 +1,108 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class PendingAttachmentRateDialog extends StatefulWidget {
|
||||
final PostWriteMedia media;
|
||||
const PendingAttachmentRateDialog({super.key, required this.media});
|
||||
|
||||
@override
|
||||
State<PendingAttachmentRateDialog> createState() =>
|
||||
_PendingAttachmentRateDialogState();
|
||||
}
|
||||
|
||||
class _PendingAttachmentRateDialogState
|
||||
extends State<PendingAttachmentRateDialog> {
|
||||
final _ratingController = TextEditingController();
|
||||
final _qualityController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_qualityController.text = widget.media.attachment!.qualityRating.toString();
|
||||
_ratingController.text = widget.media.attachment!.contentRating.toString();
|
||||
}
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _performAction() async {
|
||||
if (_isBusy) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
final result = await attach.rateOne(
|
||||
widget.media.attachment!,
|
||||
quality: int.tryParse(_qualityController.text),
|
||||
content: int.tryParse(_ratingController.text),
|
||||
);
|
||||
if (!mounted) return;
|
||||
attach.putCache([result]);
|
||||
Navigator.pop(context, result);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_qualityController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('attachmentRating').tr(),
|
||||
content: Column(
|
||||
spacing: 12,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _ratingController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldAttachmentRating'.tr(),
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
helperText: '3 - 21',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextField(
|
||||
controller: _qualityController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldAttachmentQuality'.tr(),
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
helperText: '0 - 5',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('dialogDismiss'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _performAction(),
|
||||
child: Text('dialogConfirm'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,369 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class ControlsWidget extends StatefulWidget {
|
||||
final Room room;
|
||||
final LocalParticipant participant;
|
||||
|
||||
const ControlsWidget(
|
||||
this.room,
|
||||
this.participant, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ControlsWidgetState();
|
||||
}
|
||||
|
||||
class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
CameraPosition _position = CameraPosition.front;
|
||||
|
||||
List<MediaDevice>? _audioInputs;
|
||||
List<MediaDevice>? _audioOutputs;
|
||||
List<MediaDevice>? _videoInputs;
|
||||
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
bool _speakerphoneOn = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_participant.addListener(onChange);
|
||||
_subscription = Hardware.instance.onDeviceChange.stream
|
||||
.listen((List<MediaDevice> devices) {
|
||||
_revertDevices(devices);
|
||||
});
|
||||
Hardware.instance.enumerateDevices().then(_revertDevices);
|
||||
_speakerphoneOn = Hardware.instance.speakerOn ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
_participant.removeListener(onChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
LocalParticipant get _participant => widget.participant;
|
||||
|
||||
void _revertDevices(List<MediaDevice> devices) async {
|
||||
_audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
|
||||
_audioOutputs = devices.where((d) => d.kind == 'audiooutput').toList();
|
||||
_videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void onChange() => setState(() {});
|
||||
|
||||
bool get isMuted => _participant.isMuted;
|
||||
|
||||
Future<bool?> showDisconnectDialog() {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('callDisconnect').tr(),
|
||||
content: Text('callDisconnectDescription').tr(),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text('cancel').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text('dialogConfirm').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _disconnect() async {
|
||||
if (await showDisconnectDialog() != true) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final call = context.read<ChatCallProvider>();
|
||||
if (call.current != null) {
|
||||
call.disposeRoom();
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _disableAudio() async {
|
||||
await _participant.setMicrophoneEnabled(false);
|
||||
}
|
||||
|
||||
void _enableAudio() async {
|
||||
await _participant.setMicrophoneEnabled(true);
|
||||
}
|
||||
|
||||
void _disableVideo() async {
|
||||
await _participant.setCameraEnabled(false);
|
||||
}
|
||||
|
||||
void _enableVideo() async {
|
||||
await _participant.setCameraEnabled(true);
|
||||
}
|
||||
|
||||
void _selectAudioOutput(MediaDevice device) async {
|
||||
await widget.room.setAudioOutputDevice(device);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _selectAudioInput(MediaDevice device) async {
|
||||
await widget.room.setAudioInputDevice(device);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _selectVideoInput(MediaDevice device) async {
|
||||
await widget.room.setVideoInputDevice(device);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _toggleSpeakerphoneOn() {
|
||||
_speakerphoneOn = !_speakerphoneOn;
|
||||
Hardware.instance.setSpeakerphoneOn(_speakerphoneOn);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _toggleCamera() async {
|
||||
final track = _participant.videoTrackPublications.firstOrNull?.track;
|
||||
if (track == null) return;
|
||||
|
||||
try {
|
||||
final newPosition = _position.switched();
|
||||
await track.setCameraPosition(newPosition);
|
||||
setState(() {
|
||||
_position = newPosition;
|
||||
});
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void _enableScreenShare() async {
|
||||
if (lkPlatformIsDesktop()) {
|
||||
try {
|
||||
final source = await showDialog<DesktopCapturerSource>(
|
||||
context: context,
|
||||
builder: (context) => ScreenSelectDialog(),
|
||||
);
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
var track = await LocalVideoTrack.createScreenShareTrack(
|
||||
ScreenShareCaptureOptions(
|
||||
captureScreenAudio: true,
|
||||
sourceId: source.id,
|
||||
maxFrameRate: 30.0,
|
||||
),
|
||||
);
|
||||
await _participant.publishVideoTrack(track);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (lkPlatformIs(PlatformType.iOS)) {
|
||||
var track = await LocalVideoTrack.createScreenShareTrack(
|
||||
const ScreenShareCaptureOptions(
|
||||
useiOSBroadcastExtension: true,
|
||||
captureScreenAudio: true,
|
||||
maxFrameRate: 30.0,
|
||||
),
|
||||
);
|
||||
await _participant.publishVideoTrack(track);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lkPlatformIsWebMobile()) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('Screen share is not supported mobile platform.'),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
await _participant.setScreenShareEnabled(true, captureScreenAudio: true);
|
||||
}
|
||||
|
||||
void _disableScreenShare() async {
|
||||
await _participant.setScreenShareEnabled(false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 5,
|
||||
runSpacing: 5,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.exit_to_app),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: _disconnect,
|
||||
),
|
||||
if (_participant.isMicrophoneEnabled())
|
||||
if (lkPlatformIs(PlatformType.android))
|
||||
IconButton(
|
||||
onPressed: _disableAudio,
|
||||
icon: const Icon(Symbols.mic),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
tooltip: 'callMicrophoneOff'.tr(),
|
||||
)
|
||||
else
|
||||
PopupMenuButton<MediaDevice>(
|
||||
icon: const Icon(Symbols.settings_voice),
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
PopupMenuItem<MediaDevice>(
|
||||
value: null,
|
||||
onTap: isMuted ? _enableAudio : _disableAudio,
|
||||
child: ListTile(
|
||||
leading: const Icon(Symbols.mic_off),
|
||||
title: Text(isMuted
|
||||
? 'callMicrophoneOn'.tr()
|
||||
: 'callMicrophoneOff'.tr()),
|
||||
),
|
||||
),
|
||||
if (_audioInputs != null)
|
||||
..._audioInputs!.map((device) {
|
||||
return PopupMenuItem<MediaDevice>(
|
||||
value: device,
|
||||
child: ListTile(
|
||||
leading: (device.deviceId ==
|
||||
widget.room.selectedAudioInputDeviceId)
|
||||
? const Icon(Symbols.check_box)
|
||||
: const Icon(Symbols.check_box_outline_blank),
|
||||
title: Text(device.label),
|
||||
),
|
||||
onTap: () => _selectAudioInput(device),
|
||||
);
|
||||
})
|
||||
];
|
||||
},
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: _enableAudio,
|
||||
icon: const Icon(Symbols.mic_off),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
tooltip: 'callMicrophoneOn'.tr(),
|
||||
),
|
||||
if (_participant.isCameraEnabled())
|
||||
PopupMenuButton<MediaDevice>(
|
||||
icon: const Icon(Symbols.videocam_sharp),
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
PopupMenuItem<MediaDevice>(
|
||||
value: null,
|
||||
onTap: _disableVideo,
|
||||
child: ListTile(
|
||||
leading: const Icon(Symbols.videocam_off),
|
||||
title: Text('callCameraOff'.tr()),
|
||||
),
|
||||
),
|
||||
if (_videoInputs != null)
|
||||
..._videoInputs!.map((device) {
|
||||
return PopupMenuItem<MediaDevice>(
|
||||
value: device,
|
||||
child: ListTile(
|
||||
leading: (device.deviceId ==
|
||||
widget.room.selectedVideoInputDeviceId)
|
||||
? const Icon(Symbols.check_box)
|
||||
: const Icon(Symbols.check_box_outline_blank),
|
||||
title: Text(device.label),
|
||||
),
|
||||
onTap: () => _selectVideoInput(device),
|
||||
);
|
||||
})
|
||||
];
|
||||
},
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: _enableVideo,
|
||||
icon: const Icon(Symbols.videocam_off),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
tooltip: 'callCameraOn'.tr(),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(_position == CameraPosition.back
|
||||
? Symbols.video_camera_back
|
||||
: Symbols.video_camera_front),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => _toggleCamera(),
|
||||
tooltip: 'callVideoFlip'.tr(),
|
||||
),
|
||||
if (!lkPlatformIs(PlatformType.iOS))
|
||||
PopupMenuButton<MediaDevice>(
|
||||
icon: const Icon(Symbols.volume_up),
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
PopupMenuItem<MediaDevice>(
|
||||
value: null,
|
||||
child: ListTile(
|
||||
leading: const Icon(Symbols.speaker),
|
||||
title: Text('callSpeakerSelect').tr(),
|
||||
),
|
||||
),
|
||||
if (_audioOutputs != null)
|
||||
..._audioOutputs!.map((device) {
|
||||
return PopupMenuItem<MediaDevice>(
|
||||
value: device,
|
||||
child: ListTile(
|
||||
leading: (device.deviceId ==
|
||||
widget.room.selectedAudioOutputDeviceId)
|
||||
? const Icon(Symbols.check_box)
|
||||
: const Icon(Symbols.check_box_outline_blank),
|
||||
title: Text(device.label),
|
||||
),
|
||||
onTap: () => _selectAudioOutput(device),
|
||||
);
|
||||
})
|
||||
];
|
||||
},
|
||||
),
|
||||
if (!kIsWeb && Hardware.instance.canSwitchSpeakerphone)
|
||||
IconButton(
|
||||
onPressed: _toggleSpeakerphoneOn,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
icon: _speakerphoneOn
|
||||
? Icon(Symbols.volume_up)
|
||||
: Icon(Symbols.volume_down),
|
||||
tooltip: 'callSpeakerphoneToggle'.tr(),
|
||||
),
|
||||
if (_participant.isScreenShareEnabled())
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.stop_screen_share),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => _disableScreenShare(),
|
||||
tooltip: 'callScreenOff'.tr(),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.screen_share),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => _enableScreenShare(),
|
||||
tooltip: 'callScreenOn'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
|
||||
class NoContentWidget extends StatefulWidget {
|
||||
final SnAccount? userinfo;
|
||||
final bool isSpeaking;
|
||||
final bool isFixed;
|
||||
|
||||
const NoContentWidget({
|
||||
super.key,
|
||||
this.userinfo,
|
||||
this.isFixed = false,
|
||||
required this.isSpeaking,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NoContentWidget> createState() => _NoContentWidgetState();
|
||||
}
|
||||
|
||||
class _NoContentWidgetState extends State<NoContentWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _animationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(NoContentWidget old) {
|
||||
super.didUpdateWidget(old);
|
||||
if (widget.isSpeaking) {
|
||||
_animationController.repeat(reverse: true);
|
||||
} else {
|
||||
_animationController
|
||||
.animateTo(0, duration: 300.ms)
|
||||
.then((_) => _animationController.reset());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double radius = widget.isFixed
|
||||
? 32
|
||||
: math.min(
|
||||
MediaQuery.of(context).size.width * 0.1,
|
||||
MediaQuery.of(context).size.height * 0.1,
|
||||
);
|
||||
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
child: Center(
|
||||
child: Animate(
|
||||
autoPlay: false,
|
||||
controller: _animationController,
|
||||
effects: [
|
||||
CustomEffect(
|
||||
begin: widget.isSpeaking ? 2 : 0,
|
||||
end: 8,
|
||||
curve: Curves.easeInOut,
|
||||
duration: 1250.ms,
|
||||
builder: (context, value, child) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(radius + 8)),
|
||||
border: value > 0
|
||||
? Border.all(color: Colors.green, width: value)
|
||||
: null,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
)
|
||||
],
|
||||
child: AccountImage(
|
||||
content: widget.userinfo?.avatar,
|
||||
radius: radius,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -1,242 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/chat/call/call_no_content.dart';
|
||||
import 'package:surface/widgets/chat/call/call_participant_info.dart';
|
||||
import 'package:surface/widgets/chat/call/call_participant_menu.dart';
|
||||
import 'package:surface/widgets/chat/call/call_participant_stats.dart';
|
||||
|
||||
abstract class ParticipantWidget extends StatefulWidget {
|
||||
static ParticipantWidget widgetFor(ParticipantTrack participantTrack,
|
||||
{bool isFixed = false, bool showStatsLayer = false}) {
|
||||
if (participantTrack.participant is LocalParticipant) {
|
||||
return LocalParticipantWidget(
|
||||
participantTrack.participant as LocalParticipant,
|
||||
participantTrack.videoTrack,
|
||||
isFixed,
|
||||
participantTrack.isScreenShare,
|
||||
showStatsLayer,
|
||||
);
|
||||
} else if (participantTrack.participant is RemoteParticipant) {
|
||||
return RemoteParticipantWidget(
|
||||
participantTrack.participant as RemoteParticipant,
|
||||
participantTrack.videoTrack,
|
||||
isFixed,
|
||||
participantTrack.isScreenShare,
|
||||
showStatsLayer,
|
||||
);
|
||||
}
|
||||
throw UnimplementedError('Unknown participant type');
|
||||
}
|
||||
|
||||
abstract final Participant participant;
|
||||
abstract final VideoTrack? videoTrack;
|
||||
abstract final bool isScreenShare;
|
||||
abstract final bool isFixed;
|
||||
abstract final bool showStatsLayer;
|
||||
final VideoQuality quality;
|
||||
|
||||
const ParticipantWidget({
|
||||
super.key,
|
||||
this.quality = VideoQuality.MEDIUM,
|
||||
});
|
||||
}
|
||||
|
||||
class LocalParticipantWidget extends ParticipantWidget {
|
||||
@override
|
||||
final LocalParticipant participant;
|
||||
@override
|
||||
final VideoTrack? videoTrack;
|
||||
@override
|
||||
final bool isFixed;
|
||||
@override
|
||||
final bool isScreenShare;
|
||||
@override
|
||||
final bool showStatsLayer;
|
||||
|
||||
const LocalParticipantWidget(
|
||||
this.participant,
|
||||
this.videoTrack,
|
||||
this.isFixed,
|
||||
this.isScreenShare,
|
||||
this.showStatsLayer, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _LocalParticipantWidgetState();
|
||||
}
|
||||
|
||||
class RemoteParticipantWidget extends ParticipantWidget {
|
||||
@override
|
||||
final RemoteParticipant participant;
|
||||
@override
|
||||
final VideoTrack? videoTrack;
|
||||
@override
|
||||
final bool isFixed;
|
||||
@override
|
||||
final bool isScreenShare;
|
||||
@override
|
||||
final bool showStatsLayer;
|
||||
|
||||
const RemoteParticipantWidget(
|
||||
this.participant,
|
||||
this.videoTrack,
|
||||
this.isFixed,
|
||||
this.isScreenShare,
|
||||
this.showStatsLayer, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _RemoteParticipantWidgetState();
|
||||
}
|
||||
|
||||
abstract class _ParticipantWidgetState<T extends ParticipantWidget>
|
||||
extends State<T> {
|
||||
VideoTrack? get _activeVideoTrack;
|
||||
|
||||
TrackPublication? get _firstAudioPublication;
|
||||
|
||||
SnAccount? _userinfoMetadata;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.participant.addListener(onParticipantChanged);
|
||||
onParticipantChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.participant.removeListener(onParticipantChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant T oldWidget) {
|
||||
oldWidget.participant.removeListener(onParticipantChanged);
|
||||
widget.participant.addListener(onParticipantChanged);
|
||||
onParticipantChanged();
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
void onParticipantChanged() {
|
||||
setState(() {
|
||||
if (widget.participant.metadata != null) {
|
||||
_userinfoMetadata = SnAccount.fromJson(
|
||||
jsonDecode(widget.participant.metadata!),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext ctx) {
|
||||
return Stack(
|
||||
children: [
|
||||
_activeVideoTrack != null && !_activeVideoTrack!.muted
|
||||
? VideoTrackRenderer(
|
||||
_activeVideoTrack!,
|
||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
)
|
||||
: NoContentWidget(
|
||||
userinfo: _userinfoMetadata,
|
||||
isFixed: widget.isFixed,
|
||||
isSpeaking: widget.participant.isSpeaking,
|
||||
),
|
||||
if (widget.showStatsLayer)
|
||||
Positioned(
|
||||
top: 30,
|
||||
right: 30,
|
||||
child: ParticipantStatsWidget(participant: widget.participant),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ParticipantInfoWidget(
|
||||
title: widget.participant.name.isNotEmpty
|
||||
? widget.participant.name
|
||||
: widget.participant.identity,
|
||||
audioAvailable: _firstAudioPublication?.muted == false &&
|
||||
_firstAudioPublication?.subscribed == true,
|
||||
connectionQuality: widget.participant.connectionQuality,
|
||||
isScreenShare: widget.isScreenShare,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LocalParticipantWidgetState
|
||||
extends _ParticipantWidgetState<LocalParticipantWidget> {
|
||||
@override
|
||||
LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication =>
|
||||
widget.participant.audioTrackPublications.firstOrNull;
|
||||
|
||||
@override
|
||||
VideoTrack? get _activeVideoTrack => widget.videoTrack;
|
||||
}
|
||||
|
||||
class _RemoteParticipantWidgetState
|
||||
extends _ParticipantWidgetState<RemoteParticipantWidget> {
|
||||
@override
|
||||
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
|
||||
widget.participant.audioTrackPublications.firstOrNull;
|
||||
|
||||
@override
|
||||
VideoTrack? get _activeVideoTrack => widget.videoTrack;
|
||||
}
|
||||
|
||||
class InteractiveParticipantWidget extends StatelessWidget {
|
||||
final double? width;
|
||||
final double? height;
|
||||
final Color? color;
|
||||
final bool isFixedAvatar;
|
||||
final ParticipantTrack participant;
|
||||
final Function() onTap;
|
||||
|
||||
const InteractiveParticipantWidget({
|
||||
super.key,
|
||||
this.width,
|
||||
this.height,
|
||||
this.color,
|
||||
this.isFixedAvatar = false,
|
||||
required this.participant,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
child: Container(
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
child: ParticipantWidget.widgetFor(participant, isFixed: isFixedAvatar),
|
||||
),
|
||||
onTap: () => onTap(),
|
||||
onLongPress: () {
|
||||
if (participant.participant is LocalParticipant) return;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => ParticipantMenu(
|
||||
participant: participant.participant as RemoteParticipant,
|
||||
videoTrack: participant.videoTrack,
|
||||
isScreenShare: participant.isScreenShare,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class ParticipantInfoWidget extends StatelessWidget {
|
||||
final String? title;
|
||||
final bool audioAvailable;
|
||||
final ConnectionQuality connectionQuality;
|
||||
final bool isScreenShare;
|
||||
|
||||
const ParticipantInfoWidget({
|
||||
super.key,
|
||||
this.title,
|
||||
this.audioAvailable = true,
|
||||
this.connectionQuality = ConnectionQuality.unknown,
|
||||
this.isScreenShare = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 7,
|
||||
horizontal: 10,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (title != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
title!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
const Gap(5),
|
||||
isScreenShare
|
||||
? const Icon(
|
||||
Symbols.monitor,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
)
|
||||
: Icon(
|
||||
audioAvailable ? Symbols.mic : Symbols.mic_off,
|
||||
color: audioAvailable ? Colors.white : Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
const Gap(3),
|
||||
if (connectionQuality != ConnectionQuality.unknown)
|
||||
Icon(
|
||||
{
|
||||
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
|
||||
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
|
||||
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
|
||||
}[connectionQuality],
|
||||
color: {
|
||||
ConnectionQuality.excellent: Colors.green,
|
||||
ConnectionQuality.good: Colors.orange,
|
||||
ConnectionQuality.poor: Colors.red,
|
||||
}[connectionQuality],
|
||||
size: 16,
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
).padding(all: 3),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class ParticipantMenu extends StatefulWidget {
|
||||
final RemoteParticipant participant;
|
||||
final VideoTrack? videoTrack;
|
||||
final bool isScreenShare;
|
||||
final bool showStatsLayer;
|
||||
|
||||
const ParticipantMenu({
|
||||
super.key,
|
||||
required this.participant,
|
||||
this.videoTrack,
|
||||
this.isScreenShare = false,
|
||||
this.showStatsLayer = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ParticipantMenu> createState() => _ParticipantMenuState();
|
||||
}
|
||||
|
||||
class _ParticipantMenuState extends State<ParticipantMenu> {
|
||||
RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication =>
|
||||
widget.participant.videoTrackPublications
|
||||
.where((element) => element.sid == widget.videoTrack?.sid)
|
||||
.firstOrNull;
|
||||
|
||||
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
|
||||
widget.participant.audioTrackPublications.firstOrNull;
|
||||
|
||||
void tookAction() {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Text(
|
||||
'callParticipantAction',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).tr(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
if (_firstAudioPublication != null && !widget.isScreenShare)
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Symbols.volume_up,
|
||||
color: {
|
||||
TrackSubscriptionState.notAllowed:
|
||||
Theme.of(context).colorScheme.error,
|
||||
TrackSubscriptionState.unsubscribed: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
TrackSubscriptionState.subscribed:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
}[_firstAudioPublication!.subscriptionState],
|
||||
),
|
||||
title: Text(
|
||||
_firstAudioPublication!.subscribed
|
||||
? 'callParticipantMicrophoneOff'.tr()
|
||||
: 'callParticipantMicrophoneOn'.tr(),
|
||||
),
|
||||
onTap: () {
|
||||
if (_firstAudioPublication!.subscribed) {
|
||||
_firstAudioPublication!.unsubscribe();
|
||||
} else {
|
||||
_firstAudioPublication!.subscribe();
|
||||
}
|
||||
tookAction();
|
||||
},
|
||||
),
|
||||
if (_videoPublication != null)
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
widget.isScreenShare ? Symbols.monitor : Symbols.videocam,
|
||||
color: {
|
||||
TrackSubscriptionState.notAllowed:
|
||||
Theme.of(context).colorScheme.error,
|
||||
TrackSubscriptionState.unsubscribed: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
TrackSubscriptionState.subscribed:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
}[_videoPublication!.subscriptionState],
|
||||
),
|
||||
title: Text(
|
||||
_videoPublication!.subscribed
|
||||
? 'callParticipantVideoOff'.tr()
|
||||
: 'callParticipantVideoOn'.tr(),
|
||||
),
|
||||
onTap: () {
|
||||
if (_videoPublication!.subscribed) {
|
||||
_videoPublication!.unsubscribe();
|
||||
} else {
|
||||
_videoPublication!.subscribe();
|
||||
}
|
||||
tookAction();
|
||||
},
|
||||
),
|
||||
if (_videoPublication != null) const Divider(thickness: 0.3),
|
||||
if (_videoPublication != null)
|
||||
...[30, 15, 8].map(
|
||||
(x) => ListTile(
|
||||
leading: Icon(
|
||||
_videoPublication?.fps == x
|
||||
? Symbols.check_box
|
||||
: Symbols.check_box_outline_blank,
|
||||
),
|
||||
title: Text('Set preferred frame-per-second to $x'),
|
||||
onTap: () {
|
||||
_videoPublication!.setVideoFPS(x);
|
||||
tookAction();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_videoPublication != null) const Divider(thickness: 0.3),
|
||||
if (_videoPublication != null)
|
||||
...[
|
||||
('High', VideoQuality.HIGH),
|
||||
('Medium', VideoQuality.MEDIUM),
|
||||
('Low', VideoQuality.LOW),
|
||||
].map(
|
||||
(x) => ListTile(
|
||||
leading: Icon(
|
||||
_videoPublication?.videoQuality == x.$2
|
||||
? Symbols.check_box
|
||||
: Symbols.check_box_outline_blank,
|
||||
),
|
||||
title: Text('Set preferred quality to ${x.$1}'),
|
||||
onTap: () {
|
||||
_videoPublication!.setVideoQuality(x.$2);
|
||||
tookAction();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
|
||||
class ParticipantStatsWidget extends StatefulWidget {
|
||||
const ParticipantStatsWidget({super.key, required this.participant});
|
||||
|
||||
final Participant participant;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ParticipantStatsWidgetState();
|
||||
}
|
||||
|
||||
class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
|
||||
List<EventsListener<TrackEvent>> listeners = [];
|
||||
ParticipantStatsType statsType = ParticipantStatsType.unknown;
|
||||
Map<String, String> stats = {};
|
||||
|
||||
void _setUpListener(Track track) {
|
||||
var listener = track.createListener();
|
||||
listeners.add(listener);
|
||||
if (track is LocalVideoTrack) {
|
||||
statsType = ParticipantStatsType.localVideoSender;
|
||||
listener.on<VideoSenderStatsEvent>((event) {
|
||||
setState(() {
|
||||
stats['video tx'] = 'total sent ${event.currentBitrate.toInt()} kpbs';
|
||||
event.stats.forEach((key, value) {
|
||||
stats['layer-$key'] =
|
||||
'${value.frameWidth ?? 0}x${value.frameHeight ?? 0} ${value.framesPerSecond?.toDouble() ?? 0} fps, ${event.bitrateForLayers[key] ?? 0} kbps';
|
||||
});
|
||||
var firstStats =
|
||||
event.stats['f'] ?? event.stats['h'] ?? event.stats['q'];
|
||||
if (firstStats != null) {
|
||||
stats['encoder'] = firstStats.encoderImplementation ?? '';
|
||||
stats['video codec'] =
|
||||
'${firstStats.mimeType}, ${firstStats.clockRate}hz, pt: ${firstStats.payloadType}';
|
||||
stats['qualityLimitationReason'] =
|
||||
firstStats.qualityLimitationReason ?? '';
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (track is RemoteVideoTrack) {
|
||||
statsType = ParticipantStatsType.remoteVideoReceiver;
|
||||
listener.on<VideoReceiverStatsEvent>((event) {
|
||||
setState(() {
|
||||
stats['video rx'] = '${event.currentBitrate.toInt()} kpbs';
|
||||
stats['video codec'] =
|
||||
'${event.stats.mimeType}, ${event.stats.clockRate}hz, pt: ${event.stats.payloadType}';
|
||||
stats['video size'] =
|
||||
'${event.stats.frameWidth}x${event.stats.frameHeight} ${event.stats.framesPerSecond?.toDouble()}fps';
|
||||
stats['video jitter'] = '${event.stats.jitter} s';
|
||||
stats['video decoder'] = '${event.stats.decoderImplementation}';
|
||||
stats['video packets lost'] = '${event.stats.packetsLost}';
|
||||
stats['video packets received'] = '${event.stats.packetsReceived}';
|
||||
stats['video frames received'] = '${event.stats.framesReceived}';
|
||||
stats['video frames decoded'] = '${event.stats.framesDecoded}';
|
||||
stats['video frames dropped'] = '${event.stats.framesDropped}';
|
||||
});
|
||||
});
|
||||
} else if (track is LocalAudioTrack) {
|
||||
statsType = ParticipantStatsType.localAudioSender;
|
||||
listener.on<AudioSenderStatsEvent>((event) {
|
||||
setState(() {
|
||||
stats['audio tx'] = '${event.currentBitrate.toInt()} kpbs';
|
||||
stats['audio codec'] =
|
||||
'${event.stats.mimeType}, ${event.stats.clockRate}hz, ${event.stats.channels}ch, pt: ${event.stats.payloadType}';
|
||||
});
|
||||
});
|
||||
} else if (track is RemoteAudioTrack) {
|
||||
statsType = ParticipantStatsType.remoteAudioReceiver;
|
||||
listener.on<AudioReceiverStatsEvent>((event) {
|
||||
setState(() {
|
||||
stats['audio rx'] = '${event.currentBitrate.toInt()} kpbs';
|
||||
stats['audio codec'] =
|
||||
'${event.stats.mimeType}, ${event.stats.clockRate}hz, ${event.stats.channels}ch, pt: ${event.stats.payloadType}';
|
||||
stats['audio jitter'] = '${event.stats.jitter} s';
|
||||
stats['audio concealed samples'] =
|
||||
'${event.stats.concealedSamples} / ${event.stats.concealmentEvents}';
|
||||
stats['audio packets lost'] = '${event.stats.packetsLost}';
|
||||
stats['audio packets received'] = '${event.stats.packetsReceived}';
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onParticipantChanged() {
|
||||
for (var element in listeners) {
|
||||
element.dispose();
|
||||
}
|
||||
listeners.clear();
|
||||
for (var track in [
|
||||
...widget.participant.videoTrackPublications,
|
||||
...widget.participant.audioTrackPublications
|
||||
]) {
|
||||
if (track.track != null) {
|
||||
_setUpListener(track.track!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.participant.addListener(onParticipantChanged);
|
||||
onParticipantChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
for (var element in listeners) {
|
||||
element.dispose();
|
||||
}
|
||||
widget.participant.removeListener(onParticipantChanged);
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
num sendBitrate = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Column(
|
||||
children:
|
||||
stats.entries.map((e) => Text('${e.key}: ${e.value}')).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,191 +0,0 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class ChatCallPrejoinPopup extends StatefulWidget {
|
||||
final SnChatCall ongoingCall;
|
||||
final SnChannel channel;
|
||||
final void Function() onJoin;
|
||||
|
||||
const ChatCallPrejoinPopup({
|
||||
super.key,
|
||||
required this.ongoingCall,
|
||||
required this.channel,
|
||||
required this.onJoin,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatCallPrejoinPopup> createState() => _ChatCallPrejoinPopupState();
|
||||
}
|
||||
|
||||
class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
|
||||
bool _isBusy = false;
|
||||
|
||||
late final ChatCallProvider _call = context.read<ChatCallProvider>();
|
||||
|
||||
void _performJoin() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
_call.setCall(widget.ongoingCall, widget.channel);
|
||||
_call.setIsBusy(true);
|
||||
|
||||
try {
|
||||
final resp = await _call.getRoomToken();
|
||||
final token = resp.$1;
|
||||
final endpoint = resp.$2;
|
||||
|
||||
_call.initRoom();
|
||||
_call.setupRoomListeners(
|
||||
onDisconnected: (reason) {
|
||||
context.showSnackbar(
|
||||
'callDisconnected'.tr(args: [reason.toString()]),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await _call.joinRoom(endpoint, token);
|
||||
widget.onJoin();
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(e);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final call = context.read<ChatCallProvider>();
|
||||
call.checkPermissions().then((_) {
|
||||
call.initHardware();
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final call = context.read<ChatCallProvider>();
|
||||
return ListenableBuilder(
|
||||
listenable: call,
|
||||
builder: (context, _) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 320),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('callMicrophone').tr(),
|
||||
Switch(
|
||||
value: call.enableAudio,
|
||||
onChanged: null,
|
||||
),
|
||||
],
|
||||
).padding(bottom: 5),
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<MediaDevice>(
|
||||
isExpanded: true,
|
||||
disabledHint: Text('callMicrophoneDisabled').tr(),
|
||||
hint: Text('callMicrophoneSelect').tr(),
|
||||
items: call.enableAudio
|
||||
? call.audioInputs
|
||||
.map(
|
||||
(item) => DropdownMenuItem<MediaDevice>(
|
||||
value: item,
|
||||
child: Text(item.label),
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
.cast<DropdownMenuItem<MediaDevice>>()
|
||||
: [],
|
||||
value: call.audioDevice,
|
||||
onChanged: (MediaDevice? value) async {
|
||||
if (value != null) {
|
||||
call.setAudioDevice(value);
|
||||
await call.changeLocalAudioTrack();
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
height: 40,
|
||||
width: 320,
|
||||
),
|
||||
),
|
||||
).padding(bottom: 25),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('callCamera').tr(),
|
||||
Switch(
|
||||
value: call.enableVideo,
|
||||
onChanged: call.setEnableVideo,
|
||||
),
|
||||
],
|
||||
).padding(bottom: 5),
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<MediaDevice>(
|
||||
isExpanded: true,
|
||||
disabledHint: Text('callCameraDisabled').tr(),
|
||||
hint: Text('callCameraSelect').tr(),
|
||||
items: call.enableVideo
|
||||
? call.videoInputs
|
||||
.map(
|
||||
(item) => DropdownMenuItem<MediaDevice>(
|
||||
value: item,
|
||||
child: Text(item.label),
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
.cast<DropdownMenuItem<MediaDevice>>()
|
||||
: [],
|
||||
value: call.videoDevice,
|
||||
onChanged: (MediaDevice? value) async {
|
||||
if (value != null) {
|
||||
call.setVideoDevice(value);
|
||||
await call.changeLocalVideoTrack();
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
height: 40,
|
||||
width: 320,
|
||||
),
|
||||
),
|
||||
).padding(bottom: 25),
|
||||
if (_isBusy)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(320, 56),
|
||||
),
|
||||
onPressed: _isBusy ? null : _performJoin,
|
||||
child: Text('callJoin').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_call
|
||||
..deactivateHardware()
|
||||
..disposeHardware();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -1,11 +1,8 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:popover/popover.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
@ -120,20 +117,9 @@ class ChatMessage extends StatelessWidget {
|
||||
),
|
||||
onTap: () {
|
||||
if (user == null) return;
|
||||
showPopover(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surface,
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
transition: PopoverTransition.other,
|
||||
bodyBuilder: (context) => SizedBox(
|
||||
width: math.min(
|
||||
400, MediaQuery.of(context).size.width - 10),
|
||||
child: AccountPopoverCard(data: user),
|
||||
),
|
||||
direction: PopoverDirection.bottom,
|
||||
arrowHeight: 5,
|
||||
arrowWidth: 15,
|
||||
arrowDxOffset: -190,
|
||||
builder: (context) => AccountPopoverCard(data: user),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user