Compare commits
No commits in common. "master" and "2.4.2+89" have entirely different histories.
2
android/.gitignore
vendored
2
android/.gitignore
vendored
@ -11,5 +11,3 @@ GeneratedPluginRegistrant.java
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
||||
app/.cxx
|
@ -10,15 +10,10 @@ 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.8'
|
||||
implementation 'androidx.compose.foundation:foundation-layout-android:1.7.6'
|
||||
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'
|
||||
@ -78,10 +73,8 @@ android {
|
||||
}
|
||||
release {
|
||||
signingConfig = signingConfigs.release
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
96
android/app/proguard-rules.pro
vendored
96
android/app/proguard-rules.pro
vendored
@ -1,96 +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>;
|
||||
}
|
||||
|
||||
-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" xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@ -9,13 +9,11 @@
|
||||
<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
Normal file
14
android/app/src/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
-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.11.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
|
||||
|
@ -10,22 +10,18 @@ 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.9.1' apply false
|
||||
id "com.android.application" version '8.7.3' apply false
|
||||
// START: FlutterFire Configuration
|
||||
id "com.google.gms.google-services" version "4.4.2" apply false
|
||||
id "com.google.firebase.crashlytics" version "3.0.3" apply false
|
||||
id "com.google.gms.google-services" version "4.3.15" apply false
|
||||
id "com.google.firebase.crashlytics" version "2.8.1" apply false
|
||||
// END: FlutterFire Configuration
|
||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||
}
|
||||
|
@ -12,9 +12,8 @@ post {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"reason": "吹哨管理条例 / 滥用吹哨功能,累积三次复核无效吹哨。处以禁用吹哨功能 30 天。",
|
||||
"type": 1,
|
||||
"perm_nodes": {"FlagPost":false},
|
||||
"reason": "侮辱 Solar Network 商标,煽动颠覆中华羊国政权,制造不实信息,传播谣言,制造恐慌,寻衅滋事。",
|
||||
"type": 0,
|
||||
"account_id": 5
|
||||
}
|
||||
}
|
||||
|
@ -11,5 +11,8 @@ post {
|
||||
}
|
||||
|
||||
body:json {
|
||||
{}
|
||||
{
|
||||
"sources": ["taiwan-pts"],
|
||||
"eager": true
|
||||
}
|
||||
}
|
||||
|
@ -543,7 +543,6 @@
|
||||
"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",
|
||||
@ -949,21 +948,5 @@
|
||||
"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."
|
||||
"splashScreenCaption": "Trying to establishing connection with HyperNet™"
|
||||
}
|
||||
|
@ -541,7 +541,6 @@
|
||||
"attachmentSaved": "已保存到相册",
|
||||
"attachmentSavedDesktop": "已保存到下载目录",
|
||||
"openInAlbum": "在相册中打开",
|
||||
"openInBrowser": "在浏览器中打开",
|
||||
"postAbuseReport": "检举帖子",
|
||||
"postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
|
||||
"abuseReport": "检举",
|
||||
@ -946,21 +945,5 @@
|
||||
"postEditedHint": "已编辑",
|
||||
"splashScreenServer": "服务器",
|
||||
"splashScreenServerName": "土豆",
|
||||
"splashScreenCaption": "正在尝试与 HyperNet™ 取得太阳链连接",
|
||||
"attachmentEditor": "附件编辑器",
|
||||
"attachmentEditorUnUploadHint": "该附件未上传,元数据编辑不可用,同时你可以裁剪本附件。",
|
||||
"attachmentEditorUploadHint": "该附件已上传。",
|
||||
"attachmentRating": "评级",
|
||||
"fieldAttachmentRating": "内容分级",
|
||||
"fieldAttachmentQuality": "质量评分",
|
||||
"attachmentReferenceLink": "引用外部附件",
|
||||
"fieldAttachmentReferenceLink": "引用连接",
|
||||
"attachmentReferenceLinkDescription": "作为附件的源文件。需要链接允许跨域访问。",
|
||||
"fieldAttachmentMimetype": "文件类型",
|
||||
"postVideoLive": "直播",
|
||||
"postVideoLiveDescription": "这是一条直播影片,允许用户自行嵌入源站。",
|
||||
"postVideoRendererWeb": "网页渲染器",
|
||||
"postVideoRendererWebDescription": "使用 WebView 渲染内容。",
|
||||
"fieldPostVideoUrl": "视频流地址",
|
||||
"fieldPostVideoUrlDescription": "视频内容的地址,可以为网页,将会使用 iFrame / WebView 渲染。"
|
||||
"splashScreenCaption": "正在尝试与 HyperNet™ 取得太阳链连接"
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '15.1'
|
||||
platform :ios, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
@ -122,11 +122,12 @@ 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
|
||||
- Giphy (2.2.12):
|
||||
- libwebp
|
||||
- GoogleAppMeasurement (11.10.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.10.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -183,26 +184,16 @@ PODS:
|
||||
- Flutter
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- jitsi_meet_flutter_sdk (11.1.1):
|
||||
- Flutter
|
||||
- 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
|
||||
- livekit_client (2.4.1):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
- livekit_noise_filter (0.0.1):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- LiveKitKrispNoiseFilter (= 0.0.7)
|
||||
- LiveKitKrispNoiseFilter (0.0.7)
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
- Flutter
|
||||
- media_kit_video (0.0.1):
|
||||
@ -268,6 +259,7 @@ PODS:
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- WebRTC-SDK (125.6422.06)
|
||||
- workmanager (0.0.1):
|
||||
- Flutter
|
||||
|
||||
@ -289,12 +281,14 @@ 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`)
|
||||
- livekit_noise_filter (from `.symlinks/plugins/livekit_noise_filter/ios`)
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
@ -323,14 +317,11 @@ SPEC REPOS:
|
||||
- FirebaseCoreInternal
|
||||
- FirebaseInstallations
|
||||
- FirebaseMessaging
|
||||
- Giphy
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- JitsiMeetSDK
|
||||
- JitsiWebRTC
|
||||
- Kingfisher
|
||||
- libwebp
|
||||
- LiveKitKrispNoiseFilter
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
@ -338,6 +329,7 @@ SPEC REPOS:
|
||||
- SDWebImage
|
||||
- sqlite3
|
||||
- SwiftyGif
|
||||
- WebRTC-SDK
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
audioplayers_darwin:
|
||||
@ -372,6 +364,8 @@ 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:
|
||||
@ -380,8 +374,10 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
in_app_review:
|
||||
:path: ".symlinks/plugins/in_app_review/ios"
|
||||
jitsi_meet_flutter_sdk:
|
||||
:path: ".symlinks/plugins/jitsi_meet_flutter_sdk/ios"
|
||||
livekit_client:
|
||||
:path: ".symlinks/plugins/livekit_client/ios"
|
||||
livekit_noise_filter:
|
||||
:path: ".symlinks/plugins/livekit_noise_filter/ios"
|
||||
media_kit_libs_ios_video:
|
||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||
media_kit_video:
|
||||
@ -441,19 +437,18 @@ SPEC CHECKSUMS:
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||
flutter_webrtc: 57f32415b8744e806f9c2a96ccdb60c6a627ba33
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
Giphy: 83628960ed04e1c3428ff1b4fb2b027f65e82f50
|
||||
GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457
|
||||
jitsi_meet_flutter_sdk: 0283a60730922d608fbad9872e07afdd5bb3578a
|
||||
JitsiMeetSDK: 4e1c269aaaed8f2cb7b0fff2d3c00f08359b170e
|
||||
JitsiWebRTC: b47805ab5668be38e7ee60e2258f49badfe8e1d0
|
||||
Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
livekit_client: 08755cabfa4da4ed455642f460cfbb39bc518070
|
||||
livekit_noise_filter: a26aeb1c1eae6db0a023fd2f6ea3ff108c3ecbb0
|
||||
LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
@ -476,8 +471,9 @@ SPEC CHECKSUMS:
|
||||
video_compress: f2133a07762889d67f0711ac831faa26f956980e
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
||||
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
|
||||
|
||||
PODFILE CHECKSUM: d278ce52a331dda323590121247d2046cd085ae7
|
||||
PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc
|
||||
|
||||
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 = 15.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
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 = 15.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
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 = 15.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2,8 +2,6 @@
|
||||
<!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,8 +219,6 @@ 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(
|
||||
@ -243,13 +241,9 @@ class PostWriteController extends ChangeNotifier {
|
||||
contentController.text = post.body['content'] ?? '';
|
||||
aliasController.text = post.alias ?? '';
|
||||
rewardController.text = post.body['reward']?.toString() ?? '';
|
||||
if (post.body['video'] != null) {
|
||||
if (post.body['video'] is String) {
|
||||
videoUrl = post.body['video'];
|
||||
} else {
|
||||
videoAttachment = SnAttachment.fromJson(post.body['video']);
|
||||
}
|
||||
}
|
||||
videoAttachment = post.body['video'] != null
|
||||
? SnAttachment.fromJson(post.body['video'])
|
||||
: null;
|
||||
publishedAt = post.publishedAt;
|
||||
publishedUntil = post.publishedUntil;
|
||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||
@ -268,7 +262,6 @@ class PostWriteController extends ChangeNotifier {
|
||||
);
|
||||
poll = post.poll;
|
||||
|
||||
videoLive = post.body['is_live'] ?? false;
|
||||
editingDraft = post.isDraft;
|
||||
|
||||
if (post.body['thumbnail'] != null) {
|
||||
@ -452,9 +445,8 @@ 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>());
|
||||
@ -463,12 +455,10 @@ 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 =
|
||||
@ -605,11 +595,9 @@ 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 || videoUrl.isNotEmpty)
|
||||
'video': videoUrl.isNotEmpty ? videoUrl : videoAttachment!.rid,
|
||||
if (videoAttachment != null) 'video': 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) {
|
||||
@ -750,16 +738,6 @@ 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();
|
||||
|
@ -26,6 +26,7 @@ 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';
|
||||
@ -197,6 +198,7 @@ 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
|
||||
|
474
lib/providers/chat_call.dart
Normal file
474
lib/providers/chat_call.dart
Normal file
@ -0,0 +1,474 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:livekit_noise_filter/livekit_noise_filter.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package: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();
|
||||
final timeout = const Duration(seconds: 60);
|
||||
_room = Room(
|
||||
roomOptions: RoomOptions(
|
||||
dynacast: true,
|
||||
adaptiveStream: true,
|
||||
defaultAudioCaptureOptions: AudioCaptureOptions(
|
||||
processor: LiveKitNoiseFilter(),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
connectOptions: ConnectOptions(
|
||||
autoSubscribe: true,
|
||||
timeouts: Timeouts(
|
||||
connection: timeout,
|
||||
debounce: timeout,
|
||||
publish: timeout,
|
||||
peerConnection: timeout,
|
||||
iceRestart: timeout,
|
||||
),
|
||||
),
|
||||
);
|
||||
_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();
|
||||
}
|
||||
}
|
@ -64,6 +64,11 @@ 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',
|
||||
|
@ -120,25 +120,6 @@ 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,
|
||||
@ -330,23 +311,6 @@ 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;
|
||||
|
@ -22,6 +22,7 @@ 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';
|
||||
@ -29,7 +30,8 @@ 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/feed/feed_detail.dart';
|
||||
import 'package:surface/screens/news/news_detail.dart';
|
||||
import 'package:surface/screens/news/news_list.dart';
|
||||
import 'package:surface/screens/notification.dart';
|
||||
import 'package:surface/screens/post/post_detail.dart';
|
||||
import 'package:surface/screens/post/post_draft.dart';
|
||||
@ -124,13 +126,6 @@ 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',
|
||||
@ -269,6 +264,14 @@ 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',
|
||||
@ -320,6 +323,20 @@ 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',
|
||||
@ -390,10 +407,4 @@ final appRouter = GoRouter(
|
||||
),
|
||||
),
|
||||
],
|
||||
onException: (context, state, router) {
|
||||
if (state.error is GoException) {
|
||||
router.goNamed('/');
|
||||
}
|
||||
},
|
||||
navigatorKey: GlobalKey(),
|
||||
);
|
||||
|
@ -15,6 +15,7 @@ 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';
|
||||
@ -111,7 +112,7 @@ class AccountScreen extends StatelessWidget {
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text("screenAccount").tr(),
|
||||
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
||||
? Stack(
|
||||
@ -294,7 +295,12 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
|
||||
leading: const Icon(Symbols.login),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('authLogin');
|
||||
GoRouter.of(context).pushNamed('authLogin').then((value) {
|
||||
if (value == true && context.mounted) {
|
||||
final ua = context.read<UserProvider>();
|
||||
ua.refreshUser();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
@ -1,21 +1,19 @@
|
||||
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/userinfo.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.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});
|
||||
@ -50,8 +48,6 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
Future<void> _fetchAttachments() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
const uuid = Uuid();
|
||||
|
||||
try {
|
||||
@ -59,11 +55,10 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
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()));
|
||||
|
||||
@ -102,14 +97,15 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAlbum').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
SliverToBoxAdapter(
|
||||
child: Card(
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
@ -129,7 +125,8 @@ 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(),
|
||||
@ -149,82 +146,47 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 8),
|
||||
).padding(horizontal: 8, top: 8),
|
||||
Expanded(
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
itemCount: _attachments.length,
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax:
|
||||
_totalCount != null && _attachments.length >= _totalCount!,
|
||||
onFetchData: _fetchAttachments,
|
||||
itemBuilder: (context, index) {
|
||||
final ele = _attachments[index];
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
),
|
||||
),
|
||||
SliverMasonryGrid.extent(
|
||||
childCount: _attachments.length,
|
||||
maxCrossAxisExtent: 320,
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisSpacing: 4,
|
||||
itemBuilder: (context, idx) {
|
||||
final attachment = _attachments[idx];
|
||||
return GestureDetector(
|
||||
child: ClipRRect(
|
||||
child: AspectRatio(
|
||||
aspectRatio: (ele.data['ratio'] ?? 1).toDouble(),
|
||||
aspectRatio: attachment.metadata['ratio']?.toDouble() ?? 1,
|
||||
child: AttachmentItem(
|
||||
data: ele,
|
||||
heroTag: _heroTags[index],
|
||||
onZoom: () {
|
||||
data: attachment,
|
||||
heroTag: _heroTags[idx],
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: [ele],
|
||||
data: [attachment],
|
||||
heroTags: [_heroTags[idx]],
|
||||
),
|
||||
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,7 +160,6 @@ 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).push(
|
||||
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CaptchaScreen(),
|
||||
),
|
||||
|
@ -1 +1,3 @@
|
||||
export 'captcha_native.dart' if (dart.library.html) 'captcha_web.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
|
||||
export 'captcha_native.dart' if (kIsWeb) 'captcha_web.dart';
|
||||
|
@ -1,4 +1,3 @@
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:html' as html;
|
||||
import 'dart:ui_web' as ui;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -33,7 +32,7 @@ class _CaptchaScreenState extends State<CaptchaScreen> {
|
||||
});
|
||||
|
||||
final iframe = html.IFrameElement()
|
||||
..src = '${context.read<ConfigProvider>().serverUrl}/captcha'
|
||||
..src = '${context.read<ConfigProvider>().serverUrl}/captcha?redirect_uri=web'
|
||||
..style.border = 'none'
|
||||
..width = '100%'
|
||||
..height = '100%';
|
||||
|
289
lib/screens/chat/call_room.dart
Normal file
289
lib/screens/chat/call_room.dart
Normal file
@ -0,0 +1,289 @@
|
||||
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 _buildMeetLayout() {
|
||||
final call = context.read<ChatCallProvider>();
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
||||
child: call.focusTrack != null
|
||||
? InteractiveParticipantWidget(
|
||||
participant: call.focusTrack!,
|
||||
)
|
||||
: 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 SizedBox(
|
||||
height: 128,
|
||||
width: 128,
|
||||
child: InteractiveParticipantWidget(
|
||||
participant: track,
|
||||
avatarSize: 32,
|
||||
onTap: () {
|
||||
if (track.participant.sid !=
|
||||
call.focusTrack?.participant.sid) {
|
||||
call.setFocusTrack(track);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListLayout() {
|
||||
final call = context.read<ChatCallProvider>();
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: math.max(0, call.participantTracks.length),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final track = call.participantTracks[index];
|
||||
return InteractiveParticipantWidget(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
isList: true,
|
||||
avatarSize: 24,
|
||||
participant: track,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@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: ResponsiveScaffold.getIsExpand(context),
|
||||
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: 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: EdgeInsets.zero,
|
||||
),
|
||||
).padding(all: 3),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: _layoutMode == 0
|
||||
? const Icon(Icons.view_list)
|
||||
: const Icon(Icons.grid_view),
|
||||
onPressed: () {
|
||||
_switchLayout();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(left: 20, right: 16),
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
switch (_layoutMode) {
|
||||
case 1:
|
||||
return _buildListLayout();
|
||||
default:
|
||||
return _buildMeetLayout();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (call.room.localParticipant != null)
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: ControlsWidget(
|
||||
call.room,
|
||||
call.room.localParticipant!,
|
||||
),
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
final call = context.read<ChatCallProvider>();
|
||||
call.disableDurationUpdater();
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void activate() {
|
||||
final call = context.read<ChatCallProvider>();
|
||||
call.enableDurationUpdater();
|
||||
super.activate();
|
||||
}
|
||||
}
|
@ -1,20 +1,19 @@
|
||||
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';
|
||||
@ -22,13 +21,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 {
|
||||
@ -52,11 +51,13 @@ 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;
|
||||
@ -138,35 +139,88 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _joinCall() async {
|
||||
if (kIsWeb || !(Platform.isIOS || Platform.isAndroid)) {
|
||||
return await _joinCallWeb();
|
||||
}
|
||||
Future<void> _fetchOngoingCall() async {
|
||||
setState(() => _isCalling = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
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,
|
||||
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),
|
||||
),
|
||||
);
|
||||
meet.join(confOpts);
|
||||
if (resp.statusCode == 200) {
|
||||
_ongoingCall = SnChatCall.fromJson(resp.data);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isCalling = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _joinCallWeb() async {
|
||||
Future<void> _makeCall() async {
|
||||
setState(() => _isCalling = true);
|
||||
|
||||
try {
|
||||
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);
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) {
|
||||
@ -194,7 +248,10 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
await _messageController.checkUpdate();
|
||||
await Future.wait([
|
||||
_messageController.checkUpdate(),
|
||||
_fetchOngoingCall(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@ -203,6 +260,23 @@ 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
|
||||
@ -226,6 +300,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final call = context.watch<ChatCallProvider>();
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
@ -249,9 +324,14 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
),
|
||||
if (_currentMember != null)
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.video_call),
|
||||
onPressed: _joinCall,
|
||||
onLongPress: _joinCallWeb,
|
||||
icon: _ongoingCall == null
|
||||
? const Icon(Symbols.call)
|
||||
: const Icon(Symbols.call_end),
|
||||
onPressed: _isCalling
|
||||
? null
|
||||
: _ongoingCall == null
|
||||
? _makeCall
|
||||
: _endCall,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.more_vert),
|
||||
@ -279,6 +359,28 @@ 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,9 +17,10 @@ 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_reader.dart';
|
||||
import 'package:surface/widgets/feed/feed_news.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';
|
||||
|
||||
@ -465,7 +466,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
final pt = context.read<SnPostContentProvider>();
|
||||
final result = await pt.getFeed(
|
||||
cursor: _feed
|
||||
.where((ele) => !['reader.feed'].contains(ele.type))
|
||||
.where((ele) => !['reader.news'].contains(ele.type))
|
||||
.lastOrNull
|
||||
?.createdAt,
|
||||
);
|
||||
@ -548,7 +549,12 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
refreshPosts();
|
||||
},
|
||||
);
|
||||
case 'reader.feed':
|
||||
case 'fediverse.post':
|
||||
return FediversePostWidget(
|
||||
data: SnFediversePost.fromJson(ele.data),
|
||||
maxWidth: 640,
|
||||
);
|
||||
case 'reader.news':
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
|
@ -518,7 +518,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
}
|
||||
|
||||
Future<void> _doCheckIn() async {
|
||||
final captchaTk = await Navigator.of(context).push(
|
||||
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CaptchaScreen(),
|
||||
),
|
||||
|
@ -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 ReaderPageScreen extends StatefulWidget {
|
||||
final String id;
|
||||
class NewsDetailScreen extends StatefulWidget {
|
||||
final String hash;
|
||||
|
||||
const ReaderPageScreen({super.key, required this.id});
|
||||
const NewsDetailScreen({super.key, required this.hash});
|
||||
|
||||
@override
|
||||
State<ReaderPageScreen> createState() => _ReaderPageScreenState();
|
||||
State<NewsDetailScreen> createState() => _NewsDetailScreenState();
|
||||
}
|
||||
|
||||
class _ReaderPageScreenState extends State<ReaderPageScreen> {
|
||||
SnSubscriptionItem? _article;
|
||||
class _NewsDetailScreenState extends State<NewsDetailScreen> {
|
||||
SnNewsArticle? _article;
|
||||
|
||||
Future<void> _fetchArticle() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/re/subscriptions/${widget.id}');
|
||||
_article = SnSubscriptionItem.fromJson(resp.data);
|
||||
final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
|
||||
_article = SnNewsArticle.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err).then((_) {
|
239
lib/screens/news/news_list.dart
Normal file
239
lib/screens/news/news_list.dart
Normal file
@ -0,0 +1,239 @@
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ 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';
|
||||
@ -15,6 +16,7 @@ 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';
|
||||
@ -23,7 +25,8 @@ 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_actions.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
@ -455,9 +458,7 @@ 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 +=
|
||||
@ -1105,7 +1106,7 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PostVideoEditor extends StatefulWidget {
|
||||
class _PostVideoEditor extends StatelessWidget {
|
||||
final PostWriteController controller;
|
||||
final Function? onTapPublisher;
|
||||
final Function? onTapRealm;
|
||||
@ -1113,14 +1114,7 @@ class _PostVideoEditor extends StatefulWidget {
|
||||
const _PostVideoEditor(
|
||||
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
|
||||
@override
|
||||
State<_PostVideoEditor> createState() => _PostVideoEditorState();
|
||||
}
|
||||
|
||||
class _PostVideoEditorState extends State<_PostVideoEditor> {
|
||||
final TextEditingController _streamUrlController = TextEditingController();
|
||||
|
||||
void _selectVideo() async {
|
||||
void _selectVideo(BuildContext context) async {
|
||||
final video = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
@ -1131,25 +1125,78 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (video == null) return;
|
||||
widget.controller.setVideoAttachment(video);
|
||||
controller.setVideoAttachment(video);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_streamUrlController.addListener(() {
|
||||
if (_streamUrlController.text.isEmpty) {
|
||||
widget.controller.setVideoUrl('');
|
||||
} else {
|
||||
widget.controller.setVideoUrl(_streamUrlController.text);
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
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 dispose() {
|
||||
_streamUrlController.dispose();
|
||||
super.dispose();
|
||||
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
|
||||
@ -1167,10 +1214,10 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
widget.onTapPublisher?.call();
|
||||
onTapPublisher?.call();
|
||||
},
|
||||
child: AccountImage(
|
||||
content: widget.controller.publisher?.avatar,
|
||||
content: controller.publisher?.avatar,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -1180,10 +1227,10 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
widget.onTapRealm?.call();
|
||||
onTapRealm?.call();
|
||||
},
|
||||
child: AccountImage(
|
||||
content: widget.controller.realm?.avatar,
|
||||
content: controller.realm?.avatar,
|
||||
fallbackWidget: const Icon(Symbols.globe, size: 20),
|
||||
radius: 14,
|
||||
),
|
||||
@ -1196,7 +1243,7 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
|
||||
children: [
|
||||
const Gap(6),
|
||||
TextField(
|
||||
controller: widget.controller.titleController,
|
||||
controller: controller.titleController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostTitle'.tr(),
|
||||
border: InputBorder.none,
|
||||
@ -1207,7 +1254,7 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: widget.controller.descriptionController,
|
||||
controller: controller.descriptionController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostDescription'.tr(),
|
||||
border: InputBorder.none,
|
||||
@ -1219,52 +1266,66 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
|
||||
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: 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);
|
||||
}
|
||||
});
|
||||
},
|
||||
onTap: controller.videoAttachment == null
|
||||
? () => _selectVideo(context)
|
||||
: null,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: widget.controller.videoAttachment == null
|
||||
child: controller.videoAttachment == null
|
||||
? Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -1280,21 +1341,13 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AttachmentItem(
|
||||
data: widget.controller.videoAttachment!,
|
||||
data: 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -303,7 +303,6 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
|
||||
),
|
||||
child: SliverAppBar(
|
||||
expandedHeight: _appBarHeight,
|
||||
leading: const PageBackButton(),
|
||||
title: _publisher == null
|
||||
? Text('loading').tr()
|
||||
: RichText(
|
||||
|
@ -30,7 +30,6 @@ 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,
|
||||
|
@ -28,7 +28,6 @@ mixin _$SnAttachment {
|
||||
String get hash;
|
||||
int get destination;
|
||||
int get refCount;
|
||||
String? get refUrl;
|
||||
int get contentRating;
|
||||
int get qualityRating;
|
||||
DateTime? get cleanedAt;
|
||||
@ -84,7 +83,6 @@ 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) ||
|
||||
@ -134,7 +132,6 @@ mixin _$SnAttachment {
|
||||
hash,
|
||||
destination,
|
||||
refCount,
|
||||
refUrl,
|
||||
contentRating,
|
||||
qualityRating,
|
||||
cleanedAt,
|
||||
@ -158,7 +155,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, 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)';
|
||||
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,7 +179,6 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
|
||||
String hash,
|
||||
int destination,
|
||||
int refCount,
|
||||
String? refUrl,
|
||||
int contentRating,
|
||||
int qualityRating,
|
||||
DateTime? cleanedAt,
|
||||
@ -235,7 +231,6 @@ 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,
|
||||
@ -309,10 +304,6 @@ 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
|
||||
@ -480,7 +471,6 @@ class _SnAttachment extends SnAttachment {
|
||||
required this.hash,
|
||||
required this.destination,
|
||||
required this.refCount,
|
||||
this.refUrl,
|
||||
this.contentRating = 0,
|
||||
this.qualityRating = 0,
|
||||
required this.cleanedAt,
|
||||
@ -534,8 +524,6 @@ class _SnAttachment extends SnAttachment {
|
||||
@override
|
||||
final int refCount;
|
||||
@override
|
||||
final String? refUrl;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int contentRating;
|
||||
@override
|
||||
@ -635,7 +623,6 @@ 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) ||
|
||||
@ -685,7 +672,6 @@ class _SnAttachment extends SnAttachment {
|
||||
hash,
|
||||
destination,
|
||||
refCount,
|
||||
refUrl,
|
||||
contentRating,
|
||||
qualityRating,
|
||||
cleanedAt,
|
||||
@ -709,7 +695,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, 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)';
|
||||
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -735,7 +721,6 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
|
||||
String hash,
|
||||
int destination,
|
||||
int refCount,
|
||||
String? refUrl,
|
||||
int contentRating,
|
||||
int qualityRating,
|
||||
DateTime? cleanedAt,
|
||||
@ -794,7 +779,6 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
||||
Object? hash = null,
|
||||
Object? destination = null,
|
||||
Object? refCount = null,
|
||||
Object? refUrl = freezed,
|
||||
Object? contentRating = null,
|
||||
Object? qualityRating = null,
|
||||
Object? cleanedAt = freezed,
|
||||
@ -868,10 +852,6 @@ 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
|
||||
|
@ -23,7 +23,6 @@ _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
|
||||
@ -76,7 +75,6 @@ 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(),
|
||||
|
@ -1,4 +1,5 @@
|
||||
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';
|
||||
@ -115,3 +116,24 @@ 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,43 +4,35 @@ part 'news.freezed.dart';
|
||||
part 'news.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class SnSubscriptionFeed with _$SnSubscriptionFeed {
|
||||
const factory SnSubscriptionFeed({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
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;
|
||||
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 SnSubscriptionFeed.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnSubscriptionFeedFromJson(json);
|
||||
factory SnNewsSource.fromJson(Map<String, dynamic> json) => _$SnNewsSourceFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SnSubscriptionItem with _$SnSubscriptionItem {
|
||||
const factory SnSubscriptionItem({
|
||||
abstract class SnNewsArticle with _$SnNewsArticle {
|
||||
const factory SnNewsArticle({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required dynamic deletedAt,
|
||||
required String thumbnail,
|
||||
required String title,
|
||||
required String description,
|
||||
required String content,
|
||||
required String url,
|
||||
required String hash,
|
||||
required int feedId,
|
||||
required SnSubscriptionFeed feed,
|
||||
required String source,
|
||||
required DateTime? publishedAt,
|
||||
}) = _SnSubscriptionItem;
|
||||
}) = _SnNewsArticle;
|
||||
|
||||
factory SnSubscriptionItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnSubscriptionItemFromJson(json);
|
||||
factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json);
|
||||
}
|
||||
|
@ -14,224 +14,149 @@ part of 'news.dart';
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
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;
|
||||
mixin _$SnNewsSource {
|
||||
String get id;
|
||||
String get label;
|
||||
String get type;
|
||||
String get source;
|
||||
int get depth;
|
||||
bool get enabled;
|
||||
|
||||
/// Create a copy of SnSubscriptionFeed
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnSubscriptionFeedCopyWith<SnSubscriptionFeed> get copyWith =>
|
||||
_$SnSubscriptionFeedCopyWithImpl<SnSubscriptionFeed>(
|
||||
this as SnSubscriptionFeed, _$identity);
|
||||
$SnNewsSourceCopyWith<SnNewsSource> get copyWith =>
|
||||
_$SnNewsSourceCopyWithImpl<SnNewsSource>(
|
||||
this as SnNewsSource, _$identity);
|
||||
|
||||
/// Serializes this SnSubscriptionFeed to a JSON map.
|
||||
/// Serializes this SnNewsSource to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnSubscriptionFeed &&
|
||||
other is SnNewsSource &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(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));
|
||||
(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));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
url,
|
||||
isEnabled,
|
||||
isFullContent,
|
||||
pullInterval,
|
||||
adapter,
|
||||
accountId,
|
||||
lastFetchedAt);
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, id, label, type, source, depth, enabled);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnSubscriptionFeed(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, url: $url, isEnabled: $isEnabled, isFullContent: $isFullContent, pullInterval: $pullInterval, adapter: $adapter, accountId: $accountId, lastFetchedAt: $lastFetchedAt)';
|
||||
return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnSubscriptionFeedCopyWith<$Res> {
|
||||
factory $SnSubscriptionFeedCopyWith(
|
||||
SnSubscriptionFeed value, $Res Function(SnSubscriptionFeed) _then) =
|
||||
_$SnSubscriptionFeedCopyWithImpl;
|
||||
abstract mixin class $SnNewsSourceCopyWith<$Res> {
|
||||
factory $SnNewsSourceCopyWith(
|
||||
SnNewsSource value, $Res Function(SnNewsSource) _then) =
|
||||
_$SnNewsSourceCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String url,
|
||||
bool isEnabled,
|
||||
bool isFullContent,
|
||||
int pullInterval,
|
||||
String adapter,
|
||||
int? accountId,
|
||||
DateTime? lastFetchedAt});
|
||||
{String id,
|
||||
String label,
|
||||
String type,
|
||||
String source,
|
||||
int depth,
|
||||
bool enabled});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnSubscriptionFeedCopyWithImpl<$Res>
|
||||
implements $SnSubscriptionFeedCopyWith<$Res> {
|
||||
_$SnSubscriptionFeedCopyWithImpl(this._self, this._then);
|
||||
class _$SnNewsSourceCopyWithImpl<$Res> implements $SnNewsSourceCopyWith<$Res> {
|
||||
_$SnNewsSourceCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnSubscriptionFeed _self;
|
||||
final $Res Function(SnSubscriptionFeed) _then;
|
||||
final SnNewsSource _self;
|
||||
final $Res Function(SnNewsSource) _then;
|
||||
|
||||
/// Create a copy of SnSubscriptionFeed
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? url = null,
|
||||
Object? isEnabled = null,
|
||||
Object? isFullContent = null,
|
||||
Object? pullInterval = null,
|
||||
Object? adapter = null,
|
||||
Object? accountId = freezed,
|
||||
Object? lastFetchedAt = freezed,
|
||||
Object? label = null,
|
||||
Object? type = null,
|
||||
Object? source = null,
|
||||
Object? depth = null,
|
||||
Object? enabled = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
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
|
||||
label: null == label
|
||||
? _self.label
|
||||
: label // 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?,
|
||||
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
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _SnSubscriptionFeed implements SnSubscriptionFeed {
|
||||
const _SnSubscriptionFeed(
|
||||
class _SnNewsSource implements SnNewsSource {
|
||||
const _SnNewsSource(
|
||||
{required this.id,
|
||||
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);
|
||||
required this.label,
|
||||
required this.type,
|
||||
required this.source,
|
||||
required this.depth,
|
||||
required this.enabled});
|
||||
factory _SnNewsSource.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnNewsSourceFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
final String id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
final String label;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
final String type;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
final String source;
|
||||
@override
|
||||
final String url;
|
||||
final int depth;
|
||||
@override
|
||||
final bool isEnabled;
|
||||
@override
|
||||
final bool isFullContent;
|
||||
@override
|
||||
final int pullInterval;
|
||||
@override
|
||||
final String adapter;
|
||||
@override
|
||||
final int? accountId;
|
||||
@override
|
||||
final DateTime? lastFetchedAt;
|
||||
final bool enabled;
|
||||
|
||||
/// Create a copy of SnSubscriptionFeed
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnSubscriptionFeedCopyWith<_SnSubscriptionFeed> get copyWith =>
|
||||
__$SnSubscriptionFeedCopyWithImpl<_SnSubscriptionFeed>(this, _$identity);
|
||||
_$SnNewsSourceCopyWith<_SnNewsSource> get copyWith =>
|
||||
__$SnNewsSourceCopyWithImpl<_SnNewsSource>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnSubscriptionFeedToJson(
|
||||
return _$SnNewsSourceToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
@ -240,185 +165,129 @@ class _SnSubscriptionFeed implements SnSubscriptionFeed {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnSubscriptionFeed &&
|
||||
other is _SnNewsSource &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(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));
|
||||
(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));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
url,
|
||||
isEnabled,
|
||||
isFullContent,
|
||||
pullInterval,
|
||||
adapter,
|
||||
accountId,
|
||||
lastFetchedAt);
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, id, label, type, source, depth, enabled);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnSubscriptionFeed(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, url: $url, isEnabled: $isEnabled, isFullContent: $isFullContent, pullInterval: $pullInterval, adapter: $adapter, accountId: $accountId, lastFetchedAt: $lastFetchedAt)';
|
||||
return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnSubscriptionFeedCopyWith<$Res>
|
||||
implements $SnSubscriptionFeedCopyWith<$Res> {
|
||||
factory _$SnSubscriptionFeedCopyWith(
|
||||
_SnSubscriptionFeed value, $Res Function(_SnSubscriptionFeed) _then) =
|
||||
__$SnSubscriptionFeedCopyWithImpl;
|
||||
abstract mixin class _$SnNewsSourceCopyWith<$Res>
|
||||
implements $SnNewsSourceCopyWith<$Res> {
|
||||
factory _$SnNewsSourceCopyWith(
|
||||
_SnNewsSource value, $Res Function(_SnNewsSource) _then) =
|
||||
__$SnNewsSourceCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String url,
|
||||
bool isEnabled,
|
||||
bool isFullContent,
|
||||
int pullInterval,
|
||||
String adapter,
|
||||
int? accountId,
|
||||
DateTime? lastFetchedAt});
|
||||
{String id,
|
||||
String label,
|
||||
String type,
|
||||
String source,
|
||||
int depth,
|
||||
bool enabled});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnSubscriptionFeedCopyWithImpl<$Res>
|
||||
implements _$SnSubscriptionFeedCopyWith<$Res> {
|
||||
__$SnSubscriptionFeedCopyWithImpl(this._self, this._then);
|
||||
class __$SnNewsSourceCopyWithImpl<$Res>
|
||||
implements _$SnNewsSourceCopyWith<$Res> {
|
||||
__$SnNewsSourceCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnSubscriptionFeed _self;
|
||||
final $Res Function(_SnSubscriptionFeed) _then;
|
||||
final _SnNewsSource _self;
|
||||
final $Res Function(_SnNewsSource) _then;
|
||||
|
||||
/// Create a copy of SnSubscriptionFeed
|
||||
/// Create a copy of SnNewsSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? id = 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,
|
||||
Object? label = null,
|
||||
Object? type = null,
|
||||
Object? source = null,
|
||||
Object? depth = null,
|
||||
Object? enabled = null,
|
||||
}) {
|
||||
return _then(_SnSubscriptionFeed(
|
||||
return _then(_SnNewsSource(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
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
|
||||
label: null == label
|
||||
? _self.label
|
||||
: label // 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?,
|
||||
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
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnSubscriptionItem {
|
||||
mixin _$SnNewsArticle {
|
||||
int get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
DateTime? get deletedAt;
|
||||
dynamic get deletedAt;
|
||||
String get thumbnail;
|
||||
String get title;
|
||||
String get description;
|
||||
String get content;
|
||||
String get url;
|
||||
String get hash;
|
||||
int get feedId;
|
||||
SnSubscriptionFeed get feed;
|
||||
String get source;
|
||||
DateTime? get publishedAt;
|
||||
|
||||
/// Create a copy of SnSubscriptionItem
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnSubscriptionItemCopyWith<SnSubscriptionItem> get copyWith =>
|
||||
_$SnSubscriptionItemCopyWithImpl<SnSubscriptionItem>(
|
||||
this as SnSubscriptionItem, _$identity);
|
||||
$SnNewsArticleCopyWith<SnNewsArticle> get copyWith =>
|
||||
_$SnNewsArticleCopyWithImpl<SnNewsArticle>(
|
||||
this as SnNewsArticle, _$identity);
|
||||
|
||||
/// Serializes this SnSubscriptionItem to a JSON map.
|
||||
/// Serializes this SnNewsArticle to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnSubscriptionItem &&
|
||||
other is SnNewsArticle &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
||||
(identical(other.thumbnail, thumbnail) ||
|
||||
other.thumbnail == thumbnail) &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
@ -427,8 +296,7 @@ mixin _$SnSubscriptionItem {
|
||||
(identical(other.content, content) || other.content == content) &&
|
||||
(identical(other.url, url) || other.url == url) &&
|
||||
(identical(other.hash, hash) || other.hash == hash) &&
|
||||
(identical(other.feedId, feedId) || other.feedId == feedId) &&
|
||||
(identical(other.feed, feed) || other.feed == feed) &&
|
||||
(identical(other.source, source) || other.source == source) &&
|
||||
(identical(other.publishedAt, publishedAt) ||
|
||||
other.publishedAt == publishedAt));
|
||||
}
|
||||
@ -440,56 +308,52 @@ mixin _$SnSubscriptionItem {
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
const DeepCollectionEquality().hash(deletedAt),
|
||||
thumbnail,
|
||||
title,
|
||||
description,
|
||||
content,
|
||||
url,
|
||||
hash,
|
||||
feedId,
|
||||
feed,
|
||||
source,
|
||||
publishedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
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)';
|
||||
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)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnSubscriptionItemCopyWith<$Res> {
|
||||
factory $SnSubscriptionItemCopyWith(
|
||||
SnSubscriptionItem value, $Res Function(SnSubscriptionItem) _then) =
|
||||
_$SnSubscriptionItemCopyWithImpl;
|
||||
abstract mixin class $SnNewsArticleCopyWith<$Res> {
|
||||
factory $SnNewsArticleCopyWith(
|
||||
SnNewsArticle value, $Res Function(SnNewsArticle) _then) =
|
||||
_$SnNewsArticleCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
dynamic deletedAt,
|
||||
String thumbnail,
|
||||
String title,
|
||||
String description,
|
||||
String content,
|
||||
String url,
|
||||
String hash,
|
||||
int feedId,
|
||||
SnSubscriptionFeed feed,
|
||||
String source,
|
||||
DateTime? publishedAt});
|
||||
|
||||
$SnSubscriptionFeedCopyWith<$Res> get feed;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnSubscriptionItemCopyWithImpl<$Res>
|
||||
implements $SnSubscriptionItemCopyWith<$Res> {
|
||||
_$SnSubscriptionItemCopyWithImpl(this._self, this._then);
|
||||
class _$SnNewsArticleCopyWithImpl<$Res>
|
||||
implements $SnNewsArticleCopyWith<$Res> {
|
||||
_$SnNewsArticleCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnSubscriptionItem _self;
|
||||
final $Res Function(SnSubscriptionItem) _then;
|
||||
final SnNewsArticle _self;
|
||||
final $Res Function(SnNewsArticle) _then;
|
||||
|
||||
/// Create a copy of SnSubscriptionItem
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
@ -504,8 +368,7 @@ class _$SnSubscriptionItemCopyWithImpl<$Res>
|
||||
Object? content = null,
|
||||
Object? url = null,
|
||||
Object? hash = null,
|
||||
Object? feedId = null,
|
||||
Object? feed = null,
|
||||
Object? source = null,
|
||||
Object? publishedAt = freezed,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
@ -524,7 +387,7 @@ class _$SnSubscriptionItemCopyWithImpl<$Res>
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
as dynamic,
|
||||
thumbnail: null == thumbnail
|
||||
? _self.thumbnail
|
||||
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||
@ -549,36 +412,22 @@ class _$SnSubscriptionItemCopyWithImpl<$Res>
|
||||
? _self.hash
|
||||
: hash // 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,
|
||||
source: null == source
|
||||
? _self.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
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 _SnSubscriptionItem implements SnSubscriptionItem {
|
||||
const _SnSubscriptionItem(
|
||||
class _SnNewsArticle implements SnNewsArticle {
|
||||
const _SnNewsArticle(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
@ -589,11 +438,10 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
|
||||
required this.content,
|
||||
required this.url,
|
||||
required this.hash,
|
||||
required this.feedId,
|
||||
required this.feed,
|
||||
required this.source,
|
||||
required this.publishedAt});
|
||||
factory _SnSubscriptionItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnSubscriptionItemFromJson(json);
|
||||
factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnNewsArticleFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@ -602,7 +450,7 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
final dynamic deletedAt;
|
||||
@override
|
||||
final String thumbnail;
|
||||
@override
|
||||
@ -616,23 +464,21 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
|
||||
@override
|
||||
final String hash;
|
||||
@override
|
||||
final int feedId;
|
||||
@override
|
||||
final SnSubscriptionFeed feed;
|
||||
final String source;
|
||||
@override
|
||||
final DateTime? publishedAt;
|
||||
|
||||
/// Create a copy of SnSubscriptionItem
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnSubscriptionItemCopyWith<_SnSubscriptionItem> get copyWith =>
|
||||
__$SnSubscriptionItemCopyWithImpl<_SnSubscriptionItem>(this, _$identity);
|
||||
_$SnNewsArticleCopyWith<_SnNewsArticle> get copyWith =>
|
||||
__$SnNewsArticleCopyWithImpl<_SnNewsArticle>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnSubscriptionItemToJson(
|
||||
return _$SnNewsArticleToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
@ -641,14 +487,13 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnSubscriptionItem &&
|
||||
other is _SnNewsArticle &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
||||
(identical(other.thumbnail, thumbnail) ||
|
||||
other.thumbnail == thumbnail) &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
@ -657,8 +502,7 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
|
||||
(identical(other.content, content) || other.content == content) &&
|
||||
(identical(other.url, url) || other.url == url) &&
|
||||
(identical(other.hash, hash) || other.hash == hash) &&
|
||||
(identical(other.feedId, feedId) || other.feedId == feedId) &&
|
||||
(identical(other.feed, feed) || other.feed == feed) &&
|
||||
(identical(other.source, source) || other.source == source) &&
|
||||
(identical(other.publishedAt, publishedAt) ||
|
||||
other.publishedAt == publishedAt));
|
||||
}
|
||||
@ -670,59 +514,54 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
const DeepCollectionEquality().hash(deletedAt),
|
||||
thumbnail,
|
||||
title,
|
||||
description,
|
||||
content,
|
||||
url,
|
||||
hash,
|
||||
feedId,
|
||||
feed,
|
||||
source,
|
||||
publishedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
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)';
|
||||
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)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnSubscriptionItemCopyWith<$Res>
|
||||
implements $SnSubscriptionItemCopyWith<$Res> {
|
||||
factory _$SnSubscriptionItemCopyWith(
|
||||
_SnSubscriptionItem value, $Res Function(_SnSubscriptionItem) _then) =
|
||||
__$SnSubscriptionItemCopyWithImpl;
|
||||
abstract mixin class _$SnNewsArticleCopyWith<$Res>
|
||||
implements $SnNewsArticleCopyWith<$Res> {
|
||||
factory _$SnNewsArticleCopyWith(
|
||||
_SnNewsArticle value, $Res Function(_SnNewsArticle) _then) =
|
||||
__$SnNewsArticleCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
dynamic deletedAt,
|
||||
String thumbnail,
|
||||
String title,
|
||||
String description,
|
||||
String content,
|
||||
String url,
|
||||
String hash,
|
||||
int feedId,
|
||||
SnSubscriptionFeed feed,
|
||||
String source,
|
||||
DateTime? publishedAt});
|
||||
|
||||
@override
|
||||
$SnSubscriptionFeedCopyWith<$Res> get feed;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnSubscriptionItemCopyWithImpl<$Res>
|
||||
implements _$SnSubscriptionItemCopyWith<$Res> {
|
||||
__$SnSubscriptionItemCopyWithImpl(this._self, this._then);
|
||||
class __$SnNewsArticleCopyWithImpl<$Res>
|
||||
implements _$SnNewsArticleCopyWith<$Res> {
|
||||
__$SnNewsArticleCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnSubscriptionItem _self;
|
||||
final $Res Function(_SnSubscriptionItem) _then;
|
||||
final _SnNewsArticle _self;
|
||||
final $Res Function(_SnNewsArticle) _then;
|
||||
|
||||
/// Create a copy of SnSubscriptionItem
|
||||
/// Create a copy of SnNewsArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
@ -737,11 +576,10 @@ class __$SnSubscriptionItemCopyWithImpl<$Res>
|
||||
Object? content = null,
|
||||
Object? url = null,
|
||||
Object? hash = null,
|
||||
Object? feedId = null,
|
||||
Object? feed = null,
|
||||
Object? source = null,
|
||||
Object? publishedAt = freezed,
|
||||
}) {
|
||||
return _then(_SnSubscriptionItem(
|
||||
return _then(_SnNewsArticle(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
@ -757,7 +595,7 @@ class __$SnSubscriptionItemCopyWithImpl<$Res>
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
as dynamic,
|
||||
thumbnail: null == thumbnail
|
||||
? _self.thumbnail
|
||||
: thumbnail // ignore: cast_nullable_to_non_nullable
|
||||
@ -782,30 +620,16 @@ class __$SnSubscriptionItemCopyWithImpl<$Res>
|
||||
? _self.hash
|
||||
: hash // 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,
|
||||
source: null == source
|
||||
? _self.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
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,74 +6,56 @@ part of 'news.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_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'] == 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),
|
||||
_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> _$SnSubscriptionFeedToJson(_SnSubscriptionFeed instance) =>
|
||||
Map<String, dynamic> _$SnNewsSourceToJson(_SnNewsSource 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(),
|
||||
'label': instance.label,
|
||||
'type': instance.type,
|
||||
'source': instance.source,
|
||||
'depth': instance.depth,
|
||||
'enabled': instance.enabled,
|
||||
};
|
||||
|
||||
_SnSubscriptionItem _$SnSubscriptionItemFromJson(Map<String, dynamic> json) =>
|
||||
_SnSubscriptionItem(
|
||||
_SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) =>
|
||||
_SnNewsArticle(
|
||||
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),
|
||||
deletedAt: json['deleted_at'],
|
||||
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,
|
||||
feedId: (json['feed_id'] as num).toInt(),
|
||||
feed: SnSubscriptionFeed.fromJson(json['feed'] as Map<String, dynamic>),
|
||||
source: json['source'] as String,
|
||||
publishedAt: json['published_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['published_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnSubscriptionItemToJson(_SnSubscriptionItem instance) =>
|
||||
Map<String, dynamic> _$SnNewsArticleToJson(_SnNewsArticle instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt,
|
||||
'thumbnail': instance.thumbnail,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'content': instance.content,
|
||||
'url': instance.url,
|
||||
'hash': instance.hash,
|
||||
'feed_id': instance.feedId,
|
||||
'feed': instance.feed.toJson(),
|
||||
'source': instance.source,
|
||||
'published_at': instance.publishedAt?.toIso8601String(),
|
||||
};
|
||||
|
@ -181,3 +181,41 @@ 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);
|
||||
}
|
||||
|
@ -3400,4 +3400,698 @@ class __$SnFeedEntryCopyWithImpl<$Res> implements _$SnFeedEntryCopyWith<$Res> {
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnFediversePost {
|
||||
int get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
DateTime? get deletedAt;
|
||||
String get identifier;
|
||||
String get origin;
|
||||
String get content;
|
||||
String get language;
|
||||
List<String> get images;
|
||||
SnFediverseUser get user;
|
||||
int get userId;
|
||||
|
||||
/// Create a copy of SnFediversePost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFediversePostCopyWith<SnFediversePost> get copyWith =>
|
||||
_$SnFediversePostCopyWithImpl<SnFediversePost>(
|
||||
this as SnFediversePost, _$identity);
|
||||
|
||||
/// Serializes this SnFediversePost to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnFediversePost &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.identifier, identifier) ||
|
||||
other.identifier == identifier) &&
|
||||
(identical(other.origin, origin) || other.origin == origin) &&
|
||||
(identical(other.content, content) || other.content == content) &&
|
||||
(identical(other.language, language) ||
|
||||
other.language == language) &&
|
||||
const DeepCollectionEquality().equals(other.images, images) &&
|
||||
(identical(other.user, user) || other.user == user) &&
|
||||
(identical(other.userId, userId) || other.userId == userId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
identifier,
|
||||
origin,
|
||||
content,
|
||||
language,
|
||||
const DeepCollectionEquality().hash(images),
|
||||
user,
|
||||
userId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFediversePost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, identifier: $identifier, origin: $origin, content: $content, language: $language, images: $images, user: $user, userId: $userId)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnFediversePostCopyWith<$Res> {
|
||||
factory $SnFediversePostCopyWith(
|
||||
SnFediversePost value, $Res Function(SnFediversePost) _then) =
|
||||
_$SnFediversePostCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String identifier,
|
||||
String origin,
|
||||
String content,
|
||||
String language,
|
||||
List<String> images,
|
||||
SnFediverseUser user,
|
||||
int userId});
|
||||
|
||||
$SnFediverseUserCopyWith<$Res> get user;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnFediversePostCopyWithImpl<$Res>
|
||||
implements $SnFediversePostCopyWith<$Res> {
|
||||
_$SnFediversePostCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnFediversePost _self;
|
||||
final $Res Function(SnFediversePost) _then;
|
||||
|
||||
/// Create a copy of SnFediversePost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? identifier = null,
|
||||
Object? origin = null,
|
||||
Object? content = null,
|
||||
Object? language = null,
|
||||
Object? images = null,
|
||||
Object? user = null,
|
||||
Object? userId = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
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?,
|
||||
identifier: null == identifier
|
||||
? _self.identifier
|
||||
: identifier // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
origin: null == origin
|
||||
? _self.origin
|
||||
: origin // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
content: null == content
|
||||
? _self.content
|
||||
: content // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
language: null == language
|
||||
? _self.language
|
||||
: language // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
images: null == images
|
||||
? _self.images
|
||||
: images // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
user: null == user
|
||||
? _self.user
|
||||
: user // ignore: cast_nullable_to_non_nullable
|
||||
as SnFediverseUser,
|
||||
userId: null == userId
|
||||
? _self.userId
|
||||
: userId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnFediversePost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFediverseUserCopyWith<$Res> get user {
|
||||
return $SnFediverseUserCopyWith<$Res>(_self.user, (value) {
|
||||
return _then(_self.copyWith(user: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _SnFediversePost implements SnFediversePost {
|
||||
const _SnFediversePost(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.identifier,
|
||||
required this.origin,
|
||||
required this.content,
|
||||
required this.language,
|
||||
required final List<String> images,
|
||||
required this.user,
|
||||
required this.userId})
|
||||
: _images = images;
|
||||
factory _SnFediversePost.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnFediversePostFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final String identifier;
|
||||
@override
|
||||
final String origin;
|
||||
@override
|
||||
final String content;
|
||||
@override
|
||||
final String language;
|
||||
final List<String> _images;
|
||||
@override
|
||||
List<String> get images {
|
||||
if (_images is EqualUnmodifiableListView) return _images;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_images);
|
||||
}
|
||||
|
||||
@override
|
||||
final SnFediverseUser user;
|
||||
@override
|
||||
final int userId;
|
||||
|
||||
/// Create a copy of SnFediversePost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnFediversePostCopyWith<_SnFediversePost> get copyWith =>
|
||||
__$SnFediversePostCopyWithImpl<_SnFediversePost>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnFediversePostToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnFediversePost &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.identifier, identifier) ||
|
||||
other.identifier == identifier) &&
|
||||
(identical(other.origin, origin) || other.origin == origin) &&
|
||||
(identical(other.content, content) || other.content == content) &&
|
||||
(identical(other.language, language) ||
|
||||
other.language == language) &&
|
||||
const DeepCollectionEquality().equals(other._images, _images) &&
|
||||
(identical(other.user, user) || other.user == user) &&
|
||||
(identical(other.userId, userId) || other.userId == userId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
identifier,
|
||||
origin,
|
||||
content,
|
||||
language,
|
||||
const DeepCollectionEquality().hash(_images),
|
||||
user,
|
||||
userId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFediversePost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, identifier: $identifier, origin: $origin, content: $content, language: $language, images: $images, user: $user, userId: $userId)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnFediversePostCopyWith<$Res>
|
||||
implements $SnFediversePostCopyWith<$Res> {
|
||||
factory _$SnFediversePostCopyWith(
|
||||
_SnFediversePost value, $Res Function(_SnFediversePost) _then) =
|
||||
__$SnFediversePostCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String identifier,
|
||||
String origin,
|
||||
String content,
|
||||
String language,
|
||||
List<String> images,
|
||||
SnFediverseUser user,
|
||||
int userId});
|
||||
|
||||
@override
|
||||
$SnFediverseUserCopyWith<$Res> get user;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnFediversePostCopyWithImpl<$Res>
|
||||
implements _$SnFediversePostCopyWith<$Res> {
|
||||
__$SnFediversePostCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnFediversePost _self;
|
||||
final $Res Function(_SnFediversePost) _then;
|
||||
|
||||
/// Create a copy of SnFediversePost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? identifier = null,
|
||||
Object? origin = null,
|
||||
Object? content = null,
|
||||
Object? language = null,
|
||||
Object? images = null,
|
||||
Object? user = null,
|
||||
Object? userId = null,
|
||||
}) {
|
||||
return _then(_SnFediversePost(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
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?,
|
||||
identifier: null == identifier
|
||||
? _self.identifier
|
||||
: identifier // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
origin: null == origin
|
||||
? _self.origin
|
||||
: origin // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
content: null == content
|
||||
? _self.content
|
||||
: content // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
language: null == language
|
||||
? _self.language
|
||||
: language // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
images: null == images
|
||||
? _self._images
|
||||
: images // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
user: null == user
|
||||
? _self.user
|
||||
: user // ignore: cast_nullable_to_non_nullable
|
||||
as SnFediverseUser,
|
||||
userId: null == userId
|
||||
? _self.userId
|
||||
: userId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnFediversePost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFediverseUserCopyWith<$Res> get user {
|
||||
return $SnFediverseUserCopyWith<$Res>(_self.user, (value) {
|
||||
return _then(_self.copyWith(user: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnFediverseUser {
|
||||
int get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
DateTime? get deletedAt;
|
||||
String get identifier;
|
||||
String get origin;
|
||||
String get avatar;
|
||||
String get name;
|
||||
String get nick;
|
||||
|
||||
/// Create a copy of SnFediverseUser
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFediverseUserCopyWith<SnFediverseUser> get copyWith =>
|
||||
_$SnFediverseUserCopyWithImpl<SnFediverseUser>(
|
||||
this as SnFediverseUser, _$identity);
|
||||
|
||||
/// Serializes this SnFediverseUser to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnFediverseUser &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.identifier, identifier) ||
|
||||
other.identifier == identifier) &&
|
||||
(identical(other.origin, origin) || other.origin == origin) &&
|
||||
(identical(other.avatar, avatar) || other.avatar == avatar) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.nick, nick) || other.nick == nick));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||
deletedAt, identifier, origin, avatar, name, nick);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFediverseUser(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, identifier: $identifier, origin: $origin, avatar: $avatar, name: $name, nick: $nick)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnFediverseUserCopyWith<$Res> {
|
||||
factory $SnFediverseUserCopyWith(
|
||||
SnFediverseUser value, $Res Function(SnFediverseUser) _then) =
|
||||
_$SnFediverseUserCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String identifier,
|
||||
String origin,
|
||||
String avatar,
|
||||
String name,
|
||||
String nick});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnFediverseUserCopyWithImpl<$Res>
|
||||
implements $SnFediverseUserCopyWith<$Res> {
|
||||
_$SnFediverseUserCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnFediverseUser _self;
|
||||
final $Res Function(SnFediverseUser) _then;
|
||||
|
||||
/// Create a copy of SnFediverseUser
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? identifier = null,
|
||||
Object? origin = null,
|
||||
Object? avatar = null,
|
||||
Object? name = null,
|
||||
Object? nick = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
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?,
|
||||
identifier: null == identifier
|
||||
? _self.identifier
|
||||
: identifier // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
origin: null == origin
|
||||
? _self.origin
|
||||
: origin // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
avatar: null == avatar
|
||||
? _self.avatar
|
||||
: avatar // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _self.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
nick: null == nick
|
||||
? _self.nick
|
||||
: nick // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _SnFediverseUser implements SnFediverseUser {
|
||||
const _SnFediverseUser(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.identifier,
|
||||
required this.origin,
|
||||
required this.avatar,
|
||||
required this.name,
|
||||
required this.nick});
|
||||
factory _SnFediverseUser.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnFediverseUserFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final String identifier;
|
||||
@override
|
||||
final String origin;
|
||||
@override
|
||||
final String avatar;
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final String nick;
|
||||
|
||||
/// Create a copy of SnFediverseUser
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnFediverseUserCopyWith<_SnFediverseUser> get copyWith =>
|
||||
__$SnFediverseUserCopyWithImpl<_SnFediverseUser>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnFediverseUserToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnFediverseUser &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.identifier, identifier) ||
|
||||
other.identifier == identifier) &&
|
||||
(identical(other.origin, origin) || other.origin == origin) &&
|
||||
(identical(other.avatar, avatar) || other.avatar == avatar) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.nick, nick) || other.nick == nick));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||
deletedAt, identifier, origin, avatar, name, nick);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFediverseUser(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, identifier: $identifier, origin: $origin, avatar: $avatar, name: $name, nick: $nick)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnFediverseUserCopyWith<$Res>
|
||||
implements $SnFediverseUserCopyWith<$Res> {
|
||||
factory _$SnFediverseUserCopyWith(
|
||||
_SnFediverseUser value, $Res Function(_SnFediverseUser) _then) =
|
||||
__$SnFediverseUserCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String identifier,
|
||||
String origin,
|
||||
String avatar,
|
||||
String name,
|
||||
String nick});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnFediverseUserCopyWithImpl<$Res>
|
||||
implements _$SnFediverseUserCopyWith<$Res> {
|
||||
__$SnFediverseUserCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnFediverseUser _self;
|
||||
final $Res Function(_SnFediverseUser) _then;
|
||||
|
||||
/// Create a copy of SnFediverseUser
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? identifier = null,
|
||||
Object? origin = null,
|
||||
Object? avatar = null,
|
||||
Object? name = null,
|
||||
Object? nick = null,
|
||||
}) {
|
||||
return _then(_SnFediverseUser(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
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?,
|
||||
identifier: null == identifier
|
||||
? _self.identifier
|
||||
: identifier // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
origin: null == origin
|
||||
? _self.origin
|
||||
: origin // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
avatar: null == avatar
|
||||
? _self.avatar
|
||||
: avatar // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _self.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
nick: null == nick
|
||||
? _self.nick
|
||||
: nick // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
@ -303,3 +303,64 @@ 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,
|
||||
};
|
||||
|
@ -12,9 +12,6 @@ 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;
|
||||
|
||||
@ -24,9 +21,6 @@ class AttachmentInputDialog extends StatefulWidget {
|
||||
required this.pool,
|
||||
this.analyzeNow = false,
|
||||
this.mediaType = SnMediaType.image,
|
||||
this.canPickMedia = true,
|
||||
this.canReferenceLink = true,
|
||||
this.canRandomId = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -35,8 +29,6 @@ class AttachmentInputDialog extends StatefulWidget {
|
||||
|
||||
class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
final _randomIdController = TextEditingController();
|
||||
final _referenceLinkController = TextEditingController();
|
||||
final _referenceMimetypeController = TextEditingController();
|
||||
|
||||
XFile? _file;
|
||||
double? _progress;
|
||||
@ -69,26 +61,9 @@ 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!,
|
||||
@ -114,15 +89,8 @@ 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),
|
||||
@ -130,73 +98,22 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
controller: _randomIdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldAttachmentRandomId'.tr(),
|
||||
border: const OutlineInputBorder(),
|
||||
border: const UnderlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
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: [
|
||||
const Gap(24),
|
||||
Text('attachmentInputNew').tr().fontSize(14),
|
||||
Card(
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 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();
|
||||
},
|
||||
@ -204,8 +121,6 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isBusy)
|
||||
LinearProgressIndicator(
|
||||
value: _progress,
|
||||
|
@ -15,7 +15,6 @@ 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';
|
||||
@ -229,7 +228,6 @@ 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);
|
||||
@ -242,7 +240,6 @@ 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) {
|
||||
@ -502,7 +499,6 @@ 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();
|
||||
|
||||
@ -512,7 +508,6 @@ 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) {
|
||||
|
@ -373,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((_) {
|
||||
@ -403,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((_) {
|
||||
@ -416,10 +416,10 @@ 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) {
|
||||
|
@ -1,336 +0,0 @@
|
||||
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,12 +10,10 @@ 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
|
||||
@ -65,7 +63,7 @@ class _PendingAttachmentAltDialogState
|
||||
controller: _contentController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldAttachmentAlt'.tr(),
|
||||
border: const OutlineInputBorder(),
|
||||
border: const UnderlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
@ -73,9 +71,7 @@ class _PendingAttachmentAltDialogState
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () {
|
||||
onPressed: _isBusy ? null : () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('dialogDismiss'.tr()),
|
||||
|
@ -14,12 +14,10 @@ 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;
|
||||
|
||||
@ -86,23 +84,17 @@ class _PendingAttachmentBoostDialogState
|
||||
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);
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -113,9 +105,7 @@ class _PendingAttachmentBoostDialogState
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () {
|
||||
onPressed: _isBusy ? null : () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('dialogDismiss'.tr()),
|
||||
|
@ -1,108 +0,0 @@
|
||||
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()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
369
lib/widgets/chat/call/call_controls.dart
Normal file
369
lib/widgets/chat/call/call_controls.dart
Normal file
@ -0,0 +1,369 @@
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
86
lib/widgets/chat/call/call_no_content.dart
Normal file
86
lib/widgets/chat/call/call_no_content.dart
Normal file
@ -0,0 +1,86 @@
|
||||
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 double? avatarSize;
|
||||
|
||||
const NoContentWidget({
|
||||
super.key,
|
||||
this.userinfo,
|
||||
this.avatarSize,
|
||||
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.avatarSize ??
|
||||
math.min(
|
||||
MediaQuery.of(context).size.width * 0.1,
|
||||
MediaQuery.of(context).size.height * 0.1,
|
||||
);
|
||||
|
||||
return 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();
|
||||
}
|
||||
}
|
337
lib/widgets/chat/call/call_participant.dart
Normal file
337
lib/widgets/chat/call/call_participant.dart
Normal file
@ -0,0 +1,337 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/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, {
|
||||
double? avatarSize,
|
||||
EdgeInsets? padding,
|
||||
bool showStatsLayer = false,
|
||||
bool isList = false,
|
||||
}) {
|
||||
if (participantTrack.participant is LocalParticipant) {
|
||||
return LocalParticipantWidget(
|
||||
participantTrack.participant as LocalParticipant,
|
||||
participantTrack.videoTrack,
|
||||
avatarSize,
|
||||
participantTrack.isScreenShare,
|
||||
showStatsLayer,
|
||||
isList,
|
||||
padding,
|
||||
);
|
||||
} else if (participantTrack.participant is RemoteParticipant) {
|
||||
return RemoteParticipantWidget(
|
||||
participantTrack.participant as RemoteParticipant,
|
||||
participantTrack.videoTrack,
|
||||
avatarSize,
|
||||
participantTrack.isScreenShare,
|
||||
showStatsLayer,
|
||||
isList,
|
||||
padding,
|
||||
);
|
||||
}
|
||||
throw UnimplementedError('Unknown participant type');
|
||||
}
|
||||
|
||||
abstract final Participant participant;
|
||||
abstract final VideoTrack? videoTrack;
|
||||
abstract final bool isScreenShare;
|
||||
abstract final double? avatarSize;
|
||||
abstract final bool showStatsLayer;
|
||||
abstract final bool isList;
|
||||
abstract final EdgeInsets? padding;
|
||||
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 double? avatarSize;
|
||||
@override
|
||||
final bool isScreenShare;
|
||||
@override
|
||||
final bool showStatsLayer;
|
||||
@override
|
||||
final bool isList;
|
||||
@override
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const LocalParticipantWidget(
|
||||
this.participant,
|
||||
this.videoTrack,
|
||||
this.avatarSize,
|
||||
this.isScreenShare,
|
||||
this.showStatsLayer,
|
||||
this.isList,
|
||||
this.padding, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _LocalParticipantWidgetState();
|
||||
}
|
||||
|
||||
class RemoteParticipantWidget extends ParticipantWidget {
|
||||
@override
|
||||
final RemoteParticipant participant;
|
||||
@override
|
||||
final VideoTrack? videoTrack;
|
||||
@override
|
||||
final double? avatarSize;
|
||||
@override
|
||||
final bool isScreenShare;
|
||||
@override
|
||||
final bool showStatsLayer;
|
||||
@override
|
||||
final bool isList;
|
||||
@override
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const RemoteParticipantWidget(
|
||||
this.participant,
|
||||
this.videoTrack,
|
||||
this.avatarSize,
|
||||
this.isScreenShare,
|
||||
this.showStatsLayer,
|
||||
this.isList,
|
||||
this.padding, {
|
||||
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 context) {
|
||||
if (widget.isList) {
|
||||
return Padding(
|
||||
padding: widget.padding ?? EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: (widget.avatarSize ?? 32) * 2,
|
||||
height: (widget.avatarSize ?? 32) * 2,
|
||||
child: Center(
|
||||
child: NoContentWidget(
|
||||
userinfo: _userinfoMetadata,
|
||||
avatarSize: widget.avatarSize,
|
||||
isSpeaking: widget.participant.isSpeaking,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: (widget.avatarSize ?? 32) * 2,
|
||||
child: ParticipantInfoWidget(
|
||||
isList: true,
|
||||
title: widget.participant.name.isNotEmpty
|
||||
? widget.participant.name
|
||||
: widget.participant.identity,
|
||||
audioAvailable: _firstAudioPublication?.muted == false &&
|
||||
_firstAudioPublication?.subscribed == true,
|
||||
connectionQuality: widget.participant.connectionQuality,
|
||||
isScreenShare: widget.isScreenShare,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_activeVideoTrack != null && !_activeVideoTrack!.muted)
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Material(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer
|
||||
.withOpacity(0.75),
|
||||
child: VideoTrackRenderer(
|
||||
_activeVideoTrack!,
|
||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
),
|
||||
),
|
||||
).padding(top: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
if (_activeVideoTrack != null && !_activeVideoTrack!.muted)
|
||||
VideoTrackRenderer(
|
||||
_activeVideoTrack!,
|
||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
)
|
||||
else
|
||||
Center(
|
||||
child: NoContentWidget(
|
||||
userinfo: _userinfoMetadata,
|
||||
avatarSize: widget.avatarSize,
|
||||
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? avatarSize;
|
||||
final bool isList;
|
||||
final ParticipantTrack participant;
|
||||
final Function? onTap;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const InteractiveParticipantWidget({
|
||||
super.key,
|
||||
this.avatarSize,
|
||||
this.isList = false,
|
||||
this.padding,
|
||||
required this.participant,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap != null
|
||||
? () {
|
||||
onTap?.call();
|
||||
}
|
||||
: null,
|
||||
onLongPress: () {
|
||||
if (participant.participant is LocalParticipant) return;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => ParticipantMenu(
|
||||
participant: participant.participant as RemoteParticipant,
|
||||
videoTrack: participant.videoTrack,
|
||||
isScreenShare: participant.isScreenShare,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
child: ParticipantWidget.widgetFor(
|
||||
participant,
|
||||
avatarSize: avatarSize,
|
||||
isList: isList,
|
||||
padding: padding,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
140
lib/widgets/chat/call/call_participant_info.dart
Normal file
140
lib/widgets/chat/call/call_participant_info.dart
Normal file
@ -0,0 +1,140 @@
|
||||
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;
|
||||
final bool isList;
|
||||
|
||||
const ParticipantInfoWidget({
|
||||
super.key,
|
||||
this.title,
|
||||
this.audioAvailable = true,
|
||||
this.connectionQuality = ConnectionQuality.unknown,
|
||||
this.isScreenShare = false,
|
||||
this.isList = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isList) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null)
|
||||
Text(
|
||||
title!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).padding(left: 2),
|
||||
Row(
|
||||
children: [
|
||||
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),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 7,
|
||||
horizontal: 10,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (title != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
title!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
const Gap(5),
|
||||
isScreenShare
|
||||
? const Icon(
|
||||
Symbols.monitor,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
)
|
||||
: Icon(
|
||||
audioAvailable ? Symbols.mic : Symbols.mic_off,
|
||||
color: audioAvailable ? Colors.white : Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
const Gap(3),
|
||||
if (connectionQuality != ConnectionQuality.unknown)
|
||||
Icon(
|
||||
{
|
||||
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
|
||||
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
|
||||
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
|
||||
}[connectionQuality],
|
||||
color: {
|
||||
ConnectionQuality.excellent: Colors.green,
|
||||
ConnectionQuality.good: Colors.orange,
|
||||
ConnectionQuality.poor: Colors.red,
|
||||
}[connectionQuality],
|
||||
size: 16,
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
).padding(all: 3),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
161
lib/widgets/chat/call/call_participant_menu.dart
Normal file
161
lib/widgets/chat/call/call_participant_menu.dart
Normal file
@ -0,0 +1,161 @@
|
||||
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();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
133
lib/widgets/chat/call/call_participant_stats.dart
Normal file
133
lib/widgets/chat/call/call_participant_stats.dart
Normal file
@ -0,0 +1,133 @@
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
191
lib/widgets/chat/call/call_prejoin.dart
Normal file
191
lib/widgets/chat/call/call_prejoin.dart
Normal file
@ -0,0 +1,191 @@
|
||||
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();
|
||||
}
|
||||
}
|
105
lib/widgets/feed/feed_news.dart
Normal file
105
lib/widgets/feed/feed_news.dart
Normal file
@ -0,0 +1,105 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/types/news.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
|
||||
class NewsFeedEntry extends StatelessWidget {
|
||||
final SnFeedEntry data;
|
||||
const NewsFeedEntry({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<SnNewsArticle> news = data.data
|
||||
.map((ele) => SnNewsArticle.fromJson(ele))
|
||||
.cast<SnNewsArticle>()
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Symbols.newspaper),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'newsToday',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).tr()
|
||||
],
|
||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
height: 150,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: news.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
width: 360,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: Material(
|
||||
elevation: 0,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
news[idx].title,
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
).padding(horizontal: 16, top: 12, bottom: 4),
|
||||
Text(
|
||||
news[idx].description,
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
).padding(horizontal: 16, vertical: 4),
|
||||
const Gap(4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('y/M/d HH:mm')
|
||||
.format(news[idx].createdAt.toLocal()),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
RelativeTime(context)
|
||||
.format(news[idx].createdAt.toLocal()),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
).opacity(0.8).padding(horizontal: 16),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'newsDetail',
|
||||
pathParameters: {'hash': news[idx].hash},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(12),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,67 +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:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/types/news.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class NewsFeedEntry extends StatelessWidget {
|
||||
final SnFeedEntry data;
|
||||
const NewsFeedEntry({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ele = SnSubscriptionItem.fromJson(data.data);
|
||||
|
||||
return InkWell(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (ele.thumbnail.isNotEmpty && ele.thumbnail.startsWith('http'))
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AutoResizeUniversalImage(ele.thumbnail),
|
||||
),
|
||||
),
|
||||
).padding(horizontal: 16, bottom: 8, top: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Symbols.globe),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
ele.title,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
],
|
||||
).padding(horizontal: 18, vertical: 4),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(ele.description),
|
||||
Text(DateFormat().format(ele.createdAt.toLocal()))
|
||||
.tr()
|
||||
.fontSize(13)
|
||||
.opacity(0.8),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 4),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('readerFeedDetail', pathParameters: {
|
||||
'id': ele.id.toString(),
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -232,7 +232,7 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
|
||||
final alias = match[0]!;
|
||||
final anchor = markdown.Element.text('a', alias)
|
||||
..attributes['href'] = Uri.encodeFull(
|
||||
'solink://accounts/${alias.substring(1)}',
|
||||
'solink://account/${alias.substring(1)}',
|
||||
);
|
||||
parser.addNode(anchor);
|
||||
|
||||
|
168
lib/widgets/post/fediverse_post_item.dart
Normal file
168
lib/widgets/post/fediverse_post_item.dart
Normal file
@ -0,0 +1,168 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class FediversePostWidget extends StatelessWidget {
|
||||
final SnFediversePost data;
|
||||
final double maxWidth;
|
||||
const FediversePostWidget({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.maxWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AccountImage(
|
||||
content: data.user.avatar,
|
||||
radius: 20,
|
||||
),
|
||||
const Gap(12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.user.nick.isNotEmpty
|
||||
? data.user.nick
|
||||
: '@${data.user.name}',
|
||||
maxLines: 1,
|
||||
).bold(),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
data.user.identifier.contains('@')
|
||||
? data.user.identifier
|
||||
: '${data.user.identifier}@${data.user.origin}',
|
||||
maxLines: 1,
|
||||
).fontSize(13),
|
||||
const Gap(4),
|
||||
Text(
|
||||
RelativeTime(context)
|
||||
.format(data.createdAt.toLocal()),
|
||||
).fontSize(13),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12, vertical: 8),
|
||||
MarkdownTextContent(
|
||||
isAutoWarp: true,
|
||||
content: html2md.convert(data.content),
|
||||
).padding(horizontal: 16, bottom: 6),
|
||||
if (data.images.isNotEmpty)
|
||||
_FediversePostImageList(
|
||||
data: data,
|
||||
maxWidth: maxWidth,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FediversePostImageList extends StatelessWidget {
|
||||
const _FediversePostImageList({
|
||||
required this.data,
|
||||
required this.maxWidth,
|
||||
});
|
||||
|
||||
final SnFediversePost data;
|
||||
final double maxWidth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderSide =
|
||||
BorderSide(width: 1, color: Theme.of(context).dividerColor);
|
||||
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||
|
||||
if (data.images.length == 1) {
|
||||
return AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AutoResizeUniversalImage(
|
||||
data.images.first,
|
||||
),
|
||||
),
|
||||
),
|
||||
).padding(horizontal: 8);
|
||||
}
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ScrollConfiguration(
|
||||
behavior: AttachmentListScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: data.images.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AutoResizeUniversalImage(
|
||||
data.images[idx],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(
|
||||
label: Text('${idx + 1}/${data.images.length}'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@ -42,7 +41,6 @@ import 'package:surface/widgets/post/post_poll.dart';
|
||||
import 'package:surface/widgets/post/post_reaction.dart';
|
||||
import 'package:surface/widgets/post/publisher_popover.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
class OpenablePostItem extends StatelessWidget {
|
||||
@ -2323,10 +2321,7 @@ class _PostVideoPlayer extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
@ -2338,33 +2333,12 @@ class _PostVideoPlayer extends StatelessWidget {
|
||||
aspectRatio: 16 / 9,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: (data.body['video'] is String)
|
||||
? InAppWebView(
|
||||
initialUrlRequest: URLRequest(
|
||||
url: WebUri(data.body['video']),
|
||||
),
|
||||
)
|
||||
: AttachmentItem(
|
||||
child: AttachmentItem(
|
||||
data: data.body['video'],
|
||||
heroTag: 'post-video-${data.id}',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (data.body['video'] is String)
|
||||
InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.launch, size: 16),
|
||||
const Gap(6),
|
||||
Text('openInBrowser').tr(),
|
||||
],
|
||||
).opacity(0.8),
|
||||
onTap: () {
|
||||
launchUrlString(data.body['video']);
|
||||
},
|
||||
).padding(top: 4),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
@ -24,9 +22,9 @@ 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_actions.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
|
||||
import 'package:surface/widgets/context_menu.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
@ -52,6 +50,232 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
this.onUpdateBusy,
|
||||
});
|
||||
|
||||
Future<void> _cropImage(BuildContext context, int idx) async {
|
||||
final media = attachments[idx];
|
||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||
? await showCupertinoImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
// ignore: use_build_context_synchronously
|
||||
imageProvider: media.getImageProvider(context)!,
|
||||
)
|
||||
: await showMaterialImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
// ignore: use_build_context_synchronously
|
||||
imageProvider: media.getImageProvider(context)!,
|
||||
);
|
||||
|
||||
if (result == null) return;
|
||||
|
||||
final rawBytes =
|
||||
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List();
|
||||
|
||||
if (onUpdate != null) {
|
||||
final updatedMedia = PostWriteMedia.fromBytes(
|
||||
rawBytes,
|
||||
media.name,
|
||||
media.type,
|
||||
);
|
||||
await onUpdate!(idx, updatedMedia);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setThumbnail(BuildContext context, int idx) async {
|
||||
if (idx == -1) {
|
||||
// Thumbnail only can set on video or audio. And thumbnail of the post must be an image, so it's not possible to set thumbnail on the post thumbnail.
|
||||
return;
|
||||
} else if (attachments[idx].attachment == 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(
|
||||
attachments[idx].attachment!,
|
||||
thumbnailId: thumbnail.id,
|
||||
);
|
||||
onUpdate!(idx, PostWriteMedia(newAttach));
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteAttachment(BuildContext context, int idx) async {
|
||||
final media = attachments[idx];
|
||||
if (media.attachment == null) return;
|
||||
|
||||
try {
|
||||
onUpdateBusy?.call(true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/uc/attachments/${media.attachment!.id}');
|
||||
onRemove!(idx);
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
onUpdateBusy?.call(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createBoost(BuildContext context, int idx) async {
|
||||
if (attachments[idx].attachment == null) return;
|
||||
|
||||
final result = await showDialog<SnAttachmentBoost?>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
PendingAttachmentBoostDialog(media: attachments[idx]),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
final newAttach = attachments[idx].attachment!.copyWith(
|
||||
boosts: [...attachments[idx].attachment!.boosts, result],
|
||||
);
|
||||
final newMedia = PostWriteMedia(newAttach);
|
||||
|
||||
onUpdate!(idx, newMedia);
|
||||
}
|
||||
|
||||
Future<void> _compressVideo(BuildContext context, int idx) async {
|
||||
final result = await showDialog<PostWriteMedia?>(
|
||||
context: context,
|
||||
builder: (context) => PendingVideoCompressDialog(media: attachments[idx]),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
onUpdate!(idx, result);
|
||||
}
|
||||
|
||||
Future<void> _setAlt(BuildContext context, int idx) async {
|
||||
final result = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentAltDialog(media: attachments[idx]),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
onUpdate!(idx, PostWriteMedia(result));
|
||||
}
|
||||
|
||||
ContextMenu _createContextMenu(
|
||||
BuildContext context, int idx, PostWriteMedia media) {
|
||||
final canCompressVideo =
|
||||
!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
||||
return ContextMenu(
|
||||
entries: [
|
||||
if (media.attachment == null &&
|
||||
media.type == SnMediaType.video &&
|
||||
canCompressVideo)
|
||||
MenuItem(
|
||||
label: 'attachmentCompressVideo'.tr(),
|
||||
icon: Symbols.compress,
|
||||
onSelected: () {
|
||||
_compressVideo(context, idx);
|
||||
},
|
||||
),
|
||||
if (media.attachment != null)
|
||||
MenuItem(
|
||||
label: 'attachmentSetAlt'.tr(),
|
||||
icon: Symbols.description,
|
||||
onSelected: () {
|
||||
_setAlt(context, idx);
|
||||
},
|
||||
),
|
||||
if (media.attachment != null)
|
||||
MenuItem(
|
||||
label: 'attachmentBoost'.tr(),
|
||||
icon: Symbols.bolt,
|
||||
onSelected: () {
|
||||
_createBoost(context, idx);
|
||||
},
|
||||
),
|
||||
if (media.attachment != null && media.type == SnMediaType.video)
|
||||
MenuItem(
|
||||
label: 'attachmentSetThumbnail'.tr(),
|
||||
icon: Symbols.image,
|
||||
onSelected: () {
|
||||
_setThumbnail(context, idx);
|
||||
},
|
||||
),
|
||||
if (media.attachment == null && onUpload != null)
|
||||
MenuItem(
|
||||
label: 'attachmentUpload'.tr(),
|
||||
icon: Symbols.upload,
|
||||
onSelected: () {
|
||||
onUpload!(idx);
|
||||
}),
|
||||
if (media.attachment != null && onInsertLink != null)
|
||||
MenuItem(
|
||||
label: 'attachmentInsertLink'.tr(),
|
||||
icon: Symbols.add_link,
|
||||
onSelected: () {
|
||||
onInsertLink!(idx);
|
||||
},
|
||||
),
|
||||
if (media.type == SnMediaType.image && media.attachment != null)
|
||||
MenuItem(
|
||||
label: 'preview'.tr(),
|
||||
icon: Symbols.preview,
|
||||
onSelected: () {
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(data: [media.attachment!]),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (media.type == SnMediaType.image && media.attachment == null)
|
||||
MenuItem(
|
||||
label: 'crop'.tr(),
|
||||
icon: Symbols.crop,
|
||||
onSelected: () => _cropImage(context, idx),
|
||||
),
|
||||
if (media.attachment != null)
|
||||
MenuItem(
|
||||
label: 'attachmentCopyRandomId'.tr(),
|
||||
icon: Symbols.content_copy,
|
||||
onSelected: () {
|
||||
Clipboard.setData(ClipboardData(text: media.attachment!.rid));
|
||||
},
|
||||
),
|
||||
if (media.attachment != null && onRemove != null)
|
||||
MenuItem(
|
||||
label: 'delete'.tr(),
|
||||
icon: Symbols.delete,
|
||||
onSelected: isBusy ? null : () => _deleteAttachment(context, idx),
|
||||
),
|
||||
if (media.attachment == null && onRemove != null)
|
||||
MenuItem(
|
||||
label: 'delete'.tr(),
|
||||
icon: Symbols.delete,
|
||||
onSelected: () {
|
||||
onRemove!(idx);
|
||||
},
|
||||
)
|
||||
else if (onRemove != null)
|
||||
MenuItem(
|
||||
label: 'unlink'.tr(),
|
||||
icon: Symbols.link_off,
|
||||
onSelected: () {
|
||||
onRemove!(idx);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@ -63,27 +287,9 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
itemCount: attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final media = attachments[idx];
|
||||
return GestureDetector(
|
||||
return ContextMenuArea(
|
||||
contextMenu: _createContextMenu(context, idx, media),
|
||||
child: _PostMediaPendingItem(media: media),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentActionSheet(
|
||||
media: media,
|
||||
),
|
||||
).then((value) async {
|
||||
if (value is PostWriteMedia) {
|
||||
await onUpdate!(idx, value);
|
||||
}
|
||||
if (value == 'link') {
|
||||
onInsertLink!(idx);
|
||||
} else if (value == false) {
|
||||
onRemove!(idx);
|
||||
} else if (value == true) {
|
||||
onUpload!(idx);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -94,7 +300,9 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
class _PostMediaPendingItem extends StatelessWidget {
|
||||
final PostWriteMedia media;
|
||||
|
||||
const _PostMediaPendingItem({required this.media});
|
||||
const _PostMediaPendingItem({
|
||||
required this.media,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -113,36 +321,34 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: switch (media.type) {
|
||||
SnMediaType.image => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
SnMediaType.image =>
|
||||
LayoutBuilder(builder: (context, constraints) {
|
||||
return Image(
|
||||
image: media.getImageProvider(
|
||||
context,
|
||||
width:
|
||||
(constraints.maxWidth * devicePixelRatio).round(),
|
||||
height: (constraints.maxHeight * devicePixelRatio)
|
||||
.round(),
|
||||
height:
|
||||
(constraints.maxHeight * devicePixelRatio).round(),
|
||||
)!,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
},
|
||||
),
|
||||
}),
|
||||
SnMediaType.video => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (media.attachment?.thumbnail != null)
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(
|
||||
media.attachment!.thumbnail!.rid)),
|
||||
const Icon(
|
||||
Symbols.videocam,
|
||||
const Icon(Symbols.videocam,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 8.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0))
|
||||
],
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
SnMediaType.audio => Stack(
|
||||
@ -151,16 +357,15 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
if (media.attachment?.thumbnail != null)
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(
|
||||
media.attachment!.thumbnail!.rid)),
|
||||
const Icon(
|
||||
Symbols.audio_file,
|
||||
const Icon(Symbols.audio_file,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 8.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0))
|
||||
],
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
_ => Container(
|
||||
@ -182,8 +387,11 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (media.attachment != null)
|
||||
Text(media.attachment!.alt,
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis)
|
||||
Text(
|
||||
media.attachment!.alt,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
else if (media.file != null)
|
||||
Text(media.file!.name,
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis)
|
||||
@ -260,8 +468,11 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
final VisualDensity? visualDensity;
|
||||
final Function(Iterable<PostWriteMedia>) onAdd;
|
||||
|
||||
const AddPostMediaButton(
|
||||
{super.key, required this.onAdd, this.visualDensity});
|
||||
const AddPostMediaButton({
|
||||
super.key,
|
||||
required this.onAdd,
|
||||
this.visualDensity,
|
||||
});
|
||||
|
||||
void _takeMedia(bool isVideo) async {
|
||||
final picker = ImagePicker();
|
||||
@ -276,13 +487,17 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
final picker = ImagePicker();
|
||||
final result = await picker.pickMultipleMedia();
|
||||
if (result.isEmpty) return;
|
||||
onAdd(result.map((e) => PostWriteMedia.fromFile(e)));
|
||||
onAdd(
|
||||
result.map((e) => PostWriteMedia.fromFile(e)),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectFile() async {
|
||||
final result = await FilePicker.platform.pickFiles(type: FileType.any);
|
||||
if (result == null) return;
|
||||
onAdd(result.files.map((e) => PostWriteMedia.fromFile(e.xFile)));
|
||||
onAdd(
|
||||
result.files.map((e) => PostWriteMedia.fromFile(e.xFile)),
|
||||
);
|
||||
}
|
||||
|
||||
void _pasteMedia() async {
|
||||
@ -290,7 +505,10 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
if (imageBytes == null) return;
|
||||
onAdd([
|
||||
PostWriteMedia.fromBytes(
|
||||
imageBytes, 'attachmentPastedImage'.tr(), SnMediaType.image)
|
||||
imageBytes,
|
||||
'attachmentPastedImage'.tr(),
|
||||
SnMediaType.image,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -338,15 +556,19 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
final attachment = await attach.getOne(randomId);
|
||||
|
||||
onAdd([PostWriteMedia(attachment)]);
|
||||
onAdd([
|
||||
PostWriteMedia(attachment),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton(
|
||||
style: ButtonStyle(visualDensity: visualDensity),
|
||||
icon: Icon(Symbols.add_photo_alternate,
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
icon: Icon(
|
||||
Symbols.add_photo_alternate,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
itemBuilder: (context) => [
|
||||
if (!kIsWeb &&
|
||||
!Platform.isLinux &&
|
||||
@ -373,7 +595,7 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Symbols.videocam),
|
||||
const Gap(16),
|
||||
Text('addAttachmentFromCameraVideo').tr()
|
||||
Text('addAttachmentFromCameraVideo').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@ -385,7 +607,7 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Symbols.photo_library),
|
||||
const Gap(16),
|
||||
Text('addAttachmentFromAlbum').tr()
|
||||
Text('addAttachmentFromAlbum').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@ -397,7 +619,7 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Symbols.file_upload),
|
||||
const Gap(16),
|
||||
Text('addAttachmentFromFiles').tr()
|
||||
Text('addAttachmentFromFiles').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@ -405,11 +627,13 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(children: [
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.link),
|
||||
const Gap(16),
|
||||
Text('addAttachmentFromRandomId').tr()
|
||||
]),
|
||||
Text('addAttachmentFromRandomId').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
_linkRandomId(context);
|
||||
},
|
||||
@ -419,7 +643,7 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Symbols.content_paste),
|
||||
const Gap(16),
|
||||
Text('addAttachmentFromClipboard').tr()
|
||||
Text('addAttachmentFromClipboard').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
|
@ -40,7 +40,6 @@ class UnauthorizedHint extends StatelessWidget {
|
||||
GoRouter.of(context).pushNamed('authLogin').then((value) {
|
||||
if (value == true && context.mounted) {
|
||||
final ua = context.read<UserProvider>();
|
||||
ua.refreshUser();
|
||||
context.showSnackbar('loginSuccess'.tr(args: [
|
||||
'@${ua.user?.name} (${ua.user?.nick})',
|
||||
]));
|
||||
|
@ -41,7 +41,7 @@ endif()
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Wextra)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||
endfunction()
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_timezone/flutter_timezone_plugin.h>
|
||||
#include <flutter_udid/flutter_udid_plugin.h>
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
#include <hotkey_manager_linux/hotkey_manager_linux_plugin.h>
|
||||
#include <local_notifier/local_notifier_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
@ -44,6 +45,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_udid_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin");
|
||||
flutter_udid_plugin_register_with_registrar(flutter_udid_registrar);
|
||||
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
||||
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
||||
g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
|
||||
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar);
|
||||
|
@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
flutter_timezone
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
hotkey_manager_linux
|
||||
local_notifier
|
||||
media_kit_libs_linux
|
||||
|
@ -19,9 +19,12 @@ import firebase_messaging
|
||||
import flutter_inappwebview_macos
|
||||
import flutter_timezone
|
||||
import flutter_udid
|
||||
import flutter_webrtc
|
||||
import gal
|
||||
import hotkey_manager_macos
|
||||
import in_app_review
|
||||
import livekit_client
|
||||
import livekit_noise_filter
|
||||
import local_notifier
|
||||
import media_kit_libs_macos_video
|
||||
import media_kit_video
|
||||
@ -53,9 +56,12 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
||||
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
||||
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
|
||||
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
||||
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
|
||||
LiveKitKrispNoiseFilterPlugin.register(with: registry.registrar(forPlugin: "LiveKitKrispNoiseFilterPlugin"))
|
||||
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
|
||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||
|
@ -85,6 +85,9 @@ PODS:
|
||||
- flutter_udid (0.0.1):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain
|
||||
- flutter_webrtc (0.12.6):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
- FlutterMacOS (1.0.0)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
@ -145,6 +148,15 @@ PODS:
|
||||
- HotKey
|
||||
- in_app_review (2.0.0):
|
||||
- FlutterMacOS
|
||||
- livekit_client (2.4.1):
|
||||
- flutter_webrtc
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
- livekit_noise_filter (0.0.1):
|
||||
- flutter_webrtc
|
||||
- FlutterMacOS
|
||||
- LiveKitKrispNoiseFilter (= 0.0.7)
|
||||
- LiveKitKrispNoiseFilter (0.0.7)
|
||||
- local_notifier (0.1.0):
|
||||
- FlutterMacOS
|
||||
- media_kit_libs_macos_video (1.0.4):
|
||||
@ -206,6 +218,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (125.6422.06)
|
||||
|
||||
DEPENDENCIES:
|
||||
- audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`)
|
||||
@ -223,10 +236,13 @@ DEPENDENCIES:
|
||||
- flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
|
||||
- flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`)
|
||||
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
|
||||
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
||||
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
|
||||
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
||||
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
||||
- livekit_noise_filter (from `Flutter/ephemeral/.symlinks/plugins/livekit_noise_filter/macos`)
|
||||
- local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`)
|
||||
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
|
||||
@ -255,11 +271,13 @@ SPEC REPOS:
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- HotKey
|
||||
- LiveKitKrispNoiseFilter
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- SAMKeychain
|
||||
- sqlite3
|
||||
- WebRTC-SDK
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
audioplayers_darwin:
|
||||
@ -292,6 +310,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos
|
||||
flutter_udid:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
|
||||
flutter_webrtc:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
gal:
|
||||
@ -300,6 +320,10 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
|
||||
in_app_review:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
|
||||
livekit_client:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
|
||||
livekit_noise_filter:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/livekit_noise_filter/macos
|
||||
local_notifier:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos
|
||||
media_kit_libs_macos_video:
|
||||
@ -353,6 +377,7 @@ SPEC CHECKSUMS:
|
||||
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
||||
flutter_timezone: d59eea86178cbd7943cd2431cc2eaa9850f935d8
|
||||
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
|
||||
flutter_webrtc: 377dbcebdde6fed0fc40de87bcaaa2bffcec9a88
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d
|
||||
@ -361,6 +386,9 @@ SPEC CHECKSUMS:
|
||||
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
|
||||
hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe
|
||||
in_app_review: 0599bccaed5e02f6bed2b0d30d16f86b63ed8638
|
||||
livekit_client: 35690bf9861be6325a6f7d11bb38d50c7c9fed80
|
||||
livekit_noise_filter: c5710c0871ef3621b48c0b44d3c3ff938ba414b2
|
||||
LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84
|
||||
local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e
|
||||
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
|
||||
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
|
||||
@ -381,6 +409,7 @@ SPEC CHECKSUMS:
|
||||
video_compress: 752b161da855df2492dd1a8fa899743cc8fe9534
|
||||
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
|
||||
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
|
||||
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
||||
|
||||
PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009
|
||||
|
||||
|
76
pubspec.lock
76
pubspec.lock
@ -417,6 +417,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
dart_webrtc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dart_webrtc
|
||||
sha256: "8565f1f1f412b8a6fd862f3a157560811e61eeeac26741c735a5d2ff409a0202"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.3"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -903,10 +911,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: "634622a3a826d67cb05c0e3e576d1812c430fa98404e95b60b131775c73d76ec"
|
||||
sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
version: "0.7.6+2"
|
||||
flutter_markdown_latex:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -989,14 +997,22 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_webrtc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_webrtc
|
||||
sha256: b832dc76c0d1577f14aaf35e9c38d4ed7667cbc89c492b7bf4505d8d5f62e08b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.12+hotfix.1"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: "19e64d719a9f0d2e7f74a2f59624acee0e96b3e897ecf72edcae52ccc36a424f"
|
||||
sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.4"
|
||||
freezed_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1277,14 +1293,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
jitsi_meet_flutter_sdk:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: jitsi_meet_flutter_sdk
|
||||
sha256: ad72f7ae8db1508c944a7a7f135c4cccdc676efb31ab7617b9f5b0dac4791ccd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.1.1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1365,6 +1373,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
livekit_client:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: livekit_client
|
||||
sha256: "7f489fa415253d8d99c649b7efc95a733c5e5ac38dcfb02362ced99feb139376"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
livekit_noise_filter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: livekit_noise_filter
|
||||
sha256: "398bfd1cc63ada9dee9fd7ea415e2fc1e51e091a6d217aad3649b882c35c7fcb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
local_notifier:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1773,6 +1797,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1885,6 +1917,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
sdp_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sdp_transform
|
||||
sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -2302,10 +2342,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
|
||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
version: "6.3.2"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2474,6 +2514,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
webrtc_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webrtc_interface
|
||||
sha256: e92afec11152a9ccb5c9f35482754edd99696e886ab6acaf90c06dd2d09f09eb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2+hotfix.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 2.5.2+92
|
||||
version: 2.4.2+89
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@ -82,6 +82,8 @@ dependencies:
|
||||
media_kit_libs_video: ^1.0.5
|
||||
pasteboard: ^0.3.0
|
||||
synchronized: ^3.3.0+3
|
||||
dart_webrtc: ^1.4.10
|
||||
livekit_client: ^2.3.1+hotfix.1
|
||||
wakelock_plus: ^1.2.8
|
||||
permission_handler: ^11.3.1
|
||||
flutter_staggered_grid_view: ^0.7.0
|
||||
@ -111,6 +113,7 @@ dependencies:
|
||||
version: ^3.0.2
|
||||
flutter_colorpicker: ^1.1.0
|
||||
fl_chart: ^0.70.0
|
||||
flutter_webrtc: ^0.12.5+hotfix.1
|
||||
slide_countdown: ^2.0.2
|
||||
video_compress: ^3.1.3
|
||||
cached_network_image: ^3.4.1
|
||||
@ -141,7 +144,7 @@ dependencies:
|
||||
latlong2: ^0.9.1
|
||||
crypto: ^3.0.6
|
||||
audioplayers: ^6.4.0
|
||||
jitsi_meet_flutter_sdk: ^11.1.1
|
||||
livekit_noise_filter: ^0.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -16,8 +16,10 @@
|
||||
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
||||
#include <flutter_timezone/flutter_timezone_plugin_c_api.h>
|
||||
#include <flutter_udid/flutter_udid_plugin_c_api.h>
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
#include <gal/gal_plugin_c_api.h>
|
||||
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
|
||||
#include <livekit_client/live_kit_plugin.h>
|
||||
#include <local_notifier/local_notifier_plugin.h>
|
||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
@ -50,10 +52,14 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi"));
|
||||
FlutterUdidPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
|
||||
FlutterWebRTCPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
||||
GalPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("GalPluginCApi"));
|
||||
HotkeyManagerWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
|
||||
LiveKitPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("LiveKitPlugin"));
|
||||
LocalNotifierPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
|
||||
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
||||
|
@ -13,8 +13,10 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_inappwebview_windows
|
||||
flutter_timezone
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
gal
|
||||
hotkey_manager_windows
|
||||
livekit_client
|
||||
local_notifier
|
||||
media_kit_libs_windows_video
|
||||
media_kit_video
|
||||
|
Loading…
x
Reference in New Issue
Block a user