Compare commits

..

No commits in common. "master" and "2.4.2+89" have entirely different histories.

75 changed files with 4713 additions and 1797 deletions

2
android/.gitignore vendored
View File

@ -11,5 +11,3 @@ GeneratedPluginRegistrant.java
key.properties key.properties
**/*.keystore **/*.keystore
**/*.jks **/*.jks
app/.cxx

View File

@ -10,15 +10,10 @@ plugins {
} }
dependencies { 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 'com.google.android.material:material:1.12.0'
implementation 'androidx.glance:glance:1.1.1' implementation 'androidx.glance:glance:1.1.1'
implementation 'androidx.glance:glance-appwidget: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.google.code.gson:gson:2.10.1'
implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'io.coil-kt.coil3:coil-compose:3.0.4' implementation 'io.coil-kt.coil3:coil-compose:3.0.4'
@ -78,10 +73,8 @@ android {
} }
release { release {
signingConfig = signingConfigs.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'
} }
} }
} }

View File

@ -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 *;
}

View File

@ -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" />
<uses-feature android:name="android.hardware.camera.autofocus" /> <uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.INTERNET" /> <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.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application <application
tools:replace="android:label"
android:label="Solian" android:label="Solian"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"

14
android/app/src/proguard-rules.pro vendored Normal file
View 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>;
}

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

View File

@ -10,22 +10,18 @@ pluginManagement {
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories { repositories {
maven {
url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
}
google() google()
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
maven { url 'https://www.jitpack.io' }
} }
} }
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" 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 // START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.4.2" apply false id "com.google.gms.google-services" version "4.3.15" apply false
id "com.google.firebase.crashlytics" version "3.0.3" apply false id "com.google.firebase.crashlytics" version "2.8.1" apply false
// END: FlutterFire Configuration // END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "1.8.22" apply false id "org.jetbrains.kotlin.android" version "1.8.22" apply false
} }

View File

@ -12,9 +12,8 @@ post {
body:json { body:json {
{ {
"reason": "吹哨管理条例 / 滥用吹哨功能,累积三次复核无效吹哨。处以禁用吹哨功能 30 天。", "reason": "侮辱 Solar Network 商标,煽动颠覆中华羊国政权,制造不实信息,传播谣言,制造恐慌,寻衅滋事。",
"type": 1, "type": 0,
"perm_nodes": {"FlagPost":false},
"account_id": 5 "account_id": 5
} }
} }

View File

@ -11,5 +11,8 @@ post {
} }
body:json { body:json {
{} {
"sources": ["taiwan-pts"],
"eager": true
}
} }

View File

@ -543,7 +543,6 @@
"attachmentSaved": "Saved to album", "attachmentSaved": "Saved to album",
"attachmentSavedDesktop": "Saved to Downloads folder", "attachmentSavedDesktop": "Saved to Downloads folder",
"openInAlbum": "Open in album", "openInAlbum": "Open in album",
"openInBrowser": "Open in browser",
"postAbuseReport": "Report Post", "postAbuseReport": "Report Post",
"postAbuseReportDescription": "Report posts that violate our user agreement and community guidelines to help us improve the content on Solar Network. Please describe how this post violates the relevant rules. Do not include any sensitive information. We will process your report within 24 hours.", "postAbuseReportDescription": "Report posts that violate our user agreement and community guidelines to help us improve the content on Solar Network. Please describe how this post violates the relevant rules. Do not include any sensitive information. We will process your report within 24 hours.",
"abuseReport": "Abuse Report", "abuseReport": "Abuse Report",
@ -949,21 +948,5 @@
"postEditedHint": "edited", "postEditedHint": "edited",
"splashScreenServer": "Server", "splashScreenServer": "Server",
"splashScreenServerName": "Potato", "splashScreenServerName": "Potato",
"splashScreenCaption": "Trying to establishing connection with HyperNet™", "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."
} }

View File

@ -541,7 +541,6 @@
"attachmentSaved": "已保存到相册", "attachmentSaved": "已保存到相册",
"attachmentSavedDesktop": "已保存到下载目录", "attachmentSavedDesktop": "已保存到下载目录",
"openInAlbum": "在相册中打开", "openInAlbum": "在相册中打开",
"openInBrowser": "在浏览器中打开",
"postAbuseReport": "检举帖子", "postAbuseReport": "检举帖子",
"postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。", "postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
"abuseReport": "检举", "abuseReport": "检举",
@ -946,21 +945,5 @@
"postEditedHint": "已编辑", "postEditedHint": "已编辑",
"splashScreenServer": "服务器", "splashScreenServer": "服务器",
"splashScreenServerName": "土豆", "splashScreenServerName": "土豆",
"splashScreenCaption": "正在尝试与 HyperNet™ 取得太阳链连接", "splashScreenCaption": "正在尝试与 HyperNet™ 取得太阳链连接"
"attachmentEditor": "附件编辑器",
"attachmentEditorUnUploadHint": "该附件未上传,元数据编辑不可用,同时你可以裁剪本附件。",
"attachmentEditorUploadHint": "该附件已上传。",
"attachmentRating": "评级",
"fieldAttachmentRating": "内容分级",
"fieldAttachmentQuality": "质量评分",
"attachmentReferenceLink": "引用外部附件",
"fieldAttachmentReferenceLink": "引用连接",
"attachmentReferenceLinkDescription": "作为附件的源文件。需要链接允许跨域访问。",
"fieldAttachmentMimetype": "文件类型",
"postVideoLive": "直播",
"postVideoLiveDescription": "这是一条直播影片,允许用户自行嵌入源站。",
"postVideoRendererWeb": "网页渲染器",
"postVideoRendererWebDescription": "使用 WebView 渲染内容。",
"fieldPostVideoUrl": "视频流地址",
"fieldPostVideoUrlDescription": "视频内容的地址,可以为网页,将会使用 iFrame / WebView 渲染。"
} }

View File

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
platform :ios, '15.1' platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@ -122,11 +122,12 @@ PODS:
- flutter_udid (0.0.1): - flutter_udid (0.0.1):
- Flutter - Flutter
- SAMKeychain - SAMKeychain
- flutter_webrtc (0.12.6):
- Flutter
- WebRTC-SDK (= 125.6422.06)
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- Giphy (2.2.12):
- libwebp
- GoogleAppMeasurement (11.10.0): - GoogleAppMeasurement (11.10.0):
- GoogleAppMeasurement/AdIdSupport (= 11.10.0) - GoogleAppMeasurement/AdIdSupport (= 11.10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -183,26 +184,16 @@ PODS:
- Flutter - Flutter
- in_app_review (2.0.0): - in_app_review (2.0.0):
- Flutter - 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) - Kingfisher (8.3.1)
- libwebp (1.5.0): - livekit_client (2.4.1):
- libwebp/demux (= 1.5.0) - Flutter
- libwebp/mux (= 1.5.0) - flutter_webrtc
- libwebp/sharpyuv (= 1.5.0) - WebRTC-SDK (= 125.6422.06)
- libwebp/webp (= 1.5.0) - livekit_noise_filter (0.0.1):
- libwebp/demux (1.5.0): - Flutter
- libwebp/webp - flutter_webrtc
- libwebp/mux (1.5.0): - LiveKitKrispNoiseFilter (= 0.0.7)
- libwebp/demux - LiveKitKrispNoiseFilter (0.0.7)
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- media_kit_libs_ios_video (1.0.4): - media_kit_libs_ios_video (1.0.4):
- Flutter - Flutter
- media_kit_video (0.0.1): - media_kit_video (0.0.1):
@ -268,6 +259,7 @@ PODS:
- Flutter - Flutter
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- Flutter - Flutter
- WebRTC-SDK (125.6422.06)
- workmanager (0.0.1): - workmanager (0.0.1):
- Flutter - Flutter
@ -289,12 +281,14 @@ DEPENDENCIES:
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) - flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- gal (from `.symlinks/plugins/gal/darwin`) - gal (from `.symlinks/plugins/gal/darwin`)
- home_widget (from `.symlinks/plugins/home_widget/ios`) - home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- jitsi_meet_flutter_sdk (from `.symlinks/plugins/jitsi_meet_flutter_sdk/ios`)
- Kingfisher (~> 8.0) - Kingfisher (~> 8.0)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- livekit_noise_filter (from `.symlinks/plugins/livekit_noise_filter/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@ -323,14 +317,11 @@ SPEC REPOS:
- FirebaseCoreInternal - FirebaseCoreInternal
- FirebaseInstallations - FirebaseInstallations
- FirebaseMessaging - FirebaseMessaging
- Giphy
- GoogleAppMeasurement - GoogleAppMeasurement
- GoogleDataTransport - GoogleDataTransport
- GoogleUtilities - GoogleUtilities
- JitsiMeetSDK
- JitsiWebRTC
- Kingfisher - Kingfisher
- libwebp - LiveKitKrispNoiseFilter
- nanopb - nanopb
- OrderedSet - OrderedSet
- PromisesObjC - PromisesObjC
@ -338,6 +329,7 @@ SPEC REPOS:
- SDWebImage - SDWebImage
- sqlite3 - sqlite3
- SwiftyGif - SwiftyGif
- WebRTC-SDK
EXTERNAL SOURCES: EXTERNAL SOURCES:
audioplayers_darwin: audioplayers_darwin:
@ -372,6 +364,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_timezone/ios" :path: ".symlinks/plugins/flutter_timezone/ios"
flutter_udid: flutter_udid:
:path: ".symlinks/plugins/flutter_udid/ios" :path: ".symlinks/plugins/flutter_udid/ios"
flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios"
gal: gal:
:path: ".symlinks/plugins/gal/darwin" :path: ".symlinks/plugins/gal/darwin"
home_widget: home_widget:
@ -380,8 +374,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios" :path: ".symlinks/plugins/image_picker_ios/ios"
in_app_review: in_app_review:
:path: ".symlinks/plugins/in_app_review/ios" :path: ".symlinks/plugins/in_app_review/ios"
jitsi_meet_flutter_sdk: livekit_client:
:path: ".symlinks/plugins/jitsi_meet_flutter_sdk/ios" :path: ".symlinks/plugins/livekit_client/ios"
livekit_noise_filter:
:path: ".symlinks/plugins/livekit_noise_filter/ios"
media_kit_libs_ios_video: media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios" :path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_video: media_kit_video:
@ -441,19 +437,18 @@ SPEC CHECKSUMS:
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_webrtc: 57f32415b8744e806f9c2a96ccdb60c6a627ba33
gal: baecd024ebfd13c441269ca7404792a7152fde89 gal: baecd024ebfd13c441269ca7404792a7152fde89
Giphy: 83628960ed04e1c3428ff1b4fb2b027f65e82f50
GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457 in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457
jitsi_meet_flutter_sdk: 0283a60730922d608fbad9872e07afdd5bb3578a
JitsiMeetSDK: 4e1c269aaaed8f2cb7b0fff2d3c00f08359b170e
JitsiWebRTC: b47805ab5668be38e7ee60e2258f49badfe8e1d0
Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3 Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 livekit_client: 08755cabfa4da4ed455642f460cfbb39bc518070
livekit_noise_filter: a26aeb1c1eae6db0a023fd2f6ea3ff108c3ecbb0
LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
@ -476,8 +471,9 @@ SPEC CHECKSUMS:
video_compress: f2133a07762889d67f0711ac831faa26f956980e video_compress: f2133a07762889d67f0711ac831faa26f956980e
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49 wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
PODFILE CHECKSUM: d278ce52a331dda323590121247d2046cd085ae7 PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View File

@ -961,7 +961,7 @@
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian; INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -1521,7 +1521,7 @@
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian; INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -1549,7 +1549,7 @@
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian; INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",

View File

@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSCalendarsUsageDescription</key>
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
<key>AppGroupId</key> <key>AppGroupId</key>
<string>group.solsynth.solian</string> <string>group.solsynth.solian</string>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>

View File

@ -219,8 +219,6 @@ class PostWriteController extends ChangeNotifier {
List<PostWriteMedia> attachments = List.empty(growable: true); List<PostWriteMedia> attachments = List.empty(growable: true);
DateTime? publishedAt, publishedUntil; DateTime? publishedAt, publishedUntil;
SnAttachment? videoAttachment; SnAttachment? videoAttachment;
String videoUrl = '';
bool videoLive = false;
SnPoll? poll; SnPoll? poll;
Future<void> fetchRelatedPost( Future<void> fetchRelatedPost(
@ -243,13 +241,9 @@ class PostWriteController extends ChangeNotifier {
contentController.text = post.body['content'] ?? ''; contentController.text = post.body['content'] ?? '';
aliasController.text = post.alias ?? ''; aliasController.text = post.alias ?? '';
rewardController.text = post.body['reward']?.toString() ?? ''; rewardController.text = post.body['reward']?.toString() ?? '';
if (post.body['video'] != null) { videoAttachment = post.body['video'] != null
if (post.body['video'] is String) { ? SnAttachment.fromJson(post.body['video'])
videoUrl = post.body['video']; : null;
} else {
videoAttachment = SnAttachment.fromJson(post.body['video']);
}
}
publishedAt = post.publishedAt; publishedAt = post.publishedAt;
publishedUntil = post.publishedUntil; publishedUntil = post.publishedUntil;
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true); visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
@ -268,7 +262,6 @@ class PostWriteController extends ChangeNotifier {
); );
poll = post.poll; poll = post.poll;
videoLive = post.body['is_live'] ?? false;
editingDraft = post.isDraft; editingDraft = post.isDraft;
if (post.body['thumbnail'] != null) { if (post.body['thumbnail'] != null) {
@ -452,9 +445,8 @@ class PostWriteController extends ChangeNotifier {
titleController.text = data['title'] ?? ''; titleController.text = data['title'] ?? '';
descriptionController.text = data['description'] ?? ''; descriptionController.text = data['description'] ?? '';
rewardController.text = data['reward']?.toString() ?? ''; rewardController.text = data['reward']?.toString() ?? '';
if (data['thumbnail'] != null) { if (data['thumbnail'] != null)
thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail'])); thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
}
attachments.addAll(data['attachments'] attachments.addAll(data['attachments']
.map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))) .map((ele) => PostWriteMedia(SnAttachment.fromJson(ele)))
.cast<PostWriteMedia>()); .cast<PostWriteMedia>());
@ -463,12 +455,10 @@ class PostWriteController extends ChangeNotifier {
visibility = data['visibility']; visibility = data['visibility'];
visibleUsers = List.from(data['visible_users_list'] ?? []); visibleUsers = List.from(data['visible_users_list'] ?? []);
invisibleUsers = List.from(data['invisible_users_list'] ?? []); invisibleUsers = List.from(data['invisible_users_list'] ?? []);
if (data['published_at'] != null) { if (data['published_at'] != null)
publishedAt = DateTime.tryParse(data['published_at'])?.toLocal(); publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
} if (data['published_until'] != null)
if (data['published_until'] != null) {
publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal(); publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
}
replyingPost = replyingPost =
data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null; data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
repostingPost = repostingPost =
@ -605,11 +595,9 @@ class PostWriteController extends ChangeNotifier {
if (replyingPost != null) 'reply_to': replyingPost!.id, if (replyingPost != null) 'reply_to': replyingPost!.id,
if (repostingPost != null) 'repost_to': repostingPost!.id, if (repostingPost != null) 'repost_to': repostingPost!.id,
if (reward != null) 'reward': reward, if (reward != null) 'reward': reward,
if (videoAttachment != null || videoUrl.isNotEmpty) if (videoAttachment != null) 'video': videoAttachment!.rid,
'video': videoUrl.isNotEmpty ? videoUrl : videoAttachment!.rid,
if (poll != null) 'poll': poll!.id, if (poll != null) 'poll': poll!.id,
if (realm != null) 'realm': realm!.id, if (realm != null) 'realm': realm!.id,
if (videoLive) 'is_live': videoLive,
'is_draft': saveAsDraft, 'is_draft': saveAsDraft,
}, },
onSendProgress: (count, total) { onSendProgress: (count, total) {
@ -750,16 +738,6 @@ class PostWriteController extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void setVideoUrl(String value) {
videoUrl = value;
notifyListeners();
}
void setVideoLive(bool value) {
videoLive = value;
notifyListeners();
}
void setPoll(SnPoll? value) { void setPoll(SnPoll? value) {
poll = value; poll = value;
notifyListeners(); notifyListeners();

View File

@ -26,6 +26,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/firebase_options.dart'; import 'package:surface/firebase_options.dart';
import 'package:surface/logger.dart'; import 'package:surface/logger.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/database.dart'; import 'package:surface/providers/database.dart';
import 'package:surface/providers/keypair.dart'; import 'package:surface/providers/keypair.dart';
@ -197,6 +198,7 @@ class SolianApp extends StatelessWidget {
Provider(create: (ctx) => KeyPairProvider(ctx)), Provider(create: (ctx) => KeyPairProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)), ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)), ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
Provider(create: (ctx) => SnTranslator()), Provider(create: (ctx) => SnTranslator()),
// Additional helper layer // Additional helper layer

View 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();
}
}

View File

@ -64,6 +64,11 @@ class NavigationProvider extends ChangeNotifier {
screen: 'realm', screen: 'realm',
label: 'screenRealm', label: 'screenRealm',
), ),
AppNavDestination(
icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
screen: 'news',
label: 'screenNews',
),
AppNavDestination( AppNavDestination(
icon: Icon(Symbols.settings, weight: 400, opticalSize: 20), icon: Icon(Symbols.settings, weight: 400, opticalSize: 20),
screen: 'settings', screen: 'settings',

View File

@ -120,25 +120,6 @@ class SnAttachmentProvider {
'webp': 'image/webp', 'webp': 'image/webp',
}; };
Future<SnAttachment> createWithReferenceLink(
String url,
String pool,
Map<String, dynamic>? metadata, {
String? mimetype,
}) async {
final resp = await _sn.client.post(
'/cgi/uc/attachments/referenced',
data: {
'url': url,
'pool': pool,
'metadata': metadata,
if (mimetype != null) 'mimetype': mimetype,
},
);
return SnAttachment.fromJson(resp.data);
}
Future<SnAttachment> directUploadOne( Future<SnAttachment> directUploadOne(
Uint8List data, Uint8List data,
String filename, String filename,
@ -330,23 +311,6 @@ class SnAttachmentProvider {
return out; return out;
} }
Future<SnAttachment> rateOne(
SnAttachment item, {
int? content,
int? quality,
}) async {
final resp = await _sn.client.put(
'/cgi/uc/attachments/${item.id}/rating',
data: {
'content_rating': content ?? item.contentRating,
'quality_rating': quality ?? item.qualityRating,
},
);
final out = SnAttachment.fromJson(resp.data);
_saveToLocal([out]);
return out;
}
Future<void> _saveToLocal(Iterable<SnAttachment> out) async { Future<void> _saveToLocal(Iterable<SnAttachment> out) async {
for (final ele in out) { for (final ele in out) {
if (!ele.isAnalyzed || ele.destination == 0) continue; if (!ele.isAnalyzed || ele.destination == 0) continue;

View File

@ -22,6 +22,7 @@ import 'package:surface/screens/album.dart';
import 'package:surface/screens/auth/login.dart'; import 'package:surface/screens/auth/login.dart';
import 'package:surface/screens/auth/register.dart'; import 'package:surface/screens/auth/register.dart';
import 'package:surface/screens/chat.dart'; import 'package:surface/screens/chat.dart';
import 'package:surface/screens/chat/call_room.dart';
import 'package:surface/screens/chat/channel_detail.dart'; import 'package:surface/screens/chat/channel_detail.dart';
import 'package:surface/screens/chat/manage.dart'; import 'package:surface/screens/chat/manage.dart';
import 'package:surface/screens/chat/room.dart'; import 'package:surface/screens/chat/room.dart';
@ -29,7 +30,8 @@ import 'package:surface/screens/explore.dart';
import 'package:surface/screens/friend.dart'; import 'package:surface/screens/friend.dart';
import 'package:surface/screens/home.dart'; import 'package:surface/screens/home.dart';
import 'package:surface/screens/logging.dart'; import 'package:surface/screens/logging.dart';
import 'package:surface/screens/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/notification.dart';
import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/screens/post/post_detail.dart';
import 'package:surface/screens/post/post_draft.dart'; import 'package:surface/screens/post/post_draft.dart';
@ -124,13 +126,6 @@ final _appRoutes = [
preload: state.extra as SnPost?, preload: state.extra as SnPost?,
), ),
), ),
GoRoute(
path: '/pages/:id',
name: 'readerFeedDetail',
builder: (context, state) => ReaderPageScreen(
id: state.pathParameters['id']!,
),
),
GoRoute( GoRoute(
path: '/publishers/:name', path: '/publishers/:name',
name: 'postPublisher', name: 'postPublisher',
@ -269,6 +264,14 @@ final _appRoutes = [
extra: state.extra as ChatRoomScreenExtra?, extra: state.extra as ChatRoomScreenExtra?,
), ),
), ),
GoRoute(
path: '/:scope/:alias/call',
name: 'chatCallRoom',
builder: (context, state) => CallRoomScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
GoRoute( GoRoute(
path: '/:scope/:alias/detail', path: '/:scope/:alias/detail',
name: 'channelDetail', name: 'channelDetail',
@ -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( GoRoute(
path: '/stickers', path: '/stickers',
name: 'stickers', name: 'stickers',
@ -390,10 +407,4 @@ final appRouter = GoRouter(
), ),
), ),
], ],
onException: (context, state, router) {
if (state.error is GoException) {
router.goNamed('/');
}
},
navigatorKey: GlobalKey(),
); );

View File

@ -15,6 +15,7 @@ import 'package:surface/providers/websocket.dart';
import 'package:surface/types/account.dart'; import 'package:surface/types/account.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_status.dart'; import 'package:surface/widgets/account/account_status.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
@ -111,7 +112,7 @@ class AccountScreen extends StatelessWidget {
return AppScaffold( return AppScaffold(
noBackground: ResponsiveScaffold.getIsExpand(context), noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: AutoAppBarLeading(),
title: Text("screenAccount").tr(), title: Text("screenAccount").tr(),
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
? Stack( ? Stack(
@ -294,7 +295,12 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
leading: const Icon(Symbols.login), leading: const Icon(Symbols.login),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('authLogin'); GoRouter.of(context).pushNamed('authLogin').then((value) {
if (value == true && context.mounted) {
final ua = context.read<UserProvider>();
ua.refreshUser();
}
});
}, },
), ),
ListTile( ListTile(

View File

@ -1,21 +1,19 @@
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:path/path.dart' show withoutExtension;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/attachment.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_zoom.dart';
import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class AlbumScreen extends StatefulWidget { class AlbumScreen extends StatefulWidget {
const AlbumScreen({super.key}); const AlbumScreen({super.key});
@ -50,8 +48,6 @@ class _AlbumScreenState extends State<AlbumScreen> {
Future<void> _fetchAttachments() async { Future<void> _fetchAttachments() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final ua = context.read<UserProvider>();
const uuid = Uuid(); const uuid = Uuid();
try { try {
@ -59,11 +55,10 @@ class _AlbumScreenState extends State<AlbumScreen> {
final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: { final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: {
'take': 10, 'take': 10,
'offset': _attachments.length, 'offset': _attachments.length,
'author': ua.user?.name,
}); });
final attachments = List<SnAttachment>.from( final attachments = List<SnAttachment>.from(
resp.data['data']?.map((e) => SnAttachment.fromJson(e)) ?? [], resp.data['data']?.map((e) => SnAttachment.fromJson(e)) ?? [],
); ).where((e) => e.mimetype.startsWith('image')).toList();
_attachments.addAll(attachments); _attachments.addAll(attachments);
_heroTags.addAll(_attachments.map((_) => uuid.v4())); _heroTags.addAll(_attachments.map((_) => uuid.v4()));
@ -102,14 +97,15 @@ class _AlbumScreenState extends State<AlbumScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
appBar: AppBar( body: CustomScrollView(
controller: _scrollController,
slivers: [
SliverAppBar(
leading: PageBackButton(), leading: PageBackButton(),
title: Text('screenAlbum').tr(), title: Text('screenAlbum').tr(),
), ),
body: Column( SliverToBoxAdapter(
children: [ child: Card(
Card(
margin: EdgeInsets.zero,
child: Row( child: Row(
children: [ children: [
SizedBox( SizedBox(
@ -129,7 +125,8 @@ class _AlbumScreenState extends State<AlbumScreen> {
children: [ children: [
Text('attachmentBillingUploaded').tr().bold(), Text('attachmentBillingUploaded').tr().bold(),
Text( Text(
(_billing?.currentBytes ?? 0).formatBytes(decimals: 4), (_billing?.currentBytes ?? 0)
.formatBytes(decimals: 4),
style: GoogleFonts.robotoMono(), style: GoogleFonts.robotoMono(),
), ),
Text('attachmentBillingDiscount').tr().bold(), Text('attachmentBillingDiscount').tr().bold(),
@ -149,82 +146,47 @@ class _AlbumScreenState extends State<AlbumScreen> {
), ),
], ],
).padding(horizontal: 24, vertical: 8), ).padding(horizontal: 24, vertical: 8),
).padding(horizontal: 8, top: 8), ),
Expanded( ),
child: InfiniteList( SliverMasonryGrid.extent(
padding: EdgeInsets.only(top: 8), childCount: _attachments.length,
itemCount: _attachments.length, maxCrossAxisExtent: 320,
isLoading: _isBusy, mainAxisSpacing: 4,
hasReachedMax: crossAxisSpacing: 4,
_totalCount != null && _attachments.length >= _totalCount!, itemBuilder: (context, idx) {
onFetchData: _fetchAttachments, final attachment = _attachments[idx];
itemBuilder: (context, index) { return GestureDetector(
final ele = _attachments[index]; child: ClipRRect(
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
child: AspectRatio( child: AspectRatio(
aspectRatio: (ele.data['ratio'] ?? 1).toDouble(), aspectRatio: attachment.metadata['ratio']?.toDouble() ?? 1,
child: AttachmentItem( child: AttachmentItem(
data: ele, data: attachment,
heroTag: _heroTags[index], heroTag: _heroTags[idx],
onZoom: () { ),
),
),
onTap: () {
context.pushTransparentRoute( context.pushTransparentRoute(
AttachmentZoomView( AttachmentZoomView(
data: [ele], data: [attachment],
heroTags: [_heroTags[idx]],
), ),
backgroundColor: Colors.black.withOpacity(0.7), backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true, rootNavigator: true,
); );
}, },
),
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(ele.name),
if (ele.alt != withoutExtension(ele.name))
Text(ele.alt),
Text(DateFormat().format(ele.createdAt)),
const Gap(4),
Text(ele.size.formatBytes()).fontSize(12),
],
).padding(horizontal: 16, vertical: 12),
),
Padding(
padding: EdgeInsets.only(left: 12, right: 12, top: 4),
child: IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
icon: const Icon(Symbols.info),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) => AttachmentZoomDetailPopup(
data: ele,
),
); );
}, },
), ),
if (_isBusy)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(24),
child: const CircularProgressIndicator(),
).center(),
), ),
], ],
), ),
],
);
},
separatorBuilder: (_, __) => const Gap(8),
),
)
],
),
); );
} }
} }

View File

@ -160,7 +160,6 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
sn.setTokenPair(atk, rtk); sn.setTokenPair(atk, rtk);
if (!mounted) return; if (!mounted) return;
final user = context.read<UserProvider>(); final user = context.read<UserProvider>();
user.isAuthorized = true;
await user.refreshUser(); await user.refreshUser();
if (!mounted) return; if (!mounted) return;
final ws = context.read<WebSocketProvider>(); final ws = context.read<WebSocketProvider>();

View File

@ -41,7 +41,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
return; return;
} }
final captchaTk = await Navigator.of(context).push( final captchaTk = await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => CaptchaScreen(), builder: (context) => CaptchaScreen(),
), ),

View File

@ -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';

View File

@ -1,4 +1,3 @@
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html; import 'dart:html' as html;
import 'dart:ui_web' as ui; import 'dart:ui_web' as ui;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -33,7 +32,7 @@ class _CaptchaScreenState extends State<CaptchaScreen> {
}); });
final iframe = html.IFrameElement() final iframe = html.IFrameElement()
..src = '${context.read<ConfigProvider>().serverUrl}/captcha' ..src = '${context.read<ConfigProvider>().serverUrl}/captcha?redirect_uri=web'
..style.border = 'none' ..style.border = 'none'
..width = '100%' ..width = '100%'
..height = '100%'; ..height = '100%';

View 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();
}
}

View File

@ -1,20 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:jitsi_meet_flutter_sdk/jitsi_meet_flutter_sdk.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/chat_message_controller.dart'; import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/controllers/post_write_controller.dart'; import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/notification.dart'; import 'package:surface/providers/notification.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
@ -22,13 +21,13 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart'; import 'package:surface/providers/websocket.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/types/websocket.dart'; import 'package:surface/types/websocket.dart';
import 'package:surface/widgets/chat/call/call_prejoin.dart';
import 'package:surface/widgets/chat/chat_message.dart'; import 'package:surface/widgets/chat/chat_message.dart';
import 'package:surface/widgets/chat/chat_message_input.dart'; import 'package:surface/widgets/chat/chat_message_input.dart';
import 'package:surface/widgets/chat/chat_typing_indicator.dart'; import 'package:surface/widgets/chat/chat_typing_indicator.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class ChatRoomScreenExtra { class ChatRoomScreenExtra {
@ -52,11 +51,13 @@ class ChatRoomScreen extends StatefulWidget {
class _ChatRoomScreenState extends State<ChatRoomScreen> { class _ChatRoomScreenState extends State<ChatRoomScreen> {
bool _isBusy = false; bool _isBusy = false;
bool _isCalling = false;
bool _isJoining = false; bool _isJoining = false;
SnChannel? _channel; SnChannel? _channel;
SnChannelMember? _currentMember; SnChannelMember? _currentMember;
SnChannelMember? _otherMember; SnChannelMember? _otherMember;
SnChatCall? _ongoingCall;
final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey(); final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey();
late final ChatMessageController _messageController; late final ChatMessageController _messageController;
@ -138,35 +139,88 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
} }
} }
Future<void> _joinCall() async { Future<void> _fetchOngoingCall() async {
if (kIsWeb || !(Platform.isIOS || Platform.isAndroid)) { setState(() => _isCalling = true);
return await _joinCallWeb();
} try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>(); final resp = await sn.client.get(
final meet = JitsiMeet(); '/cgi/im/channels/${_messageController.channel!.keyPath}/calls/ongoing',
final confOpts = JitsiMeetConferenceOptions( options: Options(
room: 'sn-chat-${_channel!.alias}-${_channel!.id}', validateStatus: (status) => status != null && status < 500,
serverURL: 'https://meet.element.io', receiveTimeout: const Duration(seconds: 60),
configOverrides: { sendTimeout: const Duration(seconds: 60),
"subject": _channel!.name,
},
userInfo: JitsiMeetUserInfo(
avatar: ua.user!.avatar.isNotEmpty
? sn.getAttachmentUrl(ua.user!.avatar)
: null,
displayName: _currentMember!.nick ?? ua.user!.nick,
), ),
); );
meet.join(confOpts); 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 sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>(); await sn.client.post(
final url = '/cgi/im/channels/${_messageController.channel!.keyPath}/calls',
'${sn.client.options.baseUrl}/meet/${_channel!.alias}-${_channel!.id}?tk=${await ua.atk}'; options: Options(
launchUrlString(url); 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) { 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(); super.initState();
_messageController = ChatMessageController(context); _messageController = ChatMessageController(context);
_initializeChat(); _initializeChat();
_wsSubscription = _ws.pk.stream.listen((event) {
switch (event.method) {
case 'calls.new':
final payload = SnChatCall.fromJson(event.payload!);
if (payload.channelId == _channel?.id) {
setState(() => _ongoingCall = payload);
}
break;
case 'calls.end':
final payload = SnChatCall.fromJson(event.payload!);
if (payload.channelId == _channel?.id) {
setState(() => _ongoingCall = null);
}
break;
}
});
} }
@override @override
@ -226,6 +300,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final call = context.watch<ChatCallProvider>();
final ud = context.read<UserDirectoryProvider>(); final ud = context.read<UserDirectoryProvider>();
return AppScaffold( return AppScaffold(
@ -249,9 +324,14 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
), ),
if (_currentMember != null) if (_currentMember != null)
IconButton( IconButton(
icon: const Icon(Symbols.video_call), icon: _ongoingCall == null
onPressed: _joinCall, ? const Icon(Symbols.call)
onLongPress: _joinCallWeb, : const Icon(Symbols.call_end),
onPressed: _isCalling
? null
: _ongoingCall == null
? _makeCall
: _endCall,
), ),
IconButton( IconButton(
icon: const Icon(Symbols.more_vert), icon: const Icon(Symbols.more_vert),
@ -279,6 +359,28 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
LoadingIndicator( LoadingIndicator(
isActive: _isBusy || _messageController.isAggressiveLoading, isActive: _isBusy || _messageController.isAggressiveLoading,
), ),
SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: MaterialBanner(
dividerColor: Colors.transparent,
leading: const Icon(Symbols.call_received),
content: Text('callOngoingNotice').tr().padding(top: 2),
actions: [
if (call.current == null)
TextButton(
onPressed: _onCallJoin,
child: Text('callJoin').tr(),
)
else if (call.current?.channelId == _channel?.id)
TextButton(
onPressed: _onCallResume,
child: Text('callResume').tr(),
)
],
),
).height(_ongoingCall != null ? 54 : 0, animate: true).animate(
const Duration(milliseconds: 300),
Curves.fastLinearToSlowEaseIn),
if (_currentMember == null && !_isBusy) if (_currentMember == null && !_isBusy)
Expanded( Expanded(
child: Center( child: Center(

View File

@ -17,9 +17,10 @@ import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/feed/feed_reader.dart'; import 'package:surface/widgets/feed/feed_news.dart';
import 'package:surface/widgets/feed/feed_unknown.dart'; import 'package:surface/widgets/feed/feed_unknown.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/fediverse_post_item.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -465,7 +466,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
final pt = context.read<SnPostContentProvider>(); final pt = context.read<SnPostContentProvider>();
final result = await pt.getFeed( final result = await pt.getFeed(
cursor: _feed cursor: _feed
.where((ele) => !['reader.feed'].contains(ele.type)) .where((ele) => !['reader.news'].contains(ele.type))
.lastOrNull .lastOrNull
?.createdAt, ?.createdAt,
); );
@ -548,7 +549,12 @@ class _PostListWidgetState extends State<_PostListWidget> {
refreshPosts(); refreshPosts();
}, },
); );
case 'reader.feed': case 'fediverse.post':
return FediversePostWidget(
data: SnFediversePost.fromJson(ele.data),
maxWidth: 640,
);
case 'reader.news':
return Center( return Center(
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: 640), constraints: BoxConstraints(maxWidth: 640),

View File

@ -518,7 +518,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
} }
Future<void> _doCheckIn() async { Future<void> _doCheckIn() async {
final captchaTk = await Navigator.of(context).push( final captchaTk = await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => CaptchaScreen(), builder: (context) => CaptchaScreen(),
), ),

View File

@ -14,23 +14,23 @@ import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class ReaderPageScreen extends StatefulWidget { class NewsDetailScreen extends StatefulWidget {
final String id; final String hash;
const ReaderPageScreen({super.key, required this.id}); const NewsDetailScreen({super.key, required this.hash});
@override @override
State<ReaderPageScreen> createState() => _ReaderPageScreenState(); State<NewsDetailScreen> createState() => _NewsDetailScreenState();
} }
class _ReaderPageScreenState extends State<ReaderPageScreen> { class _NewsDetailScreenState extends State<NewsDetailScreen> {
SnSubscriptionItem? _article; SnNewsArticle? _article;
Future<void> _fetchArticle() async { Future<void> _fetchArticle() async {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/re/subscriptions/${widget.id}'); final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
_article = SnSubscriptionItem.fromJson(resp.data); _article = SnNewsArticle.fromJson(resp.data);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err).then((_) { context.showErrorDialog(err).then((_) {

View 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),
],
),
),
);
},
),
),
),
),
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
@ -15,6 +16,7 @@ import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/post_write_controller.dart'; import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart'; import 'package:surface/providers/sn_realm.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
@ -23,7 +25,8 @@ import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/attachment/attachment_input.dart'; import 'package:surface/widgets/attachment/attachment_input.dart';
import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/attachment/pending_attachment_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/loading_indicator.dart';
import 'package:surface/widgets/markdown_content.dart'; import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
@ -455,9 +458,7 @@ class _PostEditorScreenState extends State<PostEditorScreen>
isBusy: _writeController.isBusy, isBusy: _writeController.isBusy,
onUpload: (int idx) async { onUpload: (int idx) async {
await _writeController.uploadSingleAttachment( await _writeController.uploadSingleAttachment(
context, context, idx);
idx,
);
}, },
onInsertLink: (int idx) async { onInsertLink: (int idx) async {
_writeController.contentController.text += _writeController.contentController.text +=
@ -1105,7 +1106,7 @@ class _PostQuestionEditor extends StatelessWidget {
} }
} }
class _PostVideoEditor extends StatefulWidget { class _PostVideoEditor extends StatelessWidget {
final PostWriteController controller; final PostWriteController controller;
final Function? onTapPublisher; final Function? onTapPublisher;
final Function? onTapRealm; final Function? onTapRealm;
@ -1113,14 +1114,7 @@ class _PostVideoEditor extends StatefulWidget {
const _PostVideoEditor( const _PostVideoEditor(
{required this.controller, this.onTapPublisher, this.onTapRealm}); {required this.controller, this.onTapPublisher, this.onTapRealm});
@override void _selectVideo(BuildContext context) async {
State<_PostVideoEditor> createState() => _PostVideoEditorState();
}
class _PostVideoEditorState extends State<_PostVideoEditor> {
final TextEditingController _streamUrlController = TextEditingController();
void _selectVideo() async {
final video = await showDialog<SnAttachment?>( final video = await showDialog<SnAttachment?>(
context: context, context: context,
builder: (context) => AttachmentInputDialog( builder: (context) => AttachmentInputDialog(
@ -1131,25 +1125,78 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
); );
if (!context.mounted) return; if (!context.mounted) return;
if (video == null) return; if (video == null) return;
widget.controller.setVideoAttachment(video); controller.setVideoAttachment(video);
} }
@override void _setAlt(BuildContext context) async {
void initState() { if (controller.videoAttachment == null) return;
_streamUrlController.addListener(() {
if (_streamUrlController.text.isEmpty) { final result = await showDialog<SnAttachment?>(
widget.controller.setVideoUrl(''); context: context,
} else { builder: (context) => PendingAttachmentAltDialog(
widget.controller.setVideoUrl(_streamUrlController.text); media: PostWriteMedia(controller.videoAttachment)),
} );
}); if (result == null) return;
super.initState();
controller.setVideoAttachment(result);
} }
@override Future<void> _createBoost(BuildContext context) async {
void dispose() { if (controller.videoAttachment == null) return;
_streamUrlController.dispose();
super.dispose(); 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 @override
@ -1167,10 +1214,10 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
borderRadius: const BorderRadius.all(Radius.circular(24)), borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
widget.onTapPublisher?.call(); onTapPublisher?.call();
}, },
child: AccountImage( 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)), borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
widget.onTapRealm?.call(); onTapRealm?.call();
}, },
child: AccountImage( child: AccountImage(
content: widget.controller.realm?.avatar, content: controller.realm?.avatar,
fallbackWidget: const Icon(Symbols.globe, size: 20), fallbackWidget: const Icon(Symbols.globe, size: 20),
radius: 14, radius: 14,
), ),
@ -1196,7 +1243,7 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
children: [ children: [
const Gap(6), const Gap(6),
TextField( TextField(
controller: widget.controller.titleController, controller: controller.titleController,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
hintText: 'fieldPostTitle'.tr(), hintText: 'fieldPostTitle'.tr(),
border: InputBorder.none, border: InputBorder.none,
@ -1207,7 +1254,7 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
).padding(horizontal: 16), ).padding(horizontal: 16),
const Gap(8), const Gap(8),
TextField( TextField(
controller: widget.controller.descriptionController, controller: controller.descriptionController,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
hintText: 'fieldPostDescription'.tr(), hintText: 'fieldPostDescription'.tr(),
border: InputBorder.none, border: InputBorder.none,
@ -1219,52 +1266,66 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16), ).padding(horizontal: 16),
const Gap(12), 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( Container(
margin: const EdgeInsets.only(left: 16, right: 16), margin: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).dividerColor), border: Border.all(color: Theme.of(context).dividerColor),
), ),
child: ContextMenuRegion(
contextMenu: ContextMenu(
entries: [
MenuItem(
label: 'attachmentSetAlt'.tr(),
icon: Symbols.description,
onSelected: () {
_setAlt(context);
},
),
MenuItem(
label: 'attachmentBoost'.tr(),
icon: Symbols.bolt,
onSelected: () {
_createBoost(context);
},
),
MenuItem(
label: 'attachmentSetThumbnail'.tr(),
icon: Symbols.image,
onSelected: () {
_setThumbnail(context);
},
),
MenuItem(
label: 'attachmentCopyRandomId'.tr(),
icon: Symbols.content_copy,
onSelected: () {
Clipboard.setData(ClipboardData(
text: controller.videoAttachment!.rid));
},
),
MenuItem(
label: 'delete'.tr(),
icon: Symbols.delete,
onSelected: () => _deleteAttachment(context),
),
MenuItem(
label: 'unlink'.tr(),
icon: Symbols.link_off,
onSelected: () {
controller.setVideoAttachment(null);
},
),
],
),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
onTap: widget.controller.videoAttachment == null onTap: controller.videoAttachment == null
? () => _selectVideo() ? () => _selectVideo(context)
: () { : null,
showModalBottomSheet(
context: context,
builder: (context) =>
PendingAttachmentActionSheet(
media: PostWriteMedia(
widget.controller.videoAttachment!,
),
),
).then((value) async {
if (value is PostWriteMedia) {
widget.controller
.setVideoAttachment(value.attachment);
} else if (value == false) {
widget.controller.setVideoAttachment(null);
}
});
},
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: widget.controller.videoAttachment == null child: controller.videoAttachment == null
? Center( ? Center(
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -1280,21 +1341,13 @@ class _PostVideoEditorState extends State<_PostVideoEditor> {
: ClipRRect( : ClipRRect(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: AttachmentItem( child: AttachmentItem(
data: widget.controller.videoAttachment!, data: controller.videoAttachment!,
heroTag: const Uuid().v4(), heroTag: const Uuid().v4(),
), ),
), ),
), ),
), ),
), ),
const Gap(8),
CheckboxListTile(
secondary: const Icon(Symbols.live_tv),
title: Text('postVideoLive').tr(),
subtitle: Text('postVideoLiveDescription').tr(),
value: widget.controller.videoLive,
onChanged: (value) =>
widget.controller.setVideoLive(value ?? false),
), ),
], ],
), ),

View File

@ -303,7 +303,6 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
), ),
child: SliverAppBar( child: SliverAppBar(
expandedHeight: _appBarHeight, expandedHeight: _appBarHeight,
leading: const PageBackButton(),
title: _publisher == null title: _publisher == null
? Text('loading').tr() ? Text('loading').tr()
: RichText( : RichText(

View File

@ -30,7 +30,6 @@ abstract class SnAttachment with _$SnAttachment {
required String hash, required String hash,
required int destination, required int destination,
required int refCount, required int refCount,
String? refUrl,
@Default(0) int contentRating, @Default(0) int contentRating,
@Default(0) int qualityRating, @Default(0) int qualityRating,
required DateTime? cleanedAt, required DateTime? cleanedAt,

View File

@ -28,7 +28,6 @@ mixin _$SnAttachment {
String get hash; String get hash;
int get destination; int get destination;
int get refCount; int get refCount;
String? get refUrl;
int get contentRating; int get contentRating;
int get qualityRating; int get qualityRating;
DateTime? get cleanedAt; DateTime? get cleanedAt;
@ -84,7 +83,6 @@ mixin _$SnAttachment {
other.destination == destination) && other.destination == destination) &&
(identical(other.refCount, refCount) || (identical(other.refCount, refCount) ||
other.refCount == refCount) && other.refCount == refCount) &&
(identical(other.refUrl, refUrl) || other.refUrl == refUrl) &&
(identical(other.contentRating, contentRating) || (identical(other.contentRating, contentRating) ||
other.contentRating == contentRating) && other.contentRating == contentRating) &&
(identical(other.qualityRating, qualityRating) || (identical(other.qualityRating, qualityRating) ||
@ -134,7 +132,6 @@ mixin _$SnAttachment {
hash, hash,
destination, destination,
refCount, refCount,
refUrl,
contentRating, contentRating,
qualityRating, qualityRating,
cleanedAt, cleanedAt,
@ -158,7 +155,7 @@ mixin _$SnAttachment {
@override @override
String toString() { String toString() {
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, 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, String hash,
int destination, int destination,
int refCount, int refCount,
String? refUrl,
int contentRating, int contentRating,
int qualityRating, int qualityRating,
DateTime? cleanedAt, DateTime? cleanedAt,
@ -235,7 +231,6 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
Object? hash = null, Object? hash = null,
Object? destination = null, Object? destination = null,
Object? refCount = null, Object? refCount = null,
Object? refUrl = freezed,
Object? contentRating = null, Object? contentRating = null,
Object? qualityRating = null, Object? qualityRating = null,
Object? cleanedAt = freezed, Object? cleanedAt = freezed,
@ -309,10 +304,6 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
? _self.refCount ? _self.refCount
: refCount // ignore: cast_nullable_to_non_nullable : refCount // ignore: cast_nullable_to_non_nullable
as int, as int,
refUrl: freezed == refUrl
? _self.refUrl
: refUrl // ignore: cast_nullable_to_non_nullable
as String?,
contentRating: null == contentRating contentRating: null == contentRating
? _self.contentRating ? _self.contentRating
: contentRating // ignore: cast_nullable_to_non_nullable : contentRating // ignore: cast_nullable_to_non_nullable
@ -480,7 +471,6 @@ class _SnAttachment extends SnAttachment {
required this.hash, required this.hash,
required this.destination, required this.destination,
required this.refCount, required this.refCount,
this.refUrl,
this.contentRating = 0, this.contentRating = 0,
this.qualityRating = 0, this.qualityRating = 0,
required this.cleanedAt, required this.cleanedAt,
@ -534,8 +524,6 @@ class _SnAttachment extends SnAttachment {
@override @override
final int refCount; final int refCount;
@override @override
final String? refUrl;
@override
@JsonKey() @JsonKey()
final int contentRating; final int contentRating;
@override @override
@ -635,7 +623,6 @@ class _SnAttachment extends SnAttachment {
other.destination == destination) && other.destination == destination) &&
(identical(other.refCount, refCount) || (identical(other.refCount, refCount) ||
other.refCount == refCount) && other.refCount == refCount) &&
(identical(other.refUrl, refUrl) || other.refUrl == refUrl) &&
(identical(other.contentRating, contentRating) || (identical(other.contentRating, contentRating) ||
other.contentRating == contentRating) && other.contentRating == contentRating) &&
(identical(other.qualityRating, qualityRating) || (identical(other.qualityRating, qualityRating) ||
@ -685,7 +672,6 @@ class _SnAttachment extends SnAttachment {
hash, hash,
destination, destination,
refCount, refCount,
refUrl,
contentRating, contentRating,
qualityRating, qualityRating,
cleanedAt, cleanedAt,
@ -709,7 +695,7 @@ class _SnAttachment extends SnAttachment {
@override @override
String toString() { String toString() {
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, 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, String hash,
int destination, int destination,
int refCount, int refCount,
String? refUrl,
int contentRating, int contentRating,
int qualityRating, int qualityRating,
DateTime? cleanedAt, DateTime? cleanedAt,
@ -794,7 +779,6 @@ class __$SnAttachmentCopyWithImpl<$Res>
Object? hash = null, Object? hash = null,
Object? destination = null, Object? destination = null,
Object? refCount = null, Object? refCount = null,
Object? refUrl = freezed,
Object? contentRating = null, Object? contentRating = null,
Object? qualityRating = null, Object? qualityRating = null,
Object? cleanedAt = freezed, Object? cleanedAt = freezed,
@ -868,10 +852,6 @@ class __$SnAttachmentCopyWithImpl<$Res>
? _self.refCount ? _self.refCount
: refCount // ignore: cast_nullable_to_non_nullable : refCount // ignore: cast_nullable_to_non_nullable
as int, as int,
refUrl: freezed == refUrl
? _self.refUrl
: refUrl // ignore: cast_nullable_to_non_nullable
as String?,
contentRating: null == contentRating contentRating: null == contentRating
? _self.contentRating ? _self.contentRating
: contentRating // ignore: cast_nullable_to_non_nullable : contentRating // ignore: cast_nullable_to_non_nullable

View File

@ -23,7 +23,6 @@ _SnAttachment _$SnAttachmentFromJson(Map<String, dynamic> json) =>
hash: json['hash'] as String, hash: json['hash'] as String,
destination: (json['destination'] as num).toInt(), destination: (json['destination'] as num).toInt(),
refCount: (json['ref_count'] as num).toInt(), refCount: (json['ref_count'] as num).toInt(),
refUrl: json['ref_url'] as String?,
contentRating: (json['content_rating'] as num?)?.toInt() ?? 0, contentRating: (json['content_rating'] as num?)?.toInt() ?? 0,
qualityRating: (json['quality_rating'] as num?)?.toInt() ?? 0, qualityRating: (json['quality_rating'] as num?)?.toInt() ?? 0,
cleanedAt: json['cleaned_at'] == null cleanedAt: json['cleaned_at'] == null
@ -76,7 +75,6 @@ Map<String, dynamic> _$SnAttachmentToJson(_SnAttachment instance) =>
'hash': instance.hash, 'hash': instance.hash,
'destination': instance.destination, 'destination': instance.destination,
'ref_count': instance.refCount, 'ref_count': instance.refCount,
'ref_url': instance.refUrl,
'content_rating': instance.contentRating, 'content_rating': instance.contentRating,
'quality_rating': instance.qualityRating, 'quality_rating': instance.qualityRating,
'cleaned_at': instance.cleanedAt?.toIso8601String(), 'cleaned_at': instance.cleanedAt?.toIso8601String(),

View File

@ -1,4 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:surface/types/account.dart'; import 'package:surface/types/account.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
@ -115,3 +116,24 @@ abstract class SnChatCall with _$SnChatCall {
factory SnChatCall.fromJson(Map<String, dynamic> json) => factory SnChatCall.fromJson(Map<String, dynamic> json) =>
_$SnChatCallFromJson(json); _$SnChatCallFromJson(json);
} }
// Call stuff
enum ParticipantStatsType {
unknown,
localAudioSender,
localVideoSender,
remoteAudioReceiver,
remoteVideoReceiver,
}
class ParticipantTrack {
ParticipantTrack(
{required this.participant,
required this.videoTrack,
required this.isScreenShare});
VideoTrack? videoTrack;
Participant participant;
bool isScreenShare;
}

View File

@ -4,43 +4,35 @@ part 'news.freezed.dart';
part 'news.g.dart'; part 'news.g.dart';
@freezed @freezed
abstract class SnSubscriptionFeed with _$SnSubscriptionFeed { abstract class SnNewsSource with _$SnNewsSource {
const factory SnSubscriptionFeed({ const factory SnNewsSource({
required int id, required String id,
required DateTime createdAt, required String label,
required DateTime updatedAt, required String type,
required DateTime? deletedAt, required String source,
required String url, required int depth,
required bool isEnabled, required bool enabled,
required bool isFullContent, }) = _SnNewsSource;
required int pullInterval,
required String adapter,
required int? accountId,
required DateTime? lastFetchedAt,
}) = _SnSubscriptionFeed;
factory SnSubscriptionFeed.fromJson(Map<String, dynamic> json) => factory SnNewsSource.fromJson(Map<String, dynamic> json) => _$SnNewsSourceFromJson(json);
_$SnSubscriptionFeedFromJson(json);
} }
@freezed @freezed
abstract class SnSubscriptionItem with _$SnSubscriptionItem { abstract class SnNewsArticle with _$SnNewsArticle {
const factory SnSubscriptionItem({ const factory SnNewsArticle({
required int id, required int id,
required DateTime createdAt, required DateTime createdAt,
required DateTime updatedAt, required DateTime updatedAt,
required DateTime? deletedAt, required dynamic deletedAt,
required String thumbnail, required String thumbnail,
required String title, required String title,
required String description, required String description,
required String content, required String content,
required String url, required String url,
required String hash, required String hash,
required int feedId, required String source,
required SnSubscriptionFeed feed,
required DateTime? publishedAt, required DateTime? publishedAt,
}) = _SnSubscriptionItem; }) = _SnNewsArticle;
factory SnSubscriptionItem.fromJson(Map<String, dynamic> json) => factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json);
_$SnSubscriptionItemFromJson(json);
} }

View File

@ -14,224 +14,149 @@ part of 'news.dart';
T _$identity<T>(T value) => value; T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$SnSubscriptionFeed { mixin _$SnNewsSource {
int get id; String get id;
DateTime get createdAt; String get label;
DateTime get updatedAt; String get type;
DateTime? get deletedAt; String get source;
String get url; int get depth;
bool get isEnabled; bool get enabled;
bool get isFullContent;
int get pullInterval;
String get adapter;
int? get accountId;
DateTime? get lastFetchedAt;
/// Create a copy of SnSubscriptionFeed /// Create a copy of SnNewsSource
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$SnSubscriptionFeedCopyWith<SnSubscriptionFeed> get copyWith => $SnNewsSourceCopyWith<SnNewsSource> get copyWith =>
_$SnSubscriptionFeedCopyWithImpl<SnSubscriptionFeed>( _$SnNewsSourceCopyWithImpl<SnNewsSource>(
this as SnSubscriptionFeed, _$identity); this as SnNewsSource, _$identity);
/// Serializes this SnSubscriptionFeed to a JSON map. /// Serializes this SnNewsSource to a JSON map.
Map<String, dynamic> toJson(); Map<String, dynamic> toJson();
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is SnSubscriptionFeed && other is SnNewsSource &&
(identical(other.id, id) || other.id == id) && (identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) || (identical(other.label, label) || other.label == label) &&
other.createdAt == createdAt) && (identical(other.type, type) || other.type == type) &&
(identical(other.updatedAt, updatedAt) || (identical(other.source, source) || other.source == source) &&
other.updatedAt == updatedAt) && (identical(other.depth, depth) || other.depth == depth) &&
(identical(other.deletedAt, deletedAt) || (identical(other.enabled, enabled) || other.enabled == enabled));
other.deletedAt == deletedAt) &&
(identical(other.url, url) || other.url == url) &&
(identical(other.isEnabled, isEnabled) ||
other.isEnabled == isEnabled) &&
(identical(other.isFullContent, isFullContent) ||
other.isFullContent == isFullContent) &&
(identical(other.pullInterval, pullInterval) ||
other.pullInterval == pullInterval) &&
(identical(other.adapter, adapter) || other.adapter == adapter) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId) &&
(identical(other.lastFetchedAt, lastFetchedAt) ||
other.lastFetchedAt == lastFetchedAt));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode =>
runtimeType, Object.hash(runtimeType, id, label, type, source, depth, enabled);
id,
createdAt,
updatedAt,
deletedAt,
url,
isEnabled,
isFullContent,
pullInterval,
adapter,
accountId,
lastFetchedAt);
@override @override
String toString() { 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 /// @nodoc
abstract mixin class $SnSubscriptionFeedCopyWith<$Res> { abstract mixin class $SnNewsSourceCopyWith<$Res> {
factory $SnSubscriptionFeedCopyWith( factory $SnNewsSourceCopyWith(
SnSubscriptionFeed value, $Res Function(SnSubscriptionFeed) _then) = SnNewsSource value, $Res Function(SnNewsSource) _then) =
_$SnSubscriptionFeedCopyWithImpl; _$SnNewsSourceCopyWithImpl;
@useResult @useResult
$Res call( $Res call(
{int id, {String id,
DateTime createdAt, String label,
DateTime updatedAt, String type,
DateTime? deletedAt, String source,
String url, int depth,
bool isEnabled, bool enabled});
bool isFullContent,
int pullInterval,
String adapter,
int? accountId,
DateTime? lastFetchedAt});
} }
/// @nodoc /// @nodoc
class _$SnSubscriptionFeedCopyWithImpl<$Res> class _$SnNewsSourceCopyWithImpl<$Res> implements $SnNewsSourceCopyWith<$Res> {
implements $SnSubscriptionFeedCopyWith<$Res> { _$SnNewsSourceCopyWithImpl(this._self, this._then);
_$SnSubscriptionFeedCopyWithImpl(this._self, this._then);
final SnSubscriptionFeed _self; final SnNewsSource _self;
final $Res Function(SnSubscriptionFeed) _then; 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. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? id = null, Object? id = null,
Object? createdAt = null, Object? label = null,
Object? updatedAt = null, Object? type = null,
Object? deletedAt = freezed, Object? source = null,
Object? url = null, Object? depth = null,
Object? isEnabled = null, Object? enabled = null,
Object? isFullContent = null,
Object? pullInterval = null,
Object? adapter = null,
Object? accountId = freezed,
Object? lastFetchedAt = freezed,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id id: null == id
? _self.id ? _self.id
: id // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
as 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, as String,
isEnabled: null == isEnabled label: null == label
? _self.isEnabled ? _self.label
: isEnabled // ignore: cast_nullable_to_non_nullable : label // ignore: cast_nullable_to_non_nullable
as bool,
isFullContent: null == isFullContent
? _self.isFullContent
: isFullContent // ignore: cast_nullable_to_non_nullable
as bool,
pullInterval: null == pullInterval
? _self.pullInterval
: pullInterval // ignore: cast_nullable_to_non_nullable
as int,
adapter: null == adapter
? _self.adapter
: adapter // ignore: cast_nullable_to_non_nullable
as String, as String,
accountId: freezed == accountId type: null == type
? _self.accountId ? _self.type
: accountId // ignore: cast_nullable_to_non_nullable : type // ignore: cast_nullable_to_non_nullable
as int?, as String,
lastFetchedAt: freezed == lastFetchedAt source: null == source
? _self.lastFetchedAt ? _self.source
: lastFetchedAt // ignore: cast_nullable_to_non_nullable : source // ignore: cast_nullable_to_non_nullable
as DateTime?, 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 /// @nodoc
@JsonSerializable() @JsonSerializable()
class _SnSubscriptionFeed implements SnSubscriptionFeed { class _SnNewsSource implements SnNewsSource {
const _SnSubscriptionFeed( const _SnNewsSource(
{required this.id, {required this.id,
required this.createdAt, required this.label,
required this.updatedAt, required this.type,
required this.deletedAt, required this.source,
required this.url, required this.depth,
required this.isEnabled, required this.enabled});
required this.isFullContent, factory _SnNewsSource.fromJson(Map<String, dynamic> json) =>
required this.pullInterval, _$SnNewsSourceFromJson(json);
required this.adapter,
required this.accountId,
required this.lastFetchedAt});
factory _SnSubscriptionFeed.fromJson(Map<String, dynamic> json) =>
_$SnSubscriptionFeedFromJson(json);
@override @override
final int id; final String id;
@override @override
final DateTime createdAt; final String label;
@override @override
final DateTime updatedAt; final String type;
@override @override
final DateTime? deletedAt; final String source;
@override @override
final String url; final int depth;
@override @override
final bool isEnabled; final bool enabled;
@override
final bool isFullContent;
@override
final int pullInterval;
@override
final String adapter;
@override
final int? accountId;
@override
final DateTime? lastFetchedAt;
/// Create a copy of SnSubscriptionFeed /// Create a copy of SnNewsSource
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$SnSubscriptionFeedCopyWith<_SnSubscriptionFeed> get copyWith => _$SnNewsSourceCopyWith<_SnNewsSource> get copyWith =>
__$SnSubscriptionFeedCopyWithImpl<_SnSubscriptionFeed>(this, _$identity); __$SnNewsSourceCopyWithImpl<_SnNewsSource>(this, _$identity);
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return _$SnSubscriptionFeedToJson( return _$SnNewsSourceToJson(
this, this,
); );
} }
@ -240,185 +165,129 @@ class _SnSubscriptionFeed implements SnSubscriptionFeed {
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _SnSubscriptionFeed && other is _SnNewsSource &&
(identical(other.id, id) || other.id == id) && (identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) || (identical(other.label, label) || other.label == label) &&
other.createdAt == createdAt) && (identical(other.type, type) || other.type == type) &&
(identical(other.updatedAt, updatedAt) || (identical(other.source, source) || other.source == source) &&
other.updatedAt == updatedAt) && (identical(other.depth, depth) || other.depth == depth) &&
(identical(other.deletedAt, deletedAt) || (identical(other.enabled, enabled) || other.enabled == enabled));
other.deletedAt == deletedAt) &&
(identical(other.url, url) || other.url == url) &&
(identical(other.isEnabled, isEnabled) ||
other.isEnabled == isEnabled) &&
(identical(other.isFullContent, isFullContent) ||
other.isFullContent == isFullContent) &&
(identical(other.pullInterval, pullInterval) ||
other.pullInterval == pullInterval) &&
(identical(other.adapter, adapter) || other.adapter == adapter) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId) &&
(identical(other.lastFetchedAt, lastFetchedAt) ||
other.lastFetchedAt == lastFetchedAt));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode =>
runtimeType, Object.hash(runtimeType, id, label, type, source, depth, enabled);
id,
createdAt,
updatedAt,
deletedAt,
url,
isEnabled,
isFullContent,
pullInterval,
adapter,
accountId,
lastFetchedAt);
@override @override
String toString() { 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 /// @nodoc
abstract mixin class _$SnSubscriptionFeedCopyWith<$Res> abstract mixin class _$SnNewsSourceCopyWith<$Res>
implements $SnSubscriptionFeedCopyWith<$Res> { implements $SnNewsSourceCopyWith<$Res> {
factory _$SnSubscriptionFeedCopyWith( factory _$SnNewsSourceCopyWith(
_SnSubscriptionFeed value, $Res Function(_SnSubscriptionFeed) _then) = _SnNewsSource value, $Res Function(_SnNewsSource) _then) =
__$SnSubscriptionFeedCopyWithImpl; __$SnNewsSourceCopyWithImpl;
@override @override
@useResult @useResult
$Res call( $Res call(
{int id, {String id,
DateTime createdAt, String label,
DateTime updatedAt, String type,
DateTime? deletedAt, String source,
String url, int depth,
bool isEnabled, bool enabled});
bool isFullContent,
int pullInterval,
String adapter,
int? accountId,
DateTime? lastFetchedAt});
} }
/// @nodoc /// @nodoc
class __$SnSubscriptionFeedCopyWithImpl<$Res> class __$SnNewsSourceCopyWithImpl<$Res>
implements _$SnSubscriptionFeedCopyWith<$Res> { implements _$SnNewsSourceCopyWith<$Res> {
__$SnSubscriptionFeedCopyWithImpl(this._self, this._then); __$SnNewsSourceCopyWithImpl(this._self, this._then);
final _SnSubscriptionFeed _self; final _SnNewsSource _self;
final $Res Function(_SnSubscriptionFeed) _then; 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. /// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$Res call({ $Res call({
Object? id = null, Object? id = null,
Object? createdAt = null, Object? label = null,
Object? updatedAt = null, Object? type = null,
Object? deletedAt = freezed, Object? source = null,
Object? url = null, Object? depth = null,
Object? isEnabled = null, Object? enabled = null,
Object? isFullContent = null,
Object? pullInterval = null,
Object? adapter = null,
Object? accountId = freezed,
Object? lastFetchedAt = freezed,
}) { }) {
return _then(_SnSubscriptionFeed( return _then(_SnNewsSource(
id: null == id id: null == id
? _self.id ? _self.id
: id // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
as 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, as String,
isEnabled: null == isEnabled label: null == label
? _self.isEnabled ? _self.label
: isEnabled // ignore: cast_nullable_to_non_nullable : label // ignore: cast_nullable_to_non_nullable
as bool,
isFullContent: null == isFullContent
? _self.isFullContent
: isFullContent // ignore: cast_nullable_to_non_nullable
as bool,
pullInterval: null == pullInterval
? _self.pullInterval
: pullInterval // ignore: cast_nullable_to_non_nullable
as int,
adapter: null == adapter
? _self.adapter
: adapter // ignore: cast_nullable_to_non_nullable
as String, as String,
accountId: freezed == accountId type: null == type
? _self.accountId ? _self.type
: accountId // ignore: cast_nullable_to_non_nullable : type // ignore: cast_nullable_to_non_nullable
as int?, as String,
lastFetchedAt: freezed == lastFetchedAt source: null == source
? _self.lastFetchedAt ? _self.source
: lastFetchedAt // ignore: cast_nullable_to_non_nullable : source // ignore: cast_nullable_to_non_nullable
as DateTime?, 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 /// @nodoc
mixin _$SnSubscriptionItem { mixin _$SnNewsArticle {
int get id; int get id;
DateTime get createdAt; DateTime get createdAt;
DateTime get updatedAt; DateTime get updatedAt;
DateTime? get deletedAt; dynamic get deletedAt;
String get thumbnail; String get thumbnail;
String get title; String get title;
String get description; String get description;
String get content; String get content;
String get url; String get url;
String get hash; String get hash;
int get feedId; String get source;
SnSubscriptionFeed get feed;
DateTime? get publishedAt; DateTime? get publishedAt;
/// Create a copy of SnSubscriptionItem /// Create a copy of SnNewsArticle
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$SnSubscriptionItemCopyWith<SnSubscriptionItem> get copyWith => $SnNewsArticleCopyWith<SnNewsArticle> get copyWith =>
_$SnSubscriptionItemCopyWithImpl<SnSubscriptionItem>( _$SnNewsArticleCopyWithImpl<SnNewsArticle>(
this as SnSubscriptionItem, _$identity); this as SnNewsArticle, _$identity);
/// Serializes this SnSubscriptionItem to a JSON map. /// Serializes this SnNewsArticle to a JSON map.
Map<String, dynamic> toJson(); Map<String, dynamic> toJson();
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is SnSubscriptionItem && other is SnNewsArticle &&
(identical(other.id, id) || other.id == id) && (identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) || (identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) && other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) || (identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) && other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) || const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
other.deletedAt == deletedAt) &&
(identical(other.thumbnail, thumbnail) || (identical(other.thumbnail, thumbnail) ||
other.thumbnail == thumbnail) && other.thumbnail == thumbnail) &&
(identical(other.title, title) || other.title == title) && (identical(other.title, title) || other.title == title) &&
@ -427,8 +296,7 @@ mixin _$SnSubscriptionItem {
(identical(other.content, content) || other.content == content) && (identical(other.content, content) || other.content == content) &&
(identical(other.url, url) || other.url == url) && (identical(other.url, url) || other.url == url) &&
(identical(other.hash, hash) || other.hash == hash) && (identical(other.hash, hash) || other.hash == hash) &&
(identical(other.feedId, feedId) || other.feedId == feedId) && (identical(other.source, source) || other.source == source) &&
(identical(other.feed, feed) || other.feed == feed) &&
(identical(other.publishedAt, publishedAt) || (identical(other.publishedAt, publishedAt) ||
other.publishedAt == publishedAt)); other.publishedAt == publishedAt));
} }
@ -440,56 +308,52 @@ mixin _$SnSubscriptionItem {
id, id,
createdAt, createdAt,
updatedAt, updatedAt,
deletedAt, const DeepCollectionEquality().hash(deletedAt),
thumbnail, thumbnail,
title, title,
description, description,
content, content,
url, url,
hash, hash,
feedId, source,
feed,
publishedAt); publishedAt);
@override @override
String toString() { 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 /// @nodoc
abstract mixin class $SnSubscriptionItemCopyWith<$Res> { abstract mixin class $SnNewsArticleCopyWith<$Res> {
factory $SnSubscriptionItemCopyWith( factory $SnNewsArticleCopyWith(
SnSubscriptionItem value, $Res Function(SnSubscriptionItem) _then) = SnNewsArticle value, $Res Function(SnNewsArticle) _then) =
_$SnSubscriptionItemCopyWithImpl; _$SnNewsArticleCopyWithImpl;
@useResult @useResult
$Res call( $Res call(
{int id, {int id,
DateTime createdAt, DateTime createdAt,
DateTime updatedAt, DateTime updatedAt,
DateTime? deletedAt, dynamic deletedAt,
String thumbnail, String thumbnail,
String title, String title,
String description, String description,
String content, String content,
String url, String url,
String hash, String hash,
int feedId, String source,
SnSubscriptionFeed feed,
DateTime? publishedAt}); DateTime? publishedAt});
$SnSubscriptionFeedCopyWith<$Res> get feed;
} }
/// @nodoc /// @nodoc
class _$SnSubscriptionItemCopyWithImpl<$Res> class _$SnNewsArticleCopyWithImpl<$Res>
implements $SnSubscriptionItemCopyWith<$Res> { implements $SnNewsArticleCopyWith<$Res> {
_$SnSubscriptionItemCopyWithImpl(this._self, this._then); _$SnNewsArticleCopyWithImpl(this._self, this._then);
final SnSubscriptionItem _self; final SnNewsArticle _self;
final $Res Function(SnSubscriptionItem) _then; 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. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
@ -504,8 +368,7 @@ class _$SnSubscriptionItemCopyWithImpl<$Res>
Object? content = null, Object? content = null,
Object? url = null, Object? url = null,
Object? hash = null, Object? hash = null,
Object? feedId = null, Object? source = null,
Object? feed = null,
Object? publishedAt = freezed, Object? publishedAt = freezed,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
@ -524,7 +387,7 @@ class _$SnSubscriptionItemCopyWithImpl<$Res>
deletedAt: freezed == deletedAt deletedAt: freezed == deletedAt
? _self.deletedAt ? _self.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as dynamic,
thumbnail: null == thumbnail thumbnail: null == thumbnail
? _self.thumbnail ? _self.thumbnail
: thumbnail // ignore: cast_nullable_to_non_nullable : thumbnail // ignore: cast_nullable_to_non_nullable
@ -549,36 +412,22 @@ class _$SnSubscriptionItemCopyWithImpl<$Res>
? _self.hash ? _self.hash
: hash // ignore: cast_nullable_to_non_nullable : hash // ignore: cast_nullable_to_non_nullable
as String, as String,
feedId: null == feedId source: null == source
? _self.feedId ? _self.source
: feedId // ignore: cast_nullable_to_non_nullable : source // ignore: cast_nullable_to_non_nullable
as int, as String,
feed: null == feed
? _self.feed
: feed // ignore: cast_nullable_to_non_nullable
as SnSubscriptionFeed,
publishedAt: freezed == publishedAt publishedAt: freezed == publishedAt
? _self.publishedAt ? _self.publishedAt
: publishedAt // ignore: cast_nullable_to_non_nullable : publishedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,
)); ));
} }
/// Create a copy of SnSubscriptionItem
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnSubscriptionFeedCopyWith<$Res> get feed {
return $SnSubscriptionFeedCopyWith<$Res>(_self.feed, (value) {
return _then(_self.copyWith(feed: value));
});
}
} }
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _SnSubscriptionItem implements SnSubscriptionItem { class _SnNewsArticle implements SnNewsArticle {
const _SnSubscriptionItem( const _SnNewsArticle(
{required this.id, {required this.id,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
@ -589,11 +438,10 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
required this.content, required this.content,
required this.url, required this.url,
required this.hash, required this.hash,
required this.feedId, required this.source,
required this.feed,
required this.publishedAt}); required this.publishedAt});
factory _SnSubscriptionItem.fromJson(Map<String, dynamic> json) => factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =>
_$SnSubscriptionItemFromJson(json); _$SnNewsArticleFromJson(json);
@override @override
final int id; final int id;
@ -602,7 +450,7 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
@override @override
final DateTime updatedAt; final DateTime updatedAt;
@override @override
final DateTime? deletedAt; final dynamic deletedAt;
@override @override
final String thumbnail; final String thumbnail;
@override @override
@ -616,23 +464,21 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
@override @override
final String hash; final String hash;
@override @override
final int feedId; final String source;
@override
final SnSubscriptionFeed feed;
@override @override
final DateTime? publishedAt; final DateTime? publishedAt;
/// Create a copy of SnSubscriptionItem /// Create a copy of SnNewsArticle
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$SnSubscriptionItemCopyWith<_SnSubscriptionItem> get copyWith => _$SnNewsArticleCopyWith<_SnNewsArticle> get copyWith =>
__$SnSubscriptionItemCopyWithImpl<_SnSubscriptionItem>(this, _$identity); __$SnNewsArticleCopyWithImpl<_SnNewsArticle>(this, _$identity);
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return _$SnSubscriptionItemToJson( return _$SnNewsArticleToJson(
this, this,
); );
} }
@ -641,14 +487,13 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _SnSubscriptionItem && other is _SnNewsArticle &&
(identical(other.id, id) || other.id == id) && (identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) || (identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) && other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) || (identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) && other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) || const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
other.deletedAt == deletedAt) &&
(identical(other.thumbnail, thumbnail) || (identical(other.thumbnail, thumbnail) ||
other.thumbnail == thumbnail) && other.thumbnail == thumbnail) &&
(identical(other.title, title) || other.title == title) && (identical(other.title, title) || other.title == title) &&
@ -657,8 +502,7 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
(identical(other.content, content) || other.content == content) && (identical(other.content, content) || other.content == content) &&
(identical(other.url, url) || other.url == url) && (identical(other.url, url) || other.url == url) &&
(identical(other.hash, hash) || other.hash == hash) && (identical(other.hash, hash) || other.hash == hash) &&
(identical(other.feedId, feedId) || other.feedId == feedId) && (identical(other.source, source) || other.source == source) &&
(identical(other.feed, feed) || other.feed == feed) &&
(identical(other.publishedAt, publishedAt) || (identical(other.publishedAt, publishedAt) ||
other.publishedAt == publishedAt)); other.publishedAt == publishedAt));
} }
@ -670,59 +514,54 @@ class _SnSubscriptionItem implements SnSubscriptionItem {
id, id,
createdAt, createdAt,
updatedAt, updatedAt,
deletedAt, const DeepCollectionEquality().hash(deletedAt),
thumbnail, thumbnail,
title, title,
description, description,
content, content,
url, url,
hash, hash,
feedId, source,
feed,
publishedAt); publishedAt);
@override @override
String toString() { 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 /// @nodoc
abstract mixin class _$SnSubscriptionItemCopyWith<$Res> abstract mixin class _$SnNewsArticleCopyWith<$Res>
implements $SnSubscriptionItemCopyWith<$Res> { implements $SnNewsArticleCopyWith<$Res> {
factory _$SnSubscriptionItemCopyWith( factory _$SnNewsArticleCopyWith(
_SnSubscriptionItem value, $Res Function(_SnSubscriptionItem) _then) = _SnNewsArticle value, $Res Function(_SnNewsArticle) _then) =
__$SnSubscriptionItemCopyWithImpl; __$SnNewsArticleCopyWithImpl;
@override @override
@useResult @useResult
$Res call( $Res call(
{int id, {int id,
DateTime createdAt, DateTime createdAt,
DateTime updatedAt, DateTime updatedAt,
DateTime? deletedAt, dynamic deletedAt,
String thumbnail, String thumbnail,
String title, String title,
String description, String description,
String content, String content,
String url, String url,
String hash, String hash,
int feedId, String source,
SnSubscriptionFeed feed,
DateTime? publishedAt}); DateTime? publishedAt});
@override
$SnSubscriptionFeedCopyWith<$Res> get feed;
} }
/// @nodoc /// @nodoc
class __$SnSubscriptionItemCopyWithImpl<$Res> class __$SnNewsArticleCopyWithImpl<$Res>
implements _$SnSubscriptionItemCopyWith<$Res> { implements _$SnNewsArticleCopyWith<$Res> {
__$SnSubscriptionItemCopyWithImpl(this._self, this._then); __$SnNewsArticleCopyWithImpl(this._self, this._then);
final _SnSubscriptionItem _self; final _SnNewsArticle _self;
final $Res Function(_SnSubscriptionItem) _then; 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. /// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@ -737,11 +576,10 @@ class __$SnSubscriptionItemCopyWithImpl<$Res>
Object? content = null, Object? content = null,
Object? url = null, Object? url = null,
Object? hash = null, Object? hash = null,
Object? feedId = null, Object? source = null,
Object? feed = null,
Object? publishedAt = freezed, Object? publishedAt = freezed,
}) { }) {
return _then(_SnSubscriptionItem( return _then(_SnNewsArticle(
id: null == id id: null == id
? _self.id ? _self.id
: id // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
@ -757,7 +595,7 @@ class __$SnSubscriptionItemCopyWithImpl<$Res>
deletedAt: freezed == deletedAt deletedAt: freezed == deletedAt
? _self.deletedAt ? _self.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as dynamic,
thumbnail: null == thumbnail thumbnail: null == thumbnail
? _self.thumbnail ? _self.thumbnail
: thumbnail // ignore: cast_nullable_to_non_nullable : thumbnail // ignore: cast_nullable_to_non_nullable
@ -782,30 +620,16 @@ class __$SnSubscriptionItemCopyWithImpl<$Res>
? _self.hash ? _self.hash
: hash // ignore: cast_nullable_to_non_nullable : hash // ignore: cast_nullable_to_non_nullable
as String, as String,
feedId: null == feedId source: null == source
? _self.feedId ? _self.source
: feedId // ignore: cast_nullable_to_non_nullable : source // ignore: cast_nullable_to_non_nullable
as int, as String,
feed: null == feed
? _self.feed
: feed // ignore: cast_nullable_to_non_nullable
as SnSubscriptionFeed,
publishedAt: freezed == publishedAt publishedAt: freezed == publishedAt
? _self.publishedAt ? _self.publishedAt
: publishedAt // ignore: cast_nullable_to_non_nullable : publishedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,
)); ));
} }
/// Create a copy of SnSubscriptionItem
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnSubscriptionFeedCopyWith<$Res> get feed {
return $SnSubscriptionFeedCopyWith<$Res>(_self.feed, (value) {
return _then(_self.copyWith(feed: value));
});
}
} }
// dart format on // dart format on

View File

@ -6,74 +6,56 @@ part of 'news.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
_SnSubscriptionFeed _$SnSubscriptionFeedFromJson(Map<String, dynamic> json) => _SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) =>
_SnSubscriptionFeed( _SnNewsSource(
id: (json['id'] as num).toInt(), id: json['id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), label: json['label'] as String,
updatedAt: DateTime.parse(json['updated_at'] as String), type: json['type'] as String,
deletedAt: json['deleted_at'] == null source: json['source'] as String,
? null depth: (json['depth'] as num).toInt(),
: DateTime.parse(json['deleted_at'] as String), enabled: json['enabled'] as bool,
url: json['url'] as String,
isEnabled: json['is_enabled'] as bool,
isFullContent: json['is_full_content'] as bool,
pullInterval: (json['pull_interval'] as num).toInt(),
adapter: json['adapter'] as String,
accountId: (json['account_id'] as num?)?.toInt(),
lastFetchedAt: json['last_fetched_at'] == null
? null
: DateTime.parse(json['last_fetched_at'] as String),
); );
Map<String, dynamic> _$SnSubscriptionFeedToJson(_SnSubscriptionFeed instance) => Map<String, dynamic> _$SnNewsSourceToJson(_SnNewsSource instance) =>
<String, dynamic>{ <String, dynamic>{
'id': instance.id, 'id': instance.id,
'created_at': instance.createdAt.toIso8601String(), 'label': instance.label,
'updated_at': instance.updatedAt.toIso8601String(), 'type': instance.type,
'deleted_at': instance.deletedAt?.toIso8601String(), 'source': instance.source,
'url': instance.url, 'depth': instance.depth,
'is_enabled': instance.isEnabled, 'enabled': instance.enabled,
'is_full_content': instance.isFullContent,
'pull_interval': instance.pullInterval,
'adapter': instance.adapter,
'account_id': instance.accountId,
'last_fetched_at': instance.lastFetchedAt?.toIso8601String(),
}; };
_SnSubscriptionItem _$SnSubscriptionItemFromJson(Map<String, dynamic> json) => _SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) =>
_SnSubscriptionItem( _SnNewsArticle(
id: (json['id'] as num).toInt(), id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null deletedAt: json['deleted_at'],
? null
: DateTime.parse(json['deleted_at'] as String),
thumbnail: json['thumbnail'] as String, thumbnail: json['thumbnail'] as String,
title: json['title'] as String, title: json['title'] as String,
description: json['description'] as String, description: json['description'] as String,
content: json['content'] as String, content: json['content'] as String,
url: json['url'] as String, url: json['url'] as String,
hash: json['hash'] as String, hash: json['hash'] as String,
feedId: (json['feed_id'] as num).toInt(), source: json['source'] as String,
feed: SnSubscriptionFeed.fromJson(json['feed'] as Map<String, dynamic>),
publishedAt: json['published_at'] == null publishedAt: json['published_at'] == null
? null ? null
: DateTime.parse(json['published_at'] as String), : DateTime.parse(json['published_at'] as String),
); );
Map<String, dynamic> _$SnSubscriptionItemToJson(_SnSubscriptionItem instance) => Map<String, dynamic> _$SnNewsArticleToJson(_SnNewsArticle instance) =>
<String, dynamic>{ <String, dynamic>{
'id': instance.id, 'id': instance.id,
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt,
'thumbnail': instance.thumbnail, 'thumbnail': instance.thumbnail,
'title': instance.title, 'title': instance.title,
'description': instance.description, 'description': instance.description,
'content': instance.content, 'content': instance.content,
'url': instance.url, 'url': instance.url,
'hash': instance.hash, 'hash': instance.hash,
'feed_id': instance.feedId, 'source': instance.source,
'feed': instance.feed.toJson(),
'published_at': instance.publishedAt?.toIso8601String(), 'published_at': instance.publishedAt?.toIso8601String(),
}; };

View File

@ -181,3 +181,41 @@ abstract class SnFeedEntry with _$SnFeedEntry {
factory SnFeedEntry.fromJson(Map<String, dynamic> json) => factory SnFeedEntry.fromJson(Map<String, dynamic> json) =>
_$SnFeedEntryFromJson(json); _$SnFeedEntryFromJson(json);
} }
@freezed
abstract class SnFediversePost with _$SnFediversePost {
const factory SnFediversePost({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String identifier,
required String origin,
required String content,
required String language,
required List<String> images,
required SnFediverseUser user,
required int userId,
}) = _SnFediversePost;
factory SnFediversePost.fromJson(Map<String, Object?> json) =>
_$SnFediversePostFromJson(json);
}
@freezed
abstract class SnFediverseUser with _$SnFediverseUser {
const factory SnFediverseUser({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String identifier,
required String origin,
required String avatar,
required String name,
required String nick,
}) = _SnFediverseUser;
factory SnFediverseUser.fromJson(Map<String, dynamic> json) =>
_$SnFediverseUserFromJson(json);
}

View File

@ -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 // dart format on

View File

@ -303,3 +303,64 @@ Map<String, dynamic> _$SnFeedEntryToJson(_SnFeedEntry instance) =>
'data': instance.data, 'data': instance.data,
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
}; };
_SnFediversePost _$SnFediversePostFromJson(Map<String, dynamic> json) =>
_SnFediversePost(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
identifier: json['identifier'] as String,
origin: json['origin'] as String,
content: json['content'] as String,
language: json['language'] as String,
images:
(json['images'] as List<dynamic>).map((e) => e as String).toList(),
user: SnFediverseUser.fromJson(json['user'] as Map<String, dynamic>),
userId: (json['user_id'] as num).toInt(),
);
Map<String, dynamic> _$SnFediversePostToJson(_SnFediversePost instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'identifier': instance.identifier,
'origin': instance.origin,
'content': instance.content,
'language': instance.language,
'images': instance.images,
'user': instance.user.toJson(),
'user_id': instance.userId,
};
_SnFediverseUser _$SnFediverseUserFromJson(Map<String, dynamic> json) =>
_SnFediverseUser(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
identifier: json['identifier'] as String,
origin: json['origin'] as String,
avatar: json['avatar'] as String,
name: json['name'] as String,
nick: json['nick'] as String,
);
Map<String, dynamic> _$SnFediverseUserToJson(_SnFediverseUser instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'identifier': instance.identifier,
'origin': instance.origin,
'avatar': instance.avatar,
'name': instance.name,
'nick': instance.nick,
};

View File

@ -12,9 +12,6 @@ import 'package:surface/widgets/dialog.dart';
class AttachmentInputDialog extends StatefulWidget { class AttachmentInputDialog extends StatefulWidget {
final String? title; final String? title;
final bool? analyzeNow; final bool? analyzeNow;
final bool canPickMedia;
final bool canReferenceLink;
final bool canRandomId;
final SnMediaType? mediaType; final SnMediaType? mediaType;
final String pool; final String pool;
@ -24,9 +21,6 @@ class AttachmentInputDialog extends StatefulWidget {
required this.pool, required this.pool,
this.analyzeNow = false, this.analyzeNow = false,
this.mediaType = SnMediaType.image, this.mediaType = SnMediaType.image,
this.canPickMedia = true,
this.canReferenceLink = true,
this.canRandomId = true,
}); });
@override @override
@ -35,8 +29,6 @@ class AttachmentInputDialog extends StatefulWidget {
class _AttachmentInputDialogState extends State<AttachmentInputDialog> { class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
final _randomIdController = TextEditingController(); final _randomIdController = TextEditingController();
final _referenceLinkController = TextEditingController();
final _referenceMimetypeController = TextEditingController();
XFile? _file; XFile? _file;
double? _progress; double? _progress;
@ -69,26 +61,9 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); 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) { } else if (_file != null) {
try { try {
final place = await attach.chunkedUploadInitialize( final place = await attach.chunkedUploadInitialize(await _file!.length(), _file!.name, widget.pool, null);
await _file!.length(), _file!.name, widget.pool, null);
final attachment = await attach.chunkedUploadParts( final attachment = await attach.chunkedUploadParts(
_file!, _file!,
@ -114,15 +89,8 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
return AlertDialog( return AlertDialog(
title: Text(widget.title ?? 'attachmentInputDialog'.tr()), title: Text(widget.title ?? 'attachmentInputDialog'.tr()),
content: Column( content: Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [
if (_file == null &&
_referenceLinkController.text.isEmpty &&
widget.canRandomId)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('attachmentInputUseRandomId').tr().fontSize(14), Text('attachmentInputUseRandomId').tr().fontSize(14),
const Gap(8), const Gap(8),
@ -130,73 +98,22 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
controller: _randomIdController, controller: _randomIdController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'fieldAttachmentRandomId'.tr(), labelText: 'fieldAttachmentRandomId'.tr(),
border: const OutlineInputBorder(), border: const UnderlineInputBorder(),
isDense: true, isDense: true,
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
], const Gap(24),
),
if (_file == null &&
_referenceLinkController.text.isEmpty &&
widget.canReferenceLink)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('attachmentReferenceLink').tr().fontSize(14),
const Gap(8),
TextField(
controller: _referenceLinkController,
decoration: InputDecoration(
labelText: 'fieldAttachmentReferenceLink'.tr(),
helperText: 'attachmentReferenceLinkDescription'.tr(),
helperMaxLines: 3,
border: const OutlineInputBorder(),
isDense: true,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(8),
TextField(
controller: _referenceLinkController,
decoration: InputDecoration(
labelText: 'fieldAttachmentMimetype'.tr(),
helperText: 'class/type',
border: const OutlineInputBorder(),
isDense: true,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
if (_referenceLinkController.text.isEmpty &&
_randomIdController.text.isEmpty &&
widget.canPickMedia)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('attachmentInputNew').tr().fontSize(14), Text('attachmentInputNew').tr().fontSize(14),
Card( Card(
margin: EdgeInsets.only(top: 8),
child: Column( child: Column(
children: [ children: [
ListTile( ListTile(
shape: RoundedRectangleBorder( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
leading: const Icon(Symbols.add_photo_alternate), leading: const Icon(Symbols.add_photo_alternate),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
title: Text('addAttachmentFromAlbum').tr(), title: Text('addAttachmentFromAlbum').tr(),
subtitle: _file == null subtitle: _file == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
? Text('unset').tr()
: Text('waitingForUpload').tr(),
onTap: () { onTap: () {
_pickMedia(); _pickMedia();
}, },
@ -204,8 +121,6 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
], ],
), ),
), ),
],
),
if (_isBusy) if (_isBusy)
LinearProgressIndicator( LinearProgressIndicator(
value: _progress, value: _progress,

View File

@ -15,7 +15,6 @@ import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/logger.dart'; import 'package:surface/logger.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -229,7 +228,6 @@ class _AttachmentItemContentVideoState
setState(() => _showContent = true); setState(() => _showContent = true);
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
final url = _showOriginal final url = _showOriginal
? sn.getAttachmentUrl(widget.data.rid) ? sn.getAttachmentUrl(widget.data.rid)
: sn.getAttachmentUrl(widget.data.compressed!.rid); : sn.getAttachmentUrl(widget.data.compressed!.rid);
@ -242,7 +240,6 @@ class _AttachmentItemContentVideoState
logging.info('[MediaPlayer] Miss cache: $url'); logging.info('[MediaPlayer] Miss cache: $url');
final fileStream = DefaultCacheManager().getFileStream( final fileStream = DefaultCacheManager().getFileStream(
url, url,
headers: {'Authorization': 'Bearer ${await ua.atk}'},
withProgress: true, withProgress: true,
); );
await for (var fileInfo in fileStream) { await for (var fileInfo in fileStream) {
@ -502,7 +499,6 @@ class _AttachmentItemContentAudioState
setState(() => _showContent = true); setState(() => _showContent = true);
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
final url = sn.getAttachmentUrl(widget.data.rid); final url = sn.getAttachmentUrl(widget.data.rid);
_audioPlayer = Player(); _audioPlayer = Player();
@ -512,7 +508,6 @@ class _AttachmentItemContentAudioState
logging.info('[MediaPlayer] Miss cache: $url'); logging.info('[MediaPlayer] Miss cache: $url');
final fileStream = DefaultCacheManager().getFileStream( final fileStream = DefaultCacheManager().getFileStream(
url, url,
headers: {'Authorization': 'Bearer ${await ua.atk}'},
withProgress: true, withProgress: true,
); );
await for (var fileInfo in fileStream) { await for (var fileInfo in fileStream) {

View File

@ -373,7 +373,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
_showDetail = true; _showDetail = true;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => AttachmentZoomDetailPopup( builder: (context) => _AttachmentZoomDetailPopup(
data: widget.data.elementAt(_page), data: widget.data.elementAt(_page),
), ),
).then((_) { ).then((_) {
@ -403,7 +403,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
_showDetail = true; _showDetail = true;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => AttachmentZoomDetailPopup( builder: (context) => _AttachmentZoomDetailPopup(
data: widget.data.elementAt(_page), data: widget.data.elementAt(_page),
), ),
).then((_) { ).then((_) {
@ -416,10 +416,10 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
} }
} }
class AttachmentZoomDetailPopup extends StatelessWidget { class _AttachmentZoomDetailPopup extends StatelessWidget {
final SnAttachment data; final SnAttachment data;
const AttachmentZoomDetailPopup({required this.data}); const _AttachmentZoomDetailPopup({required this.data});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -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(),
),
],
),
),
],
);
}
}

View File

@ -10,12 +10,10 @@ class PendingAttachmentAltDialog extends StatefulWidget {
const PendingAttachmentAltDialog({super.key, required this.media}); const PendingAttachmentAltDialog({super.key, required this.media});
@override @override
State<PendingAttachmentAltDialog> createState() => State<PendingAttachmentAltDialog> createState() => _PendingAttachmentAltDialogState();
_PendingAttachmentAltDialogState();
} }
class _PendingAttachmentAltDialogState class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> {
extends State<PendingAttachmentAltDialog> {
final _contentController = TextEditingController(); final _contentController = TextEditingController();
@override @override
@ -65,7 +63,7 @@ class _PendingAttachmentAltDialogState
controller: _contentController, controller: _contentController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'fieldAttachmentAlt'.tr(), labelText: 'fieldAttachmentAlt'.tr(),
border: const OutlineInputBorder(), border: const UnderlineInputBorder(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
@ -73,9 +71,7 @@ class _PendingAttachmentAltDialogState
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: _isBusy onPressed: _isBusy ? null : () {
? null
: () {
Navigator.pop(context); Navigator.pop(context);
}, },
child: Text('dialogDismiss'.tr()), child: Text('dialogDismiss'.tr()),

View File

@ -14,12 +14,10 @@ class PendingAttachmentBoostDialog extends StatefulWidget {
const PendingAttachmentBoostDialog({super.key, required this.media}); const PendingAttachmentBoostDialog({super.key, required this.media});
@override @override
State<PendingAttachmentBoostDialog> createState() => State<PendingAttachmentBoostDialog> createState() => _PendingAttachmentBoostDialogState();
_PendingAttachmentBoostDialogState();
} }
class _PendingAttachmentBoostDialogState class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDialog> {
extends State<PendingAttachmentBoostDialog> {
List<SnAttachmentDestination>? _regions; List<SnAttachmentDestination>? _regions;
SnAttachmentDestination? _selectedRegion; SnAttachmentDestination? _selectedRegion;
@ -86,23 +84,17 @@ class _PendingAttachmentBoostDialogState
children: _regions!.map( children: _regions!.map(
(ele) { (ele) {
return RadioListTile( return RadioListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
title: Text(ele.label).tr(), title: Text(ele.label).tr(),
subtitle: Text( subtitle: Text(
'attachmentDestinationRegion${ele.region}'.trExists() 'attachmentDestinationRegion${ele.region}'.trExists()
? 'attachmentDestinationRegion${ele.region}' ? 'attachmentDestinationRegion${ele.region}'.tr()
.tr()
: ele.region, : ele.region,
), ),
selected: _selectedRegion == ele, selected: _selectedRegion == ele,
value: ele, value: ele,
groupValue: _selectedRegion, groupValue: _selectedRegion,
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) setState(() => _selectedRegion = value);
setState(() => _selectedRegion = value);
}
}, },
); );
}, },
@ -113,9 +105,7 @@ class _PendingAttachmentBoostDialogState
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: _isBusy onPressed: _isBusy ? null : () {
? null
: () {
Navigator.pop(context); Navigator.pop(context);
}, },
child: Text('dialogDismiss'.tr()), child: Text('dialogDismiss'.tr()),

View File

@ -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()),
),
],
);
}
}

View 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(),
),
],
),
);
}
}

View 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();
}
}

View 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,
),
),
),
);
}
}

View 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),
],
),
);
}
}

View 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();
},
),
),
],
),
),
],
);
}
}

View 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(),
),
);
}
}

View 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();
}
}

View 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),
),
),
],
);
}
}

View File

@ -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(),
});
},
);
}
}

View File

@ -232,7 +232,7 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
final alias = match[0]!; final alias = match[0]!;
final anchor = markdown.Element.text('a', alias) final anchor = markdown.Element.text('a', alias)
..attributes['href'] = Uri.encodeFull( ..attributes['href'] = Uri.encodeFull(
'solink://accounts/${alias.substring(1)}', 'solink://account/${alias.substring(1)}',
); );
parser.addNode(anchor); parser.addNode(anchor);

View 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,
),
),
);
}
}

View File

@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@ -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/post_reaction.dart';
import 'package:surface/widgets/post/publisher_popover.dart'; import 'package:surface/widgets/post/publisher_popover.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
class OpenablePostItem extends StatelessWidget { class OpenablePostItem extends StatelessWidget {
@ -2323,10 +2321,7 @@ class _PostVideoPlayer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Container(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all( border: Border.all(
@ -2338,33 +2333,12 @@ class _PostVideoPlayer extends StatelessWidget {
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: (data.body['video'] is String) child: AttachmentItem(
? InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri(data.body['video']),
),
)
: AttachmentItem(
data: data.body['video'], data: data.body['video'],
heroTag: 'post-video-${data.id}', 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),
],
); );
} }
} }

View File

@ -1,5 +1,3 @@
// ignore_for_file: unused_import
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui';
@ -24,9 +22,9 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_input.dart'; import 'package:surface/widgets/attachment/attachment_input.dart';
import 'package:surface/widgets/attachment/attachment_zoom.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_alt.dart';
import 'package:surface/widgets/attachment/pending_attachment_boost.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/dialog.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
@ -52,6 +50,232 @@ class PostMediaPendingList extends StatelessWidget {
this.onUpdateBusy, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
@ -63,27 +287,9 @@ class PostMediaPendingList extends StatelessWidget {
itemCount: attachments.length, itemCount: attachments.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final media = attachments[idx]; final media = attachments[idx];
return GestureDetector( return ContextMenuArea(
contextMenu: _createContextMenu(context, idx, media),
child: _PostMediaPendingItem(media: 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 { class _PostMediaPendingItem extends StatelessWidget {
final PostWriteMedia media; final PostWriteMedia media;
const _PostMediaPendingItem({required this.media}); const _PostMediaPendingItem({
required this.media,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -113,36 +321,34 @@ class _PostMediaPendingItem extends StatelessWidget {
AspectRatio( AspectRatio(
aspectRatio: 1, aspectRatio: 1,
child: switch (media.type) { child: switch (media.type) {
SnMediaType.image => LayoutBuilder( SnMediaType.image =>
builder: (context, constraints) { LayoutBuilder(builder: (context, constraints) {
return Image( return Image(
image: media.getImageProvider( image: media.getImageProvider(
context, context,
width: width:
(constraints.maxWidth * devicePixelRatio).round(), (constraints.maxWidth * devicePixelRatio).round(),
height: (constraints.maxHeight * devicePixelRatio) height:
.round(), (constraints.maxHeight * devicePixelRatio).round(),
)!, )!,
fit: BoxFit.contain, fit: BoxFit.contain,
); );
}, }),
),
SnMediaType.video => Stack( SnMediaType.video => Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
if (media.attachment?.thumbnail != null) if (media.attachment?.thumbnail != null)
AutoResizeUniversalImage(sn.getAttachmentUrl( AutoResizeUniversalImage(sn.getAttachmentUrl(
media.attachment!.thumbnail!.rid)), media.attachment!.thumbnail!.rid)),
const Icon( const Icon(Symbols.videocam,
Symbols.videocam,
color: Colors.white, color: Colors.white,
shadows: [ shadows: [
Shadow( Shadow(
offset: Offset(1, 1), offset: Offset(1, 1),
blurRadius: 8.0, blurRadius: 8.0,
color: Color.fromARGB(255, 0, 0, 0)) color: Color.fromARGB(255, 0, 0, 0),
],
), ),
]),
], ],
), ),
SnMediaType.audio => Stack( SnMediaType.audio => Stack(
@ -151,16 +357,15 @@ class _PostMediaPendingItem extends StatelessWidget {
if (media.attachment?.thumbnail != null) if (media.attachment?.thumbnail != null)
AutoResizeUniversalImage(sn.getAttachmentUrl( AutoResizeUniversalImage(sn.getAttachmentUrl(
media.attachment!.thumbnail!.rid)), media.attachment!.thumbnail!.rid)),
const Icon( const Icon(Symbols.audio_file,
Symbols.audio_file,
color: Colors.white, color: Colors.white,
shadows: [ shadows: [
Shadow( Shadow(
offset: Offset(1, 1), offset: Offset(1, 1),
blurRadius: 8.0, blurRadius: 8.0,
color: Color.fromARGB(255, 0, 0, 0)) color: Color.fromARGB(255, 0, 0, 0),
],
), ),
]),
], ],
), ),
_ => Container( _ => Container(
@ -182,8 +387,11 @@ class _PostMediaPendingItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (media.attachment != null) if (media.attachment != null)
Text(media.attachment!.alt, Text(
maxLines: 1, overflow: TextOverflow.ellipsis) media.attachment!.alt,
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
else if (media.file != null) else if (media.file != null)
Text(media.file!.name, Text(media.file!.name,
maxLines: 1, overflow: TextOverflow.ellipsis) maxLines: 1, overflow: TextOverflow.ellipsis)
@ -260,8 +468,11 @@ class AddPostMediaButton extends StatelessWidget {
final VisualDensity? visualDensity; final VisualDensity? visualDensity;
final Function(Iterable<PostWriteMedia>) onAdd; final Function(Iterable<PostWriteMedia>) onAdd;
const AddPostMediaButton( const AddPostMediaButton({
{super.key, required this.onAdd, this.visualDensity}); super.key,
required this.onAdd,
this.visualDensity,
});
void _takeMedia(bool isVideo) async { void _takeMedia(bool isVideo) async {
final picker = ImagePicker(); final picker = ImagePicker();
@ -276,13 +487,17 @@ class AddPostMediaButton extends StatelessWidget {
final picker = ImagePicker(); final picker = ImagePicker();
final result = await picker.pickMultipleMedia(); final result = await picker.pickMultipleMedia();
if (result.isEmpty) return; if (result.isEmpty) return;
onAdd(result.map((e) => PostWriteMedia.fromFile(e))); onAdd(
result.map((e) => PostWriteMedia.fromFile(e)),
);
} }
void _selectFile() async { void _selectFile() async {
final result = await FilePicker.platform.pickFiles(type: FileType.any); final result = await FilePicker.platform.pickFiles(type: FileType.any);
if (result == null) return; 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 { void _pasteMedia() async {
@ -290,7 +505,10 @@ class AddPostMediaButton extends StatelessWidget {
if (imageBytes == null) return; if (imageBytes == null) return;
onAdd([ onAdd([
PostWriteMedia.fromBytes( 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 attach = context.read<SnAttachmentProvider>();
final attachment = await attach.getOne(randomId); final attachment = await attach.getOne(randomId);
onAdd([PostWriteMedia(attachment)]); onAdd([
PostWriteMedia(attachment),
]);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopupMenuButton( return PopupMenuButton(
style: ButtonStyle(visualDensity: visualDensity), style: ButtonStyle(visualDensity: visualDensity),
icon: Icon(Symbols.add_photo_alternate, icon: Icon(
color: Theme.of(context).colorScheme.primary), Symbols.add_photo_alternate,
color: Theme.of(context).colorScheme.primary,
),
itemBuilder: (context) => [ itemBuilder: (context) => [
if (!kIsWeb && if (!kIsWeb &&
!Platform.isLinux && !Platform.isLinux &&
@ -373,7 +595,7 @@ class AddPostMediaButton extends StatelessWidget {
children: [ children: [
const Icon(Symbols.videocam), const Icon(Symbols.videocam),
const Gap(16), const Gap(16),
Text('addAttachmentFromCameraVideo').tr() Text('addAttachmentFromCameraVideo').tr(),
], ],
), ),
onTap: () { onTap: () {
@ -385,7 +607,7 @@ class AddPostMediaButton extends StatelessWidget {
children: [ children: [
const Icon(Symbols.photo_library), const Icon(Symbols.photo_library),
const Gap(16), const Gap(16),
Text('addAttachmentFromAlbum').tr() Text('addAttachmentFromAlbum').tr(),
], ],
), ),
onTap: () { onTap: () {
@ -397,7 +619,7 @@ class AddPostMediaButton extends StatelessWidget {
children: [ children: [
const Icon(Symbols.file_upload), const Icon(Symbols.file_upload),
const Gap(16), const Gap(16),
Text('addAttachmentFromFiles').tr() Text('addAttachmentFromFiles').tr(),
], ],
), ),
onTap: () { onTap: () {
@ -405,11 +627,13 @@ class AddPostMediaButton extends StatelessWidget {
}, },
), ),
PopupMenuItem( PopupMenuItem(
child: Row(children: [ child: Row(
children: [
const Icon(Symbols.link), const Icon(Symbols.link),
const Gap(16), const Gap(16),
Text('addAttachmentFromRandomId').tr() Text('addAttachmentFromRandomId').tr(),
]), ],
),
onTap: () { onTap: () {
_linkRandomId(context); _linkRandomId(context);
}, },
@ -419,7 +643,7 @@ class AddPostMediaButton extends StatelessWidget {
children: [ children: [
const Icon(Symbols.content_paste), const Icon(Symbols.content_paste),
const Gap(16), const Gap(16),
Text('addAttachmentFromClipboard').tr() Text('addAttachmentFromClipboard').tr(),
], ],
), ),
onTap: () { onTap: () {

View File

@ -40,7 +40,6 @@ class UnauthorizedHint extends StatelessWidget {
GoRouter.of(context).pushNamed('authLogin').then((value) { GoRouter.of(context).pushNamed('authLogin').then((value) {
if (value == true && context.mounted) { if (value == true && context.mounted) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
ua.refreshUser();
context.showSnackbar('loginSuccess'.tr(args: [ context.showSnackbar('loginSuccess'.tr(args: [
'@${ua.user?.name} (${ua.user?.nick})', '@${ua.user?.name} (${ua.user?.nick})',
])); ]));

View File

@ -41,7 +41,7 @@ endif()
# of modifying this function. # of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET) function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14) 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_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>") target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction() endfunction()

View File

@ -13,6 +13,7 @@
#include <file_selector_linux/file_selector_plugin.h> #include <file_selector_linux/file_selector_plugin.h>
#include <flutter_timezone/flutter_timezone_plugin.h> #include <flutter_timezone/flutter_timezone_plugin.h>
#include <flutter_udid/flutter_udid_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 <hotkey_manager_linux/hotkey_manager_linux_plugin.h>
#include <local_notifier/local_notifier_plugin.h> #include <local_notifier/local_notifier_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_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 = g_autoptr(FlPluginRegistrar) flutter_udid_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin");
flutter_udid_plugin_register_with_registrar(flutter_udid_registrar); 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 = g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar); hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar);

View File

@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux file_selector_linux
flutter_timezone flutter_timezone
flutter_udid flutter_udid
flutter_webrtc
hotkey_manager_linux hotkey_manager_linux
local_notifier local_notifier
media_kit_libs_linux media_kit_libs_linux

View File

@ -19,9 +19,12 @@ import firebase_messaging
import flutter_inappwebview_macos import flutter_inappwebview_macos
import flutter_timezone import flutter_timezone
import flutter_udid import flutter_udid
import flutter_webrtc
import gal import gal
import hotkey_manager_macos import hotkey_manager_macos
import in_app_review import in_app_review
import livekit_client
import livekit_noise_filter
import local_notifier import local_notifier
import media_kit_libs_macos_video import media_kit_libs_macos_video
import media_kit_video import media_kit_video
@ -53,9 +56,12 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) 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")) LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))

View File

@ -85,6 +85,9 @@ PODS:
- flutter_udid (0.0.1): - flutter_udid (0.0.1):
- FlutterMacOS - FlutterMacOS
- SAMKeychain - SAMKeychain
- flutter_webrtc (0.12.6):
- FlutterMacOS
- WebRTC-SDK (= 125.6422.06)
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
@ -145,6 +148,15 @@ PODS:
- HotKey - HotKey
- in_app_review (2.0.0): - in_app_review (2.0.0):
- FlutterMacOS - 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): - local_notifier (0.1.0):
- FlutterMacOS - FlutterMacOS
- media_kit_libs_macos_video (1.0.4): - media_kit_libs_macos_video (1.0.4):
@ -206,6 +218,7 @@ PODS:
- FlutterMacOS - FlutterMacOS
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (125.6422.06)
DEPENDENCIES: DEPENDENCIES:
- audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`) - 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_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
- flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`) - flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`)
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/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`) - FlutterMacOS (from `Flutter/ephemeral`)
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/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`) - 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_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`) - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
@ -255,11 +271,13 @@ SPEC REPOS:
- GoogleDataTransport - GoogleDataTransport
- GoogleUtilities - GoogleUtilities
- HotKey - HotKey
- LiveKitKrispNoiseFilter
- nanopb - nanopb
- OrderedSet - OrderedSet
- PromisesObjC - PromisesObjC
- SAMKeychain - SAMKeychain
- sqlite3 - sqlite3
- WebRTC-SDK
EXTERNAL SOURCES: EXTERNAL SOURCES:
audioplayers_darwin: audioplayers_darwin:
@ -292,6 +310,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos :path: Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos
flutter_udid: flutter_udid:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos :path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
flutter_webrtc:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
gal: gal:
@ -300,6 +320,10 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
in_app_review: in_app_review:
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos :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: local_notifier:
:path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos :path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos
media_kit_libs_macos_video: media_kit_libs_macos_video:
@ -353,6 +377,7 @@ SPEC CHECKSUMS:
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
flutter_timezone: d59eea86178cbd7943cd2431cc2eaa9850f935d8 flutter_timezone: d59eea86178cbd7943cd2431cc2eaa9850f935d8
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495 flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
flutter_webrtc: 377dbcebdde6fed0fc40de87bcaaa2bffcec9a88
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: baecd024ebfd13c441269ca7404792a7152fde89 gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d
@ -361,6 +386,9 @@ SPEC CHECKSUMS:
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe
in_app_review: 0599bccaed5e02f6bed2b0d30d16f86b63ed8638 in_app_review: 0599bccaed5e02f6bed2b0d30d16f86b63ed8638
livekit_client: 35690bf9861be6325a6f7d11bb38d50c7c9fed80
livekit_noise_filter: c5710c0871ef3621b48c0b44d3c3ff938ba414b2
LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84
local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
@ -381,6 +409,7 @@ SPEC CHECKSUMS:
video_compress: 752b161da855df2492dd1a8fa899743cc8fe9534 video_compress: 752b161da855df2492dd1a8fa899743cc8fe9534
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497 wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009 PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009

View File

@ -417,6 +417,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" 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: dbus:
dependency: transitive dependency: transitive
description: description:
@ -903,10 +911,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_markdown name: flutter_markdown
sha256: "634622a3a826d67cb05c0e3e576d1812c430fa98404e95b60b131775c73d76ec" sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.7" version: "0.7.6+2"
flutter_markdown_latex: flutter_markdown_latex:
dependency: "direct main" dependency: "direct main"
description: description:
@ -989,14 +997,22 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: freezed:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: freezed name: freezed
sha256: "19e64d719a9f0d2e7f74a2f59624acee0e96b3e897ecf72edcae52ccc36a424f" sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.4"
freezed_annotation: freezed_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1277,14 +1293,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" 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: js:
dependency: transitive dependency: transitive
description: description:
@ -1365,6 +1373,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" 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: local_notifier:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1773,6 +1797,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
protobuf:
dependency: transitive
description:
name: protobuf
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
provider: provider:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1885,6 +1917,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" 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: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2302,10 +2342,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.3" version: "6.3.2"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@ -2474,6 +2514,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" 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: win32:
dependency: transitive dependency: transitive
description: description:

View File

@ -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 # 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 # 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. # 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: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@ -82,6 +82,8 @@ dependencies:
media_kit_libs_video: ^1.0.5 media_kit_libs_video: ^1.0.5
pasteboard: ^0.3.0 pasteboard: ^0.3.0
synchronized: ^3.3.0+3 synchronized: ^3.3.0+3
dart_webrtc: ^1.4.10
livekit_client: ^2.3.1+hotfix.1
wakelock_plus: ^1.2.8 wakelock_plus: ^1.2.8
permission_handler: ^11.3.1 permission_handler: ^11.3.1
flutter_staggered_grid_view: ^0.7.0 flutter_staggered_grid_view: ^0.7.0
@ -111,6 +113,7 @@ dependencies:
version: ^3.0.2 version: ^3.0.2
flutter_colorpicker: ^1.1.0 flutter_colorpicker: ^1.1.0
fl_chart: ^0.70.0 fl_chart: ^0.70.0
flutter_webrtc: ^0.12.5+hotfix.1
slide_countdown: ^2.0.2 slide_countdown: ^2.0.2
video_compress: ^3.1.3 video_compress: ^3.1.3
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
@ -141,7 +144,7 @@ dependencies:
latlong2: ^0.9.1 latlong2: ^0.9.1
crypto: ^3.0.6 crypto: ^3.0.6
audioplayers: ^6.4.0 audioplayers: ^6.4.0
jitsi_meet_flutter_sdk: ^11.1.1 livekit_noise_filter: ^0.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -16,8 +16,10 @@
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h> #include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_timezone/flutter_timezone_plugin_c_api.h> #include <flutter_timezone/flutter_timezone_plugin_c_api.h>
#include <flutter_udid/flutter_udid_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 <gal/gal_plugin_c_api.h>
#include <hotkey_manager_windows/hotkey_manager_windows_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 <local_notifier/local_notifier_plugin.h>
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.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> #include <media_kit_video/media_kit_video_plugin_c_api.h>
@ -50,10 +52,14 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi")); registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi"));
FlutterUdidPluginCApiRegisterWithRegistrar( FlutterUdidPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi")); registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
FlutterWebRTCPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
GalPluginCApiRegisterWithRegistrar( GalPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GalPluginCApi")); registry->GetRegistrarForPlugin("GalPluginCApi"));
HotkeyManagerWindowsPluginCApiRegisterWithRegistrar( HotkeyManagerWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi")); registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
LiveKitPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LiveKitPlugin"));
LocalNotifierPluginRegisterWithRegistrar( LocalNotifierPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalNotifierPlugin")); registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(

View File

@ -13,8 +13,10 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_inappwebview_windows flutter_inappwebview_windows
flutter_timezone flutter_timezone
flutter_udid flutter_udid
flutter_webrtc
gal gal
hotkey_manager_windows hotkey_manager_windows
livekit_client
local_notifier local_notifier
media_kit_libs_windows_video media_kit_libs_windows_video
media_kit_video media_kit_video