Compare commits

...

24 Commits

Author SHA1 Message Date
b84fafb53c 🚀 Launch 3.0.0+103 2025-06-04 02:03:33 +08:00
9e9d8b6fab 🐛 Bug fixes, and fixes 2025-06-04 01:59:17 +08:00
088cb4d5a2 🚀 Launch 3.0.0+102 2025-06-02 22:52:19 +08:00
33e84805d7 :drunk: I have no idea what did I did 2025-06-02 22:51:52 +08:00
9aca6eb674 Post show replies / forwareded 2025-06-02 02:43:57 +08:00
e431a54a89 🐛 Fix NSE, get token from flutter side in native iOS 2025-06-02 02:23:59 +08:00
c00987dfdc 🚀 Launch 3.0.0+100 2025-06-02 01:08:13 +08:00
4187ceb248 👽 Support new attachment reference system 2025-06-02 01:08:04 +08:00
152e076d44 🐛 Trying to fix NSE 2025-06-01 11:36:50 +08:00
7f36c86c55 New NSE and hold notification to reply 2025-06-01 03:29:37 +08:00
311420e1f7 🚀 Launch 3.0.0+97 w/ NSE 2025-05-31 19:16:47 +08:00
afe1c700eb More customize options are back 2025-05-30 01:23:24 +08:00
b0c1981c9a 🐛 Fix android building 2025-05-29 21:59:27 +08:00
d943275ed5 🔨 Upgrade android gradle project 2025-05-29 21:40:34 +08:00
010a49251c 🚀 Launch 3.0.0+96 2025-05-29 01:45:51 +08:00
bdc13978c3 👽 Support the new Dyson Token 2025-05-28 23:21:13 +08:00
5d8c73e468 🐛 Fix replies activities didn't rendered 2025-05-28 23:08:53 +08:00
360572d7d1 🚀 Launch 3.0.0+95 2025-05-28 02:02:17 +08:00
9c1a983466 Reactive notification unread count 2025-05-28 01:52:44 +08:00
bfa97dcd11 💄 Optimized compose page 2025-05-28 01:13:49 +08:00
aaa505e83e Accoun settings 2025-05-28 01:08:18 +08:00
552bdfa58f 🐛 Fix publisher page renders error when publisher name didn't match username 2025-05-27 23:03:56 +08:00
ebde4eeed5 Reset password 2025-05-27 22:41:27 +08:00
688f035f85 Better chat overlay 2025-05-27 02:35:08 +08:00
118 changed files with 5033 additions and 2086 deletions

83
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,83 @@
name: Build Release
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
build-web:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
- run: flutter build web --release
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-web
path: build/web
build-exe:
runs-on: windows-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
- run: flutter build windows
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-windows
path: build/windows/x64/runner/Release
build-linux:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
sudo apt-get install -y libmpv-dev mpv
sudo apt-get install -y libayatana-appindicator3-dev
sudo apt-get install -y keybinder-3.0
sudo apt-get install -y libnotify-dev
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
sudo apt-get install -y gstreamer-1.0
- run: flutter pub get
- run: flutter build linux
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-linux
path: build/linux/x64/release/bundle
- name: Build AppImage
run: |
rm -r Solian.AppDir | true
mkdir Solian.AppDir
cp -r build/linux/x64/release/bundle/* Solian.AppDir
cp -r buildtools/appimage_config/* Solian.AppDir
cp assets/icon/icon-light-radius.png Solian.AppDir
sudo chmod +x buildtools/appimagetool-x86_64.AppImage
sudo chmod +x Solian.AppDir/AppRun
./buildtools/appimagetool-x86_64.AppImage Solian.AppDir
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-linux-appimage
path: './*.AppImage*'

1
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -11,7 +11,7 @@ plugins {
android {
namespace = "dev.solsynth.solian"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
ndkVersion = "29.0.13113456"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17

View File

@ -34,19 +34,22 @@
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="com.superlist.super_native_extensions.DataProvider"
android:authorities="dev.solsynth.solian.SuperClipboardDataProvider"
android:exported="true"
android:grantUriPermissions="true" >
android:name="androidx.core.content.FileProvider"
android:authorities="dev.solsynth.solian.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@ -61,8 +64,8 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>
</manifest>

View File

@ -1,5 +0,0 @@
package dev.solsynth.solian
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@ -0,0 +1,14 @@
package dev.solsynth.solian
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin
class MainActivity : FlutterActivity()
{
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// https://github.com/flutter/flutter/issues/153075#issuecomment-2693189362
flutterEngine.plugins.add(LegacySharedPreferencesPlugin())
}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="external_files"
path="." />
</paths>

File diff suppressed because one or more lines are too long

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

View File

@ -18,7 +18,7 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
id("com.android.application") version "8.10.1" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration

View File

@ -99,6 +99,16 @@
"permissionMember": "Member",
"reply": "Reply",
"forward": "Forward",
"repliedTo": "Replied to",
"forwarded": "Forwarded",
"hasAttachments": {
"one": "{} attachment",
"other": "{} attachments"
},
"postHasAttachments": {
"one": "{} attachment",
"other": "{} attachments"
},
"edited": "Edited",
"addVideo": "Add video",
"addPhoto": "Add photo",
@ -278,5 +288,47 @@
"settingsHideBottomNav": "Hide Bottom Navigation",
"settingsSoundEffects": "Sound Effects",
"settingsAprilFoolFeatures": "April Fool Features",
"settingsEnterToSend": "Enter to Send"
"settingsEnterToSend": "Enter to Send",
"settingsTransparentAppBar": "Transparent App Bar",
"settingsCustomFonts": "Custom Fonts",
"settingsCustomFontsHint": "Custom fonts will be used for all text in the app. Make sure it is installed on your device.",
"settingsColorScheme": "Color Scheme",
"postTitle": "Title",
"postDescription": "Description",
"call": "Call",
"done": "Done",
"loginResetPasswordSent": "Password reset link sent, please check your email inbox.",
"accountDeletion": "Delete Account",
"accountDeletionHint": "Are you sure to delete your account? If you confirmed, we will send an confirmation email to your primary email address, you can continue the deletion process by follow the insturctions in the email.",
"accountDeletionSent": "Account deletion confirmation email sent, please check your email inbox.",
"accountSecurityTitle": "Security",
"accountPrivacyTitle": "Privacy",
"accountDangerZoneTitle": "Danger Zone",
"accountPassword": "Password",
"accountPasswordDescription": "Change your account password",
"accountPasswordChange": "Change Password",
"accountPasswordChangeSent": "Password reset link sent, please check your email inbox.",
"accountPasswordChangeDescription": "We will send an email to your primary email address to reset your password.",
"accountTwoFactor": "Two-Factor Authentication",
"accountTwoFactorDescription": "Add an extra layer of security to your account",
"accountTwoFactorSetup": "Set Up 2FA",
"accountTwoFactorSetupDescription": "Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to sign in.",
"accountPrivacy": "Privacy Settings",
"accountPrivacyDescription": "Control who can see your profile and content",
"accountDataExport": "Export Your Data",
"accountDataExportDescription": "Download a copy of your data",
"accountDataExportConfirmation": "We'll prepare an export of your data which may take some time. You'll receive an email when it's ready to download.",
"accountDataExportConfirm": "Request Export",
"accountDataExportRequested": "Data export requested. You'll receive an email when it's ready.",
"accountDeletionDescription": "Permanently delete your account and all your data",
"accountSettingsHelp": "Account Settings Help",
"accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support.",
"unauthorized": "Unauthorized",
"unauthorizedHint": "You're not signed in or session expired, please sign in again.",
"publisherVisitAccountPage": "Visit the profile of {}",
"postVisibility": "Visibility",
"postVisibilityPublic": "Public",
"postVisibilityFriends": "Friends Only",
"postVisibilityUnlisted": "Unlisted",
"postVisibilityPrivate": "Private"
}

View File

@ -99,6 +99,14 @@
"permissionMember": "成员",
"reply": "回复",
"forward": "转发",
"repliedTo": "回复了",
"forwarded": "转发了",
"hasAttachments": {
"other": "{}个附件"
},
"postHasAttachments": {
"other": "{}个附件"
},
"edited": "已编辑",
"addVideo": "添加视频",
"addPhoto": "添加照片",
@ -278,5 +286,10 @@
"settingsHideBottomNav": "隐藏底部导航",
"settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "愚人节功能",
"settingsEnterToSend": "按下 Enter 发送"
"settingsEnterToSend": "按下 Enter 发送",
"postVisibility": "可见性",
"postVisibilityPublic": "公开",
"postVisibilityFriends": "仅好友可见",
"postVisibilityUnlisted": "不公开",
"postVisibilityPrivate": "私密"
}

View File

@ -99,6 +99,14 @@
"permissionMember": "成員",
"reply": "回覆",
"forward": "轉發",
"repliedTo": "回覆了",
"forwarded": "轉發了",
"hasAttachments": {
"other": "{}個附件"
},
"postHasAttachments": {
"other": "{}個附件"
},
"edited": "已編輯",
"addVideo": "新增影片",
"addPhoto": "新增照片",
@ -278,5 +286,10 @@
"settingsHideBottomNav": "隱藏底部導航",
"settingsSoundEffects": "音效",
"settingsAprilFoolFeatures": "愚人節功能",
"settingsEnterToSend": "按下 Enter 傳送"
"settingsEnterToSend": "按下 Enter 傳送",
"postVisibility": "可見性",
"postVisibilityPublic": "公開",
"postVisibilityFriends": "僅好友可見",
"postVisibilityUnlisted": "不公開",
"postVisibilityPrivate": "私密"
}

View File

@ -29,13 +29,21 @@ flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
pod 'Kingfisher', '~> 8.0'
pod 'Alamofire'
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
target 'SolianNotificationService' do
inherit! :search_paths
pod 'Kingfisher', '~> 8.0'
pod 'Alamofire'
end
end
post_install do |installer|

View File

@ -1,4 +1,5 @@
PODS:
- Alamofire (5.10.2)
- connectivity_plus (0.0.1):
- Flutter
- croppy (0.0.1):
@ -89,6 +90,8 @@ PODS:
- flutter_webrtc (0.14.0):
- Flutter
- WebRTC-SDK (= 125.6422.07)
- gal (1.0.0):
- Flutter
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
@ -137,10 +140,14 @@ PODS:
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
- pasteboard (0.0.1):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- PromisesObjC (2.4.0)
- record_ios (1.0.0):
- Flutter
- SAMKeychain (1.5.3)
- SDWebImage (5.21.0):
- SDWebImage/Core (= 5.21.0)
@ -185,6 +192,7 @@ PODS:
- WebRTC-SDK (125.6422.07)
DEPENDENCIES:
- Alamofire
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
@ -197,6 +205,7 @@ DEPENDENCIES:
- flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- gal (from `.symlinks/plugins/gal/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- Kingfisher (~> 8.0)
@ -204,7 +213,9 @@ DEPENDENCIES:
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- record_ios (from `.symlinks/plugins/record_ios/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
@ -215,6 +226,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- Alamofire
- DKImagePickerController
- DKPhotoGallery
- Firebase
@ -259,6 +271,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_udid/ios"
flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios"
gal:
:path: ".symlinks/plugins/gal/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
irondash_engine_context:
@ -271,8 +285,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/media_kit_video/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
pasteboard:
:path: ".symlinks/plugins/pasteboard/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
record_ios:
:path: ".symlinks/plugins/record_ios/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
@ -289,6 +307,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
@ -308,6 +327,7 @@ SPEC CHECKSUMS:
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117
gal: 29e711cd17bccb47f839d9b30afe9bc9750950b2
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
@ -319,8 +339,10 @@ SPEC CHECKSUMS:
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
@ -334,6 +356,6 @@ SPEC CHECKSUMS:
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
WebRTC-SDK: dff00a3892bc570b6014e046297782084071657e
PODFILE CHECKSUM: 2608312fddeea6d787ca3aa478c756da8f4ca66d
PODFILE CHECKSUM: 0c13198c20d0416ef589aeb2e1dac5c50262254f
COCOAPODS: 1.16.2

View File

@ -10,12 +10,15 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
73CDD6812DEC00480059D95D /* SolianNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
73D4264B2DEB815D006C0AAE /* NotifyDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D4264A2DEB815D006C0AAE /* NotifyDelegate.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
755557017FD1B99AFC4F9127 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B87C0E607033790E71B54D73 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6D834CA86410B09796B312B /* Pods_Runner.framework */; };
D1772CE196985AE8E8C9F2E5 /* Pods_SolianNotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */; };
E7A0B456EF7AAA71D1397081 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9AE244813FCDFAA941430393 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */
@ -27,9 +30,27 @@
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
73CDD67F2DEC00480059D95D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 73CDD6792DEC00480059D95D;
remoteInfo = SolianNotificationService;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
73CDD6812DEC00480059D95D /* SolianNotificationService.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@ -47,16 +68,23 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
192FDACE67D7CB6AED15C634 /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
1C14F71D23E4371602065522 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
252A83CE6862573BB856ED8E /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = "<group>"; };
29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2D2457F8B2E6EF9C0F935035 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianNotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
73D4264A2DEB815D006C0AAE /* NotifyDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyDelegate.swift; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.profile.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -66,10 +94,50 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
A499FDB2082EB000933AA8C5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.release.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.release.xcconfig"; sourceTree = "<group>"; };
E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
F6D834CA86410B09796B312B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.debug.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
73CDD6822DEC00480059D95D /* Exceptions for "SolianNotificationService" folder in "SolianNotificationService" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 73CDD6792DEC00480059D95D /* SolianNotificationService */;
};
73CDD68E2DEC00BE0059D95D /* Exceptions for "Services" folder in "SolianNotificationService" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
CloudFile.swift,
DataExchange.swift,
);
target = 73CDD6792DEC00480059D95D /* SolianNotificationService */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
73268D272DEB012A0076E970 /* Services */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
73CDD68E2DEC00BE0059D95D /* Exceptions for "Services" folder in "SolianNotificationService" target */,
);
path = Services;
sourceTree = "<group>";
};
73CDD67B2DEC00480059D95D /* SolianNotificationService */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
73CDD6822DEC00480059D95D /* Exceptions for "SolianNotificationService" folder in "SolianNotificationService" target */,
);
path = SolianNotificationService;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
1DFF8FEBBD0CF5A990600776 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@ -79,6 +147,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
73CDD6772DEC00480059D95D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D1772CE196985AE8E8C9F2E5 /* Pods_SolianNotificationService.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -103,6 +179,8 @@
children = (
F6D834CA86410B09796B312B /* Pods_Runner.framework */,
29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */,
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */,
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -116,6 +194,12 @@
14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */,
14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */,
E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */,
192FDACE67D7CB6AED15C634 /* Pods-NotificationService.debug.xcconfig */,
252A83CE6862573BB856ED8E /* Pods-NotificationService.release.xcconfig */,
2D2457F8B2E6EF9C0F935035 /* Pods-NotificationService.profile.xcconfig */,
F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */,
B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */,
8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@ -136,6 +220,7 @@
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
91E124CE95BCB4DCD890160D /* Pods */,
@ -149,6 +234,7 @@
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */,
);
name = Products;
sourceTree = "<group>";
@ -156,6 +242,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
73268D272DEB012A0076E970 /* Services */,
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
@ -165,6 +252,7 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
73D4264A2DEB815D006C0AAE /* NotifyDelegate.swift */,
);
path = Runner;
sourceTree = "<group>";
@ -191,6 +279,27 @@
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
73CDD6792DEC00480059D95D /* SolianNotificationService */ = {
isa = PBXNativeTarget;
buildConfigurationList = 73CDD6832DEC00480059D95D /* Build configuration list for PBXNativeTarget "SolianNotificationService" */;
buildPhases = (
5FC5C1EF9F17B7166B432FF2 /* [CP] Check Pods Manifest.lock */,
73CDD6762DEC00480059D95D /* Sources */,
73CDD6772DEC00480059D95D /* Frameworks */,
73CDD6782DEC00480059D95D /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
);
name = SolianNotificationService;
productName = SolianNotificationService;
productReference = 73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */;
productType = "com.apple.product-type.app-extension";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
@ -199,6 +308,7 @@
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
@ -208,6 +318,10 @@
buildRules = (
);
dependencies = (
73CDD6802DEC00480059D95D /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
73268D272DEB012A0076E970 /* Services */,
);
name = Runner;
productName = Runner;
@ -221,6 +335,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1640;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@ -228,6 +343,9 @@
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
73CDD6792DEC00480059D95D = {
CreatedOnToolsVersion = 16.4;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
@ -235,7 +353,6 @@
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
@ -243,12 +360,14 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
preferredProjectObjectVersion = 77;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
73CDD6792DEC00480059D95D /* SolianNotificationService */,
);
};
/* End PBXProject section */
@ -261,6 +380,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
73CDD6782DEC00480059D95D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -331,6 +457,28 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
5FC5C1EF9F17B7166B432FF2 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-SolianNotificationService-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
8C0351B03869BBF493808288 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -396,12 +544,20 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
73CDD6762DEC00480059D95D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
73D4264B2DEB815D006C0AAE /* NotifyDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -413,6 +569,11 @@
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
73CDD6802DEC00480059D95D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 73CDD6792DEC00480059D95D /* SolianNotificationService */;
targetProxy = 73CDD67F2DEC00480059D95D /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@ -562,6 +723,123 @@
};
name = Profile;
};
73CDD6842DEC00480059D95D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SolianNotificationService/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianNotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
73CDD6852DEC00480059D95D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SolianNotificationService/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianNotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
73CDD6862DEC00480059D95D /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SolianNotificationService/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianNotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -737,6 +1015,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
73CDD6832DEC00480059D95D /* Build configuration list for PBXNativeTarget "SolianNotificationService" */ = {
isa = XCConfigurationList;
buildConfigurations = (
73CDD6842DEC00480059D95D /* Debug */,
73CDD6852DEC00480059D95D /* Release */,
73CDD6862DEC00480059D95D /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@ -3,11 +3,15 @@ import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
let notifyDelegate = NotifyDelegate()
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
UNUserNotificationCenter.current().delegate = notifyDelegate
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
@ -64,5 +66,10 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
<string>INSendMessageIntent</string>
</array>
</dict>
</plist>

View File

@ -1,153 +0,0 @@
//
// BlurHashDecoder.swift
// Runner
//
// Created by LittleSheep on 2025/4/21.
//
import UIKit
extension UIImage {
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
guard blurHash.count >= 6 else { return nil }
let sizeFlag = String(blurHash[0]).decode83()
let numY = (sizeFlag / 9) + 1
let numX = (sizeFlag % 9) + 1
let quantisedMaximumValue = String(blurHash[1]).decode83()
let maximumValue = Float(quantisedMaximumValue + 1) / 166
guard blurHash.count == 4 + 2 * numX * numY else { return nil }
let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in
if i == 0 {
let value = String(blurHash[2 ..< 6]).decode83()
return decodeDC(value)
} else {
let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83()
return decodeAC(value, maximumValue: maximumValue * punch)
}
}
let width = Int(size.width)
let height = Int(size.height)
let bytesPerRow = width * 3
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
CFDataSetLength(data, bytesPerRow * height)
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }
for y in 0 ..< height {
for x in 0 ..< width {
var r: Float = 0
var g: Float = 0
var b: Float = 0
for j in 0 ..< numY {
for i in 0 ..< numX {
let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height))
let colour = colours[i + j * numX]
r += colour.0 * basis
g += colour.1 * basis
b += colour.2 * basis
}
}
let intR = UInt8(linearTosRGB(r))
let intG = UInt8(linearTosRGB(g))
let intB = UInt8(linearTosRGB(b))
pixels[3 * x + 0 + y * bytesPerRow] = intR
pixels[3 * x + 1 + y * bytesPerRow] = intG
pixels[3 * x + 2 + y * bytesPerRow] = intB
}
}
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
guard let provider = CGDataProvider(data: data) else { return nil }
guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil }
self.init(cgImage: cgImage)
}
}
private func decodeDC(_ value: Int) -> (Float, Float, Float) {
let intR = value >> 16
let intG = (value >> 8) & 255
let intB = value & 255
return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB))
}
private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
let quantR = value / (19 * 19)
let quantG = (value / 19) % 19
let quantB = value % 19
let rgb = (
signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
signPow((Float(quantB) - 9) / 9, 2) * maximumValue
)
return rgb
}
private func signPow(_ value: Float, _ exp: Float) -> Float {
return copysign(pow(abs(value), exp), value)
}
private func linearTosRGB(_ value: Float) -> Int {
let v = max(0, min(1, value))
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) }
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
}
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
let v = Float(Int64(value)) / 255
if v <= 0.04045 { return v / 12.92 }
else { return pow((v + 0.055) / 1.055, 2.4) }
}
private let encodeCharacters: [String] = {
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
}()
private let decodeCharacters: [String: Int] = {
var dict: [String: Int] = [:]
for (index, character) in encodeCharacters.enumerated() {
dict[character] = index
}
return dict
}()
extension String {
func decode83() -> Int {
var value: Int = 0
for character in self {
if let digit = decodeCharacters[String(character)] {
value = value * 83 + digit
}
}
return value
}
}
private extension String {
subscript (offset: Int) -> Character {
return self[index(startIndex, offsetBy: offset)]
}
subscript (bounds: CountableClosedRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start...end]
}
subscript (bounds: CountableRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start..<end]
}
}

View File

@ -1,42 +0,0 @@
//
// ImageView.swift
// Runner
//
// Created by LittleSheep on 2025/4/21.
//
import UIKit
import Kingfisher
class ImageView: UIImageView {
private var _size: CGSize = CGSize(width: 0, height: 0)
// Initialize the image view
override init(frame: CGRect) {
super.init(frame: frame)
contentMode = .scaleAspectFill
clipsToBounds = true
}
required init?(coder: NSCoder) {
super.init(coder: coder)
contentMode = .scaleAspectFill
clipsToBounds = true
}
// Method to set the image from a URL using Kingfisher
func setImage(from url: URL, blurHash: String?) {
let placeholderImage = blurHash != nil ? UIImage.init(blurHash: blurHash!, size: _size) : nil
let processor = DownsamplingImageProcessor(size: _size) |> RoundCornerImageProcessor(cornerRadius: 20)
self.kf.indicatorType = .activity
self.kf.setImage(
with: url,
placeholder: placeholderImage,
options: [
.processor(processor),
.transition(.fade(0.3))
]
)
}
}

View File

@ -1,62 +0,0 @@
//
// NativeImage.swift
// Runner
//
// Created by LittleSheep on 2025/4/21.
//
import Flutter
import UIKit
import Kingfisher
class FLNativeImageFactory : NSObject, FlutterPlatformViewFactory {
private var messenger: FlutterBinaryMessenger
init(messenger: FlutterBinaryMessenger) {
self.messenger = messenger
super.init()
}
func create(
withFrame frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?
) -> FlutterPlatformView {
return FLNativeImage(
frame: frame,
viewIdentifier: viewId,
arguments: args,
binaryMessenger: messenger)
}
/// Implementing this method is only necessary when the `arguments` in `createWithFrame` is not `nil`.
public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
return FlutterStandardMessageCodec.sharedInstance()
}
}
class FLNativeImage : NSObject, FlutterPlatformView {
private var _view: ImageView
init(
frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?,
binaryMessenger messenger: FlutterBinaryMessenger?
) {
_view = ImageView(frame: frame)
super.init()
let argsMap = args as! [AnyHashable: Any]
let source = argsMap["src"] as! String
let blurHash = argsMap["blur"] as? String
if let url = URL(string: source) {
_view.setImage(from: url, blurHash: blurHash)
}
}
func view() -> UIView {
return _view
}
}

View File

@ -0,0 +1,49 @@
//
// NotifyDelegate.swift
// Runner
//
// Created by LittleSheep on 2025/6/1.
//
import Foundation
import Alamofire
class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
if let textResponse = response as? UNTextInputNotificationResponse {
let content = response.notification.request.content
guard let metadata = content.userInfo["meta"] as? [AnyHashable: Any] else {
return
}
var token: String? = UserDefaults.standard.getFlutterToken()
if token == nil {
return
}
let serverUrl = UserDefaults.standard.getServerUrl()
let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages"
let parameters: [String: Any?] = [
"content": textResponse.userText,
"replied_message_id": metadata["message_id"]
]
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: HTTPHeaders(
[HTTPHeader(name: "Authorization", value: "AtField \(token!)")]
))
.validate()
.responseString { response in
switch response.result {
case .success(_):
break
case .failure(let error):
print("Failed to send chat reply message: \(error)")
break
}
}
}
completionHandler()
}
}

View File

@ -4,5 +4,7 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.usernotifications.communication</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,14 @@
//
// CloudFile.swift
// Runner
//
// Created by LittleSheep on 2025/5/31.
//
import Foundation
func getAttachmentUrl(for identifier: String) -> String {
let serverBaseUrl = "https://nt.solian.app"
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/files/\(identifier)"
}

View File

@ -0,0 +1,31 @@
//
// DataExchange.swift
// Runner
//
// Created by LittleSheep on 2025/6/2.
//
import Foundation
extension UserDefaults {
func getFlutterValue<T>(forKey key: String) -> T? {
let prefixedKey = "flutter.\(key)"
return self.object(forKey: prefixedKey) as? T
}
func getFlutterToken(forKey key: String = "dyn_user_tk") -> String? {
let prefixedKey = "flutter.\(key)"
guard let jsonString = self.string(forKey: prefixedKey),
let data = jsonString.data(using: .utf8),
let jsonObject = try? JSONSerialization.jsonObject(with: data),
let jsonDict = jsonObject as? [String: Any],
let token = jsonDict["token"] as? String else {
return nil
}
return token
}
func getServerUrl(forKey key: String = "app_server_url") -> String {
return self.getFlutterValue(forKey: key) ?? "https://nt.solian.app"
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
<string>INSendMessageIntent</string>
</array>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,221 @@
//
// NotificationService.swift
// NotificationService
//
// Created by LittleSheep on 2025/5/31.
//
import UserNotifications
import Intents
import Kingfisher
import UniformTypeIdentifiers
enum ParseNotificationPayloadError: Error {
case missingMetadata(String)
case missingAvatarUrl(String)
}
class NotificationService: UNNotificationServiceExtension {
private var contentHandler: ((UNNotificationContent) -> Void)?
private var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
self.contentHandler = contentHandler
guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
contentHandler(request.content)
return
}
self.bestAttemptContent = bestAttemptContent
do {
try processNotification(request: request, content: bestAttemptContent)
} catch {
contentHandler(bestAttemptContent)
}
}
override func serviceExtensionTimeWillExpire() {
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws {
switch content.userInfo["type"] as? String {
case "messages.new":
try handleMessagingNotification(request: request, content: content)
default:
try handleDefaultNotification(content: content)
}
}
private func handleMessagingNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws {
guard let meta = content.userInfo["meta"] as? [AnyHashable: Any] else {
throw ParseNotificationPayloadError.missingMetadata("The notification has no meta.")
}
let pfpIdentifier = meta["pfp"] as? String
let replyableMessageCategory = UNNotificationCategory(
identifier: content.categoryIdentifier,
actions: [
UNTextInputNotificationAction(
identifier: "reply_action",
title: "Reply",
options: []
),
],
intentIdentifiers: [],
options: []
)
UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory])
content.categoryIdentifier = replyableMessageCategory.identifier
let metaCopy = meta as? [String: Any] ?? [:]
let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil
let targetSize = 512
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
KingfisherManager.shared.retrieveImage(with: URL(string: pfpUrl!)!, options: [.processor(scaleProcessor)], completionHandler: { result in
var image: Data?
switch result {
case .success(let value):
image = value.image.pngData()
case .failure(let error):
print("Unable to get pfp url: \(error)")
}
let handle = INPersonHandle(value: "\(metaCopy["user_id"] ?? "")", type: .unknown)
let sender = INPerson(
personHandle: handle,
nameComponents: PersonNameComponents(nickname: "\(metaCopy["sender_name"] ?? "")"),
displayName: content.title,
image: image == nil ? nil : INImage(imageData: image!),
contactIdentifier: nil,
customIdentifier: nil
)
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
self.donateInteraction(for: intent)
let updatedContent = try? request.content.updating(from: intent)
self.contentHandler?(updatedContent ?? content)
})
}
private func handleDefaultNotification(content: UNMutableNotificationContent) throws {
guard let meta = content.userInfo["meta"] as? [AnyHashable: Any] else {
throw ParseNotificationPayloadError.missingMetadata("The notification has no meta.")
}
if let imageIdentifier = meta["image"] as? String {
attachMedia(to: content, withIdentifier: [imageIdentifier], fileType: UTType.webP, doScaleDown: true)
} else if let pfpIdentifier = meta["pfp"] as? String {
attachMedia(to: content, withIdentifier: [pfpIdentifier], fileType: UTType.webP, doScaleDown: true)
} else if let imagesIdentifier = meta["images"] as? Array<String> {
attachMedia(to: content, withIdentifier: imagesIdentifier, fileType: UTType.webP, doScaleDown: true)
} else {
contentHandler?(content)
}
}
private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: Array<String>, fileType type: UTType?, doScaleDown scaleDown: Bool = false) {
let attachmentUrls = identifier.compactMap { element in
return getAttachmentUrl(for: element)
}
guard !attachmentUrls.isEmpty else {
print("Invalid URLs for attachments: \(attachmentUrls)")
return
}
let targetSize = 512
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
for attachmentUrl in attachmentUrls {
guard let remoteUrl = URL(string: attachmentUrl) else {
print("Invalid URL for attachment: \(attachmentUrl)")
continue // Skip this URL and move to the next one
}
KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [
.processor(scaleProcessor)
] : nil) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let retrievalResult):
// The image is either retrieved from cache or downloaded
let tempDirectory = FileManager.default.temporaryDirectory
let cachedFileUrl = tempDirectory.appendingPathComponent(UUID().uuidString) // Unique identifier for each file
do {
// Write the image data to a temporary file for UNNotificationAttachment
try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: attachmentUrl)
} catch {
print("Failed to write media to temporary file: \(error.localizedDescription)")
self.contentHandler?(content)
}
case .failure(let error):
print("Failed to retrieve image: \(error.localizedDescription)")
self.contentHandler?(content)
}
}
}
}
private func attachLocalMedia(to content: UNMutableNotificationContent, fileType type: String?, from localUrl: URL, withIdentifier identifier: String) {
do {
let attachment = try UNNotificationAttachment(identifier: identifier, url: localUrl, options: [
UNNotificationAttachmentOptionsTypeHintKey: type as Any,
UNNotificationAttachmentOptionsThumbnailHiddenKey: 0,
])
content.attachments = [attachment]
} catch let error as NSError {
// Log detailed error information
print("Failed to create attachment from file at \(localUrl.path)")
print("Error: \(error.localizedDescription)")
// Check specific error codes if needed
if error.domain == NSCocoaErrorDomain {
switch error.code {
case NSFileReadNoSuchFileError:
print("File does not exist at \(localUrl.path)")
case NSFileReadNoPermissionError:
print("No permission to read file at \(localUrl.path)")
default:
print("Unhandled file error: \(error.code)")
}
}
}
// Call content handler regardless of success or failure
self.contentHandler?(content)
}
private func createMessageIntent(with sender: INPerson, meta: [AnyHashable: Any], body: String) -> INSendMessageIntent {
INSendMessageIntent(
recipients: nil,
outgoingMessageType: .outgoingMessageText,
content: body,
speakableGroupName: meta["room_name"] != nil ? INSpeakableString(spokenPhrase: meta["room_name"] as! String) : nil,
conversationIdentifier: "\(meta["room_id"] ?? "")",
serviceName: nil,
sender: sender,
attachments: nil
)
}
private func donateInteraction(for intent: INIntent) {
let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .incoming
interaction.donate(completion: nil)
}
}

View File

@ -186,7 +186,7 @@ class MessageRepository {
}
Future<LocalChatMessage> sendMessage(
String atk,
String token,
String baseUrl,
String roomId,
String content,
@ -231,8 +231,8 @@ class MessageRepository {
for (var idx = 0; idx < attachments.length; idx++) {
final cloudFile =
await putMediaToCloud(
fileData: attachments[idx].data,
atk: atk,
fileData: attachments[idx],
atk: token,
baseUrl: baseUrl,
filename: attachments[idx].data.name ?? 'Post media',
mimetype:

View File

@ -4,14 +4,11 @@ part 'auth.freezed.dart';
part 'auth.g.dart';
@freezed
sealed class AppTokenPair with _$AppTokenPair {
const factory AppTokenPair({
required String accessToken,
required String refreshToken,
}) = _AppTokenPair;
sealed class AppToken with _$AppToken {
const factory AppToken({required String token}) = _AppToken;
factory AppTokenPair.fromJson(Map<String, dynamic> json) =>
_$AppTokenPairFromJson(json);
factory AppToken.fromJson(Map<String, dynamic> json) =>
_$AppTokenFromJson(json);
}
@freezed

View File

@ -14,42 +14,42 @@ part of 'auth.dart';
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AppTokenPair {
mixin _$AppToken {
String get accessToken; String get refreshToken;
/// Create a copy of AppTokenPair
String get token;
/// Create a copy of AppToken
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AppTokenPairCopyWith<AppTokenPair> get copyWith => _$AppTokenPairCopyWithImpl<AppTokenPair>(this as AppTokenPair, _$identity);
$AppTokenCopyWith<AppToken> get copyWith => _$AppTokenCopyWithImpl<AppToken>(this as AppToken, _$identity);
/// Serializes this AppTokenPair to a JSON map.
/// Serializes this AppToken to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppTokenPair&&(identical(other.accessToken, accessToken) || other.accessToken == accessToken)&&(identical(other.refreshToken, refreshToken) || other.refreshToken == refreshToken));
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppToken&&(identical(other.token, token) || other.token == token));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,accessToken,refreshToken);
int get hashCode => Object.hash(runtimeType,token);
@override
String toString() {
return 'AppTokenPair(accessToken: $accessToken, refreshToken: $refreshToken)';
return 'AppToken(token: $token)';
}
}
/// @nodoc
abstract mixin class $AppTokenPairCopyWith<$Res> {
factory $AppTokenPairCopyWith(AppTokenPair value, $Res Function(AppTokenPair) _then) = _$AppTokenPairCopyWithImpl;
abstract mixin class $AppTokenCopyWith<$Res> {
factory $AppTokenCopyWith(AppToken value, $Res Function(AppToken) _then) = _$AppTokenCopyWithImpl;
@useResult
$Res call({
String accessToken, String refreshToken
String token
});
@ -57,19 +57,18 @@ $Res call({
}
/// @nodoc
class _$AppTokenPairCopyWithImpl<$Res>
implements $AppTokenPairCopyWith<$Res> {
_$AppTokenPairCopyWithImpl(this._self, this._then);
class _$AppTokenCopyWithImpl<$Res>
implements $AppTokenCopyWith<$Res> {
_$AppTokenCopyWithImpl(this._self, this._then);
final AppTokenPair _self;
final $Res Function(AppTokenPair) _then;
final AppToken _self;
final $Res Function(AppToken) _then;
/// Create a copy of AppTokenPair
/// Create a copy of AppToken
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? accessToken = null,Object? refreshToken = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? token = null,}) {
return _then(_self.copyWith(
accessToken: null == accessToken ? _self.accessToken : accessToken // ignore: cast_nullable_to_non_nullable
as String,refreshToken: null == refreshToken ? _self.refreshToken : refreshToken // ignore: cast_nullable_to_non_nullable
token: null == token ? _self.token : token // ignore: cast_nullable_to_non_nullable
as String,
));
}
@ -80,47 +79,46 @@ as String,
/// @nodoc
@JsonSerializable()
class _AppTokenPair implements AppTokenPair {
const _AppTokenPair({required this.accessToken, required this.refreshToken});
factory _AppTokenPair.fromJson(Map<String, dynamic> json) => _$AppTokenPairFromJson(json);
class _AppToken implements AppToken {
const _AppToken({required this.token});
factory _AppToken.fromJson(Map<String, dynamic> json) => _$AppTokenFromJson(json);
@override final String accessToken;
@override final String refreshToken;
@override final String token;
/// Create a copy of AppTokenPair
/// Create a copy of AppToken
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$AppTokenPairCopyWith<_AppTokenPair> get copyWith => __$AppTokenPairCopyWithImpl<_AppTokenPair>(this, _$identity);
_$AppTokenCopyWith<_AppToken> get copyWith => __$AppTokenCopyWithImpl<_AppToken>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$AppTokenPairToJson(this, );
return _$AppTokenToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppTokenPair&&(identical(other.accessToken, accessToken) || other.accessToken == accessToken)&&(identical(other.refreshToken, refreshToken) || other.refreshToken == refreshToken));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppToken&&(identical(other.token, token) || other.token == token));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,accessToken,refreshToken);
int get hashCode => Object.hash(runtimeType,token);
@override
String toString() {
return 'AppTokenPair(accessToken: $accessToken, refreshToken: $refreshToken)';
return 'AppToken(token: $token)';
}
}
/// @nodoc
abstract mixin class _$AppTokenPairCopyWith<$Res> implements $AppTokenPairCopyWith<$Res> {
factory _$AppTokenPairCopyWith(_AppTokenPair value, $Res Function(_AppTokenPair) _then) = __$AppTokenPairCopyWithImpl;
abstract mixin class _$AppTokenCopyWith<$Res> implements $AppTokenCopyWith<$Res> {
factory _$AppTokenCopyWith(_AppToken value, $Res Function(_AppToken) _then) = __$AppTokenCopyWithImpl;
@override @useResult
$Res call({
String accessToken, String refreshToken
String token
});
@ -128,19 +126,18 @@ $Res call({
}
/// @nodoc
class __$AppTokenPairCopyWithImpl<$Res>
implements _$AppTokenPairCopyWith<$Res> {
__$AppTokenPairCopyWithImpl(this._self, this._then);
class __$AppTokenCopyWithImpl<$Res>
implements _$AppTokenCopyWith<$Res> {
__$AppTokenCopyWithImpl(this._self, this._then);
final _AppTokenPair _self;
final $Res Function(_AppTokenPair) _then;
final _AppToken _self;
final $Res Function(_AppToken) _then;
/// Create a copy of AppTokenPair
/// Create a copy of AppToken
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? accessToken = null,Object? refreshToken = null,}) {
return _then(_AppTokenPair(
accessToken: null == accessToken ? _self.accessToken : accessToken // ignore: cast_nullable_to_non_nullable
as String,refreshToken: null == refreshToken ? _self.refreshToken : refreshToken // ignore: cast_nullable_to_non_nullable
@override @pragma('vm:prefer-inline') $Res call({Object? token = null,}) {
return _then(_AppToken(
token: null == token ? _self.token : token // ignore: cast_nullable_to_non_nullable
as String,
));
}

View File

@ -6,17 +6,12 @@ part of 'auth.dart';
// JsonSerializableGenerator
// **************************************************************************
_AppTokenPair _$AppTokenPairFromJson(Map<String, dynamic> json) =>
_AppTokenPair(
accessToken: json['access_token'] as String,
refreshToken: json['refresh_token'] as String,
);
_AppToken _$AppTokenFromJson(Map<String, dynamic> json) =>
_AppToken(token: json['token'] as String);
Map<String, dynamic> _$AppTokenPairToJson(_AppTokenPair instance) =>
<String, dynamic>{
'access_token': instance.accessToken,
'refresh_token': instance.refreshToken,
};
Map<String, dynamic> _$AppTokenToJson(_AppToken instance) => <String, dynamic>{
'token': instance.token,
};
_SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
_SnAuthChallenge(

View File

@ -14,9 +14,7 @@ sealed class SnChatRoom with _$SnChatRoom {
required String? description,
required int type,
required bool isPublic,
required String? pictureId,
required SnCloudFile? picture,
required String? backgroundId,
required SnCloudFile? background,
required String? realmId,
required SnRealm? realm,

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnChatRoom {
String get id; String? get name; String? get description; int get type; bool get isPublic; String? get pictureId; SnCloudFile? get picture; String? get backgroundId; SnCloudFile? get background; String? get realmId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnChatMember>? get members;
String get id; String? get name; String? get description; int get type; bool get isPublic; SnCloudFile? get picture; SnCloudFile? get background; String? get realmId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnChatMember>? get members;
/// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -29,16 +29,16 @@ $SnChatRoomCopyWith<SnChatRoom> get copyWith => _$SnChatRoomCopyWithImpl<SnChatR
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.members, members));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.members, members));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,pictureId,picture,backgroundId,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(members));
int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,picture,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(members));
@override
String toString() {
return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)';
return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, picture: $picture, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)';
}
@ -49,7 +49,7 @@ abstract mixin class $SnChatRoomCopyWith<$Res> {
factory $SnChatRoomCopyWith(SnChatRoom value, $Res Function(SnChatRoom) _then) = _$SnChatRoomCopyWithImpl;
@useResult
$Res call({
String id, String? name, String? description, int type, bool isPublic, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members
String id, String? name, String? description, int type, bool isPublic, SnCloudFile? picture, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members
});
@ -66,17 +66,15 @@ class _$SnChatRoomCopyWithImpl<$Res>
/// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable
as bool,pictureId: freezed == pictureId ? _self.pictureId : pictureId // ignore: cast_nullable_to_non_nullable
as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,backgroundId: freezed == backgroundId ? _self.backgroundId : backgroundId // ignore: cast_nullable_to_non_nullable
as String?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as bool,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
as String?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_to_non_nullable
as SnRealm?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
@ -130,7 +128,7 @@ $SnRealmCopyWith<$Res>? get realm {
@JsonSerializable()
class _SnChatRoom implements SnChatRoom {
const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, required this.isPublic, required this.pictureId, required this.picture, required this.backgroundId, required this.background, required this.realmId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final List<SnChatMember>? members}): _members = members;
const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, required this.isPublic, required this.picture, required this.background, required this.realmId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final List<SnChatMember>? members}): _members = members;
factory _SnChatRoom.fromJson(Map<String, dynamic> json) => _$SnChatRoomFromJson(json);
@override final String id;
@ -138,9 +136,7 @@ class _SnChatRoom implements SnChatRoom {
@override final String? description;
@override final int type;
@override final bool isPublic;
@override final String? pictureId;
@override final SnCloudFile? picture;
@override final String? backgroundId;
@override final SnCloudFile? background;
@override final String? realmId;
@override final SnRealm? realm;
@ -170,16 +166,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._members, _members));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._members, _members));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,pictureId,picture,backgroundId,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_members));
int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,picture,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_members));
@override
String toString() {
return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)';
return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, picture: $picture, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)';
}
@ -190,7 +186,7 @@ abstract mixin class _$SnChatRoomCopyWith<$Res> implements $SnChatRoomCopyWith<$
factory _$SnChatRoomCopyWith(_SnChatRoom value, $Res Function(_SnChatRoom) _then) = __$SnChatRoomCopyWithImpl;
@override @useResult
$Res call({
String id, String? name, String? description, int type, bool isPublic, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members
String id, String? name, String? description, int type, bool isPublic, SnCloudFile? picture, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnChatMember>? members
});
@ -207,17 +203,15 @@ class __$SnChatRoomCopyWithImpl<$Res>
/// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) {
return _then(_SnChatRoom(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable
as bool,pictureId: freezed == pictureId ? _self.pictureId : pictureId // ignore: cast_nullable_to_non_nullable
as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,backgroundId: freezed == backgroundId ? _self.backgroundId : backgroundId // ignore: cast_nullable_to_non_nullable
as String?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as bool,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
as String?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_to_non_nullable
as SnRealm?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable

View File

@ -12,12 +12,10 @@ _SnChatRoom _$SnChatRoomFromJson(Map<String, dynamic> json) => _SnChatRoom(
description: json['description'] as String?,
type: (json['type'] as num).toInt(),
isPublic: json['is_public'] as bool,
pictureId: json['picture_id'] as String?,
picture:
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
backgroundId: json['background_id'] as String?,
background:
json['background'] == null
? null
@ -46,9 +44,7 @@ Map<String, dynamic> _$SnChatRoomToJson(_SnChatRoom instance) =>
'description': instance.description,
'type': instance.type,
'is_public': instance.isPublic,
'picture_id': instance.pictureId,
'picture': instance.picture?.toJson(),
'background_id': instance.backgroundId,
'background': instance.background?.toJson(),
'realm_id': instance.realmId,
'realm': instance.realm?.toJson(),

View File

@ -43,7 +43,6 @@ sealed class SnCloudFile with _$SnCloudFile {
required int size,
required DateTime? uploadedAt,
required String? uploadedTo,
required int usedCount,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,

View File

@ -146,7 +146,7 @@ as UniversalFileType,
/// @nodoc
mixin _$SnCloudFile {
String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; String? get uploadedTo; int get usedCount; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; String? get uploadedTo; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -159,16 +159,16 @@ $SnCloudFileCopyWith<SnCloudFile> get copyWith => _$SnCloudFileCopyWithImpl<SnCl
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.fileMeta, fileMeta)&&const DeepCollectionEquality().equals(other.userMeta, userMeta)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.usedCount, usedCount) || other.usedCount == usedCount)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.fileMeta, fileMeta)&&const DeepCollectionEquality().equals(other.userMeta, userMeta)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(fileMeta),const DeepCollectionEquality().hash(userMeta),mimeType,hash,size,uploadedAt,uploadedTo,usedCount,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(fileMeta),const DeepCollectionEquality().hash(userMeta),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, usedCount: $usedCount, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@ -179,7 +179,7 @@ abstract mixin class $SnCloudFileCopyWith<$Res> {
factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl;
@useResult
$Res call({
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, int usedCount, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@ -196,7 +196,7 @@ class _$SnCloudFileCopyWithImpl<$Res>
/// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? usedCount = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@ -208,8 +208,7 @@ as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to
as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
as int,uploadedAt: freezed == uploadedAt ? _self.uploadedAt : uploadedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,uploadedTo: freezed == uploadedTo ? _self.uploadedTo : uploadedTo // ignore: cast_nullable_to_non_nullable
as String?,usedCount: null == usedCount ? _self.usedCount : usedCount // ignore: cast_nullable_to_non_nullable
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as String?,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?,
@ -223,7 +222,7 @@ as DateTime?,
@JsonSerializable()
class _SnCloudFile implements SnCloudFile {
const _SnCloudFile({required this.id, required this.name, required this.description, required final Map<String, dynamic>? fileMeta, required final Map<String, dynamic>? userMeta, required this.mimeType, required this.hash, required this.size, required this.uploadedAt, required this.uploadedTo, required this.usedCount, required this.createdAt, required this.updatedAt, required this.deletedAt}): _fileMeta = fileMeta,_userMeta = userMeta;
const _SnCloudFile({required this.id, required this.name, required this.description, required final Map<String, dynamic>? fileMeta, required final Map<String, dynamic>? userMeta, required this.mimeType, required this.hash, required this.size, required this.uploadedAt, required this.uploadedTo, required this.createdAt, required this.updatedAt, required this.deletedAt}): _fileMeta = fileMeta,_userMeta = userMeta;
factory _SnCloudFile.fromJson(Map<String, dynamic> json) => _$SnCloudFileFromJson(json);
@override final String id;
@ -252,7 +251,6 @@ class _SnCloudFile implements SnCloudFile {
@override final int size;
@override final DateTime? uploadedAt;
@override final String? uploadedTo;
@override final int usedCount;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
@ -270,16 +268,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._fileMeta, _fileMeta)&&const DeepCollectionEquality().equals(other._userMeta, _userMeta)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.usedCount, usedCount) || other.usedCount == usedCount)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._fileMeta, _fileMeta)&&const DeepCollectionEquality().equals(other._userMeta, _userMeta)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_fileMeta),const DeepCollectionEquality().hash(_userMeta),mimeType,hash,size,uploadedAt,uploadedTo,usedCount,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_fileMeta),const DeepCollectionEquality().hash(_userMeta),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, usedCount: $usedCount, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@ -290,7 +288,7 @@ abstract mixin class _$SnCloudFileCopyWith<$Res> implements $SnCloudFileCopyWith
factory _$SnCloudFileCopyWith(_SnCloudFile value, $Res Function(_SnCloudFile) _then) = __$SnCloudFileCopyWithImpl;
@override @useResult
$Res call({
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, int usedCount, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@ -307,7 +305,7 @@ class __$SnCloudFileCopyWithImpl<$Res>
/// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? usedCount = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnCloudFile(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@ -319,8 +317,7 @@ as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to
as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
as int,uploadedAt: freezed == uploadedAt ? _self.uploadedAt : uploadedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,uploadedTo: freezed == uploadedTo ? _self.uploadedTo : uploadedTo // ignore: cast_nullable_to_non_nullable
as String?,usedCount: null == usedCount ? _self.usedCount : usedCount // ignore: cast_nullable_to_non_nullable
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as String?,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?,

View File

@ -20,7 +20,6 @@ _SnCloudFile _$SnCloudFileFromJson(Map<String, dynamic> json) => _SnCloudFile(
? null
: DateTime.parse(json['uploaded_at'] as String),
uploadedTo: json['uploaded_to'] as String?,
usedCount: (json['used_count'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
@ -41,7 +40,6 @@ Map<String, dynamic> _$SnCloudFileToJson(_SnCloudFile instance) =>
'size': instance.size,
'uploaded_at': instance.uploadedAt?.toIso8601String(),
'uploaded_to': instance.uploadedTo,
'used_count': instance.usedCount,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),

View File

@ -1,5 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/user.dart';
part 'post.freezed.dart';
part 'post.g.dart';
@ -50,10 +51,9 @@ sealed class SnPublisher with _$SnPublisher {
required String name,
required String nick,
@Default('') String bio,
required String? pictureId,
required SnCloudFile? picture,
required String? backgroundId,
required SnCloudFile? background,
required SnAccount? account,
required String? accountId,
required DateTime createdAt,
required DateTime updatedAt,

View File

@ -370,7 +370,7 @@ $SnPublisherCopyWith<$Res> get publisher {
/// @nodoc
mixin _$SnPublisher {
String get id; int get type; String get name; String get nick; String get bio; String? get pictureId; SnCloudFile? get picture; String? get backgroundId; SnCloudFile? get background; String? get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String? get realmId;
String get id; int get type; String get name; String get nick; String get bio; SnCloudFile? get picture; SnCloudFile? get background; SnAccount? get account; String? get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String? get realmId;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -383,16 +383,16 @@ $SnPublisherCopyWith<SnPublisher> get copyWith => _$SnPublisherCopyWithImpl<SnPu
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,pictureId,picture,backgroundId,background,accountId,createdAt,updatedAt,deletedAt,realmId);
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId)';
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId)';
}
@ -403,11 +403,11 @@ abstract mixin class $SnPublisherCopyWith<$Res> {
factory $SnPublisherCopyWith(SnPublisher value, $Res Function(SnPublisher) _then) = _$SnPublisherCopyWithImpl;
@useResult
$Res call({
String id, int type, String name, String nick, String bio, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, String? accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? realmId
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? realmId
});
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnAccountCopyWith<$Res>? get account;
}
/// @nodoc
@ -420,18 +420,17 @@ class _$SnPublisherCopyWithImpl<$Res>
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? accountId = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,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,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,pictureId: freezed == pictureId ? _self.pictureId : pictureId // ignore: cast_nullable_to_non_nullable
as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,backgroundId: freezed == backgroundId ? _self.backgroundId : backgroundId // ignore: cast_nullable_to_non_nullable
as String?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,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
@ -463,6 +462,18 @@ $SnCloudFileCopyWith<$Res>? get background {
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
}
@ -471,7 +482,7 @@ $SnCloudFileCopyWith<$Res>? get background {
@JsonSerializable()
class _SnPublisher implements SnPublisher {
const _SnPublisher({required this.id, required this.type, required this.name, required this.nick, this.bio = '', required this.pictureId, required this.picture, required this.backgroundId, required this.background, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt, required this.realmId});
const _SnPublisher({required this.id, required this.type, required this.name, required this.nick, this.bio = '', required this.picture, required this.background, required this.account, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt, required this.realmId});
factory _SnPublisher.fromJson(Map<String, dynamic> json) => _$SnPublisherFromJson(json);
@override final String id;
@ -479,10 +490,9 @@ class _SnPublisher implements SnPublisher {
@override final String name;
@override final String nick;
@override@JsonKey() final String bio;
@override final String? pictureId;
@override final SnCloudFile? picture;
@override final String? backgroundId;
@override final SnCloudFile? background;
@override final SnAccount? account;
@override final String? accountId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@ -502,16 +512,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,pictureId,picture,backgroundId,background,accountId,createdAt,updatedAt,deletedAt,realmId);
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId)';
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId)';
}
@ -522,11 +532,11 @@ abstract mixin class _$SnPublisherCopyWith<$Res> implements $SnPublisherCopyWith
factory _$SnPublisherCopyWith(_SnPublisher value, $Res Function(_SnPublisher) _then) = __$SnPublisherCopyWithImpl;
@override @useResult
$Res call({
String id, int type, String name, String nick, String bio, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, String? accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? realmId
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? realmId
});
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnAccountCopyWith<$Res>? get account;
}
/// @nodoc
@ -539,18 +549,17 @@ class __$SnPublisherCopyWithImpl<$Res>
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? accountId = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,}) {
return _then(_SnPublisher(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,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,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,pictureId: freezed == pictureId ? _self.pictureId : pictureId // ignore: cast_nullable_to_non_nullable
as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,backgroundId: freezed == backgroundId ? _self.backgroundId : backgroundId // ignore: cast_nullable_to_non_nullable
as String?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,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
@ -583,6 +592,18 @@ $SnCloudFileCopyWith<$Res>? get background {
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
}

View File

@ -100,16 +100,18 @@ _SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
name: json['name'] as String,
nick: json['nick'] as String,
bio: json['bio'] as String? ?? '',
pictureId: json['picture_id'] as String?,
picture:
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
backgroundId: json['background_id'] as String?,
background:
json['background'] == null
? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
account:
json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
accountId: json['account_id'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
@ -127,10 +129,9 @@ Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
'name': instance.name,
'nick': instance.nick,
'bio': instance.bio,
'picture_id': instance.pictureId,
'picture': instance.picture?.toJson(),
'background_id': instance.backgroundId,
'background': instance.background?.toJson(),
'account': instance.account?.toJson(),
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),

View File

@ -16,9 +16,7 @@ sealed class SnRealm with _$SnRealm {
required DateTime? verifiedAt,
required bool isCommunity,
required bool isPublic,
required String? pictureId,
required SnCloudFile? picture,
required String? backgroundId,
required SnCloudFile? background,
required String accountId,
required DateTime createdAt,

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnRealm {
String get id; String get slug; String get name; String get description; String? get verifiedAs; DateTime? get verifiedAt; bool get isCommunity; bool get isPublic; String? get pictureId; SnCloudFile? get picture; String? get backgroundId; SnCloudFile? get background; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String get slug; String get name; String get description; String? get verifiedAs; DateTime? get verifiedAt; bool get isCommunity; bool get isPublic; SnCloudFile? get picture; SnCloudFile? get background; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnRealm
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -29,16 +29,16 @@ $SnRealmCopyWith<SnRealm> get copyWith => _$SnRealmCopyWithImpl<SnRealm>(this as
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnRealm&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.verifiedAs, verifiedAs) || other.verifiedAs == verifiedAs)&&(identical(other.verifiedAt, verifiedAt) || other.verifiedAt == verifiedAt)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnRealm&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.verifiedAs, verifiedAs) || other.verifiedAs == verifiedAs)&&(identical(other.verifiedAt, verifiedAt) || other.verifiedAt == verifiedAt)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,description,verifiedAs,verifiedAt,isCommunity,isPublic,pictureId,picture,backgroundId,background,accountId,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,slug,name,description,verifiedAs,verifiedAt,isCommunity,isPublic,picture,background,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnRealm(id: $id, slug: $slug, name: $name, description: $description, verifiedAs: $verifiedAs, verifiedAt: $verifiedAt, isCommunity: $isCommunity, isPublic: $isPublic, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnRealm(id: $id, slug: $slug, name: $name, description: $description, verifiedAs: $verifiedAs, verifiedAt: $verifiedAt, isCommunity: $isCommunity, isPublic: $isPublic, picture: $picture, background: $background, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@ -49,7 +49,7 @@ abstract mixin class $SnRealmCopyWith<$Res> {
factory $SnRealmCopyWith(SnRealm value, $Res Function(SnRealm) _then) = _$SnRealmCopyWithImpl;
@useResult
$Res call({
String id, String slug, String name, String description, String? verifiedAs, DateTime? verifiedAt, bool isCommunity, bool isPublic, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String slug, String name, String description, String? verifiedAs, DateTime? verifiedAt, bool isCommunity, bool isPublic, SnCloudFile? picture, SnCloudFile? background, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@ -66,7 +66,7 @@ class _$SnRealmCopyWithImpl<$Res>
/// Create a copy of SnRealm
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = null,Object? verifiedAs = freezed,Object? verifiedAt = freezed,Object? isCommunity = null,Object? isPublic = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = null,Object? verifiedAs = freezed,Object? verifiedAt = freezed,Object? isCommunity = null,Object? isPublic = null,Object? picture = freezed,Object? background = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
@ -76,10 +76,8 @@ as String,verifiedAs: freezed == verifiedAs ? _self.verifiedAs : verifiedAs // i
as String?,verifiedAt: freezed == verifiedAt ? _self.verifiedAt : verifiedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isCommunity: null == isCommunity ? _self.isCommunity : isCommunity // ignore: cast_nullable_to_non_nullable
as bool,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable
as bool,pictureId: freezed == pictureId ? _self.pictureId : pictureId // ignore: cast_nullable_to_non_nullable
as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,backgroundId: freezed == backgroundId ? _self.backgroundId : backgroundId // ignore: cast_nullable_to_non_nullable
as String?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as bool,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,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
@ -119,7 +117,7 @@ $SnCloudFileCopyWith<$Res>? get background {
@JsonSerializable()
class _SnRealm implements SnRealm {
const _SnRealm({required this.id, required this.slug, required this.name, required this.description, required this.verifiedAs, required this.verifiedAt, required this.isCommunity, required this.isPublic, required this.pictureId, required this.picture, required this.backgroundId, required this.background, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
const _SnRealm({required this.id, required this.slug, required this.name, required this.description, required this.verifiedAs, required this.verifiedAt, required this.isCommunity, required this.isPublic, required this.picture, required this.background, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnRealm.fromJson(Map<String, dynamic> json) => _$SnRealmFromJson(json);
@override final String id;
@ -130,9 +128,7 @@ class _SnRealm implements SnRealm {
@override final DateTime? verifiedAt;
@override final bool isCommunity;
@override final bool isPublic;
@override final String? pictureId;
@override final SnCloudFile? picture;
@override final String? backgroundId;
@override final SnCloudFile? background;
@override final String accountId;
@override final DateTime createdAt;
@ -152,16 +148,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnRealm&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.verifiedAs, verifiedAs) || other.verifiedAs == verifiedAs)&&(identical(other.verifiedAt, verifiedAt) || other.verifiedAt == verifiedAt)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnRealm&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.verifiedAs, verifiedAs) || other.verifiedAs == verifiedAs)&&(identical(other.verifiedAt, verifiedAt) || other.verifiedAt == verifiedAt)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,description,verifiedAs,verifiedAt,isCommunity,isPublic,pictureId,picture,backgroundId,background,accountId,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,slug,name,description,verifiedAs,verifiedAt,isCommunity,isPublic,picture,background,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnRealm(id: $id, slug: $slug, name: $name, description: $description, verifiedAs: $verifiedAs, verifiedAt: $verifiedAt, isCommunity: $isCommunity, isPublic: $isPublic, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnRealm(id: $id, slug: $slug, name: $name, description: $description, verifiedAs: $verifiedAs, verifiedAt: $verifiedAt, isCommunity: $isCommunity, isPublic: $isPublic, picture: $picture, background: $background, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@ -172,7 +168,7 @@ abstract mixin class _$SnRealmCopyWith<$Res> implements $SnRealmCopyWith<$Res> {
factory _$SnRealmCopyWith(_SnRealm value, $Res Function(_SnRealm) _then) = __$SnRealmCopyWithImpl;
@override @useResult
$Res call({
String id, String slug, String name, String description, String? verifiedAs, DateTime? verifiedAt, bool isCommunity, bool isPublic, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String slug, String name, String description, String? verifiedAs, DateTime? verifiedAt, bool isCommunity, bool isPublic, SnCloudFile? picture, SnCloudFile? background, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@ -189,7 +185,7 @@ class __$SnRealmCopyWithImpl<$Res>
/// Create a copy of SnRealm
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = null,Object? verifiedAs = freezed,Object? verifiedAt = freezed,Object? isCommunity = null,Object? isPublic = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = null,Object? verifiedAs = freezed,Object? verifiedAt = freezed,Object? isCommunity = null,Object? isPublic = null,Object? picture = freezed,Object? background = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnRealm(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
@ -199,10 +195,8 @@ as String,verifiedAs: freezed == verifiedAs ? _self.verifiedAs : verifiedAs // i
as String?,verifiedAt: freezed == verifiedAt ? _self.verifiedAt : verifiedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isCommunity: null == isCommunity ? _self.isCommunity : isCommunity // ignore: cast_nullable_to_non_nullable
as bool,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable
as bool,pictureId: freezed == pictureId ? _self.pictureId : pictureId // ignore: cast_nullable_to_non_nullable
as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,backgroundId: freezed == backgroundId ? _self.backgroundId : backgroundId // ignore: cast_nullable_to_non_nullable
as String?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as bool,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,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

View File

@ -18,12 +18,10 @@ _SnRealm _$SnRealmFromJson(Map<String, dynamic> json) => _SnRealm(
: DateTime.parse(json['verified_at'] as String),
isCommunity: json['is_community'] as bool,
isPublic: json['is_public'] as bool,
pictureId: json['picture_id'] as String?,
picture:
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
backgroundId: json['background_id'] as String?,
background:
json['background'] == null
? null
@ -46,9 +44,7 @@ Map<String, dynamic> _$SnRealmToJson(_SnRealm instance) => <String, dynamic>{
'verified_at': instance.verifiedAt?.toIso8601String(),
'is_community': instance.isCommunity,
'is_public': instance.isPublic,
'picture_id': instance.pictureId,
'picture': instance.picture?.toJson(),
'background_id': instance.backgroundId,
'background': instance.background?.toJson(),
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),

View File

@ -31,12 +31,10 @@ sealed class SnAccountProfile with _$SnAccountProfile {
required String? middleName,
required String? lastName,
@Default('') String bio,
required String? pictureId,
required int experience,
required int level,
required double levelingProgress,
required SnCloudFile? picture,
required String? backgroundId,
required SnCloudFile? background,
required DateTime createdAt,
required DateTime updatedAt,

View File

@ -200,7 +200,7 @@ $SnAccountProfileCopyWith<$Res> get profile {
/// @nodoc
mixin _$SnAccountProfile {
String get id; String? get firstName; String? get middleName; String? get lastName; String get bio; String? get pictureId; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; String? get backgroundId; SnCloudFile? get background; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String? get firstName; String? get middleName; String? get lastName; String get bio; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -213,16 +213,16 @@ $SnAccountProfileCopyWith<SnAccountProfile> get copyWith => _$SnAccountProfileCo
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,firstName,middleName,lastName,bio,pictureId,experience,level,levelingProgress,picture,backgroundId,background,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,firstName,middleName,lastName,bio,experience,level,levelingProgress,picture,background,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, pictureId: $pictureId, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, backgroundId: $backgroundId, background: $background, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@ -233,7 +233,7 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> {
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
@useResult
$Res call({
String id, String? firstName, String? middleName, String? lastName, String bio, String? pictureId, int experience, int level, double levelingProgress, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String? firstName, String? middleName, String? lastName, String bio, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@ -250,20 +250,18 @@ class _$SnAccountProfileCopyWithImpl<$Res>
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = freezed,Object? middleName = freezed,Object? lastName = freezed,Object? bio = null,Object? pictureId = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = freezed,Object? middleName = freezed,Object? lastName = freezed,Object? bio = null,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,firstName: freezed == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String?,middleName: freezed == middleName ? _self.middleName : middleName // ignore: cast_nullable_to_non_nullable
as String?,lastName: freezed == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable
as String?,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,pictureId: freezed == pictureId ? _self.pictureId : pictureId // ignore: cast_nullable_to_non_nullable
as String?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as String,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : levelingProgress // ignore: cast_nullable_to_non_nullable
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,backgroundId: freezed == backgroundId ? _self.backgroundId : backgroundId // ignore: cast_nullable_to_non_nullable
as String?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,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
@ -302,7 +300,7 @@ $SnCloudFileCopyWith<$Res>? get background {
@JsonSerializable()
class _SnAccountProfile implements SnAccountProfile {
const _SnAccountProfile({required this.id, required this.firstName, required this.middleName, required this.lastName, this.bio = '', required this.pictureId, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.backgroundId, required this.background, required this.createdAt, required this.updatedAt, required this.deletedAt});
const _SnAccountProfile({required this.id, required this.firstName, required this.middleName, required this.lastName, this.bio = '', required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
@override final String id;
@ -310,12 +308,10 @@ class _SnAccountProfile implements SnAccountProfile {
@override final String? middleName;
@override final String? lastName;
@override@JsonKey() final String bio;
@override final String? pictureId;
@override final int experience;
@override final int level;
@override final double levelingProgress;
@override final SnCloudFile? picture;
@override final String? backgroundId;
@override final SnCloudFile? background;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@ -334,16 +330,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,firstName,middleName,lastName,bio,pictureId,experience,level,levelingProgress,picture,backgroundId,background,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,firstName,middleName,lastName,bio,experience,level,levelingProgress,picture,background,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, pictureId: $pictureId, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, backgroundId: $backgroundId, background: $background, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@ -354,7 +350,7 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
@override @useResult
$Res call({
String id, String? firstName, String? middleName, String? lastName, String bio, String? pictureId, int experience, int level, double levelingProgress, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String? firstName, String? middleName, String? lastName, String bio, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@ -371,20 +367,18 @@ class __$SnAccountProfileCopyWithImpl<$Res>
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = freezed,Object? middleName = freezed,Object? lastName = freezed,Object? bio = null,Object? pictureId = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = freezed,Object? middleName = freezed,Object? lastName = freezed,Object? bio = null,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccountProfile(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,firstName: freezed == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String?,middleName: freezed == middleName ? _self.middleName : middleName // ignore: cast_nullable_to_non_nullable
as String?,lastName: freezed == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable
as String?,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,pictureId: freezed == pictureId ? _self.pictureId : pictureId // ignore: cast_nullable_to_non_nullable
as String?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as String,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : levelingProgress // ignore: cast_nullable_to_non_nullable
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,backgroundId: freezed == backgroundId ? _self.backgroundId : backgroundId // ignore: cast_nullable_to_non_nullable
as String?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,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

View File

@ -47,7 +47,6 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
middleName: json['middle_name'] as String?,
lastName: json['last_name'] as String?,
bio: json['bio'] as String? ?? '',
pictureId: json['picture_id'] as String?,
experience: (json['experience'] as num).toInt(),
level: (json['level'] as num).toInt(),
levelingProgress: (json['leveling_progress'] as num).toDouble(),
@ -55,7 +54,6 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
backgroundId: json['background_id'] as String?,
background:
json['background'] == null
? null
@ -77,12 +75,10 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
'middle_name': instance.middleName,
'last_name': instance.lastName,
'bio': instance.bio,
'picture_id': instance.pictureId,
'experience': instance.experience,
'level': instance.level,
'leveling_progress': instance.levelingProgress,
'picture': instance.picture?.toJson(),
'background_id': instance.backgroundId,
'background': instance.background?.toJson(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),

View File

@ -1,5 +1,6 @@
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/chat/chat.dart';
import 'package:island/widgets/chat/call_button.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
@ -11,6 +12,14 @@ import 'package:island/pods/websocket.dart';
part 'call.g.dart';
part 'call.freezed.dart';
String formatDuration(Duration duration) {
String negativeSign = duration.isNegative ? '-' : '';
String twoDigits(int n) => n.toString().padLeft(2, "0");
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60).abs());
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60).abs());
return "$negativeSign${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds";
}
@freezed
sealed class CallState with _$CallState {
const factory CallState({
@ -18,6 +27,7 @@ sealed class CallState with _$CallState {
required bool isMicrophoneEnabled,
required bool isCameraEnabled,
required bool isScreenSharing,
@Default(Duration(seconds: 0)) Duration duration,
String? error,
}) = _CallState;
}
@ -54,6 +64,8 @@ class CallNotifier extends _$CallNotifier {
List.unmodifiable(_participants);
LocalParticipant? get localParticipant => _localParticipant;
Timer? _durationTimer;
@override
CallState build() {
// Subscribe to websocket updates
@ -219,8 +231,16 @@ class CallNotifier extends _$CallNotifier {
Future<void> joinRoom(String roomId) async {
_roomId = roomId;
if (_room != null) {
await _room!.disconnect();
await _room!.dispose();
_room = null;
_localParticipant = null;
_participants = [];
}
try {
final apiClient = ref.read(apiClientProvider);
final ongoingCall = await ref.read(ongoingCallProvider(roomId).future);
final response = await apiClient.get('/chat/realtime/$roomId/join');
if (response.statusCode == 200 && response.data != null) {
final data = response.data;
@ -229,6 +249,19 @@ class CallNotifier extends _$CallNotifier {
final participants = joinResponse.participants;
final String endpoint = joinResponse.endpoint;
final String token = joinResponse.token;
// Setup duration timer
_durationTimer?.cancel();
_durationTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
state = state.copyWith(
duration: Duration(
milliseconds:
(DateTime.now().millisecondsSinceEpoch -
(ongoingCall?.createdAt.millisecondsSinceEpoch ?? 0)),
),
);
});
// Connect to LiveKit
_room = Room();
@ -314,5 +347,6 @@ class CallNotifier extends _$CallNotifier {
_roomListener?.dispose();
_room?.removeListener(_onRoomChange);
_room?.dispose();
_durationTimer?.cancel();
}
}

View File

@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$CallState {
bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; String? get error;
bool get isConnected; bool get isMicrophoneEnabled; bool get isCameraEnabled; bool get isScreenSharing; Duration get duration; String? get error;
/// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -26,16 +26,16 @@ $CallStateCopyWith<CallState> get copyWith => _$CallStateCopyWithImpl<CallState>
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.error, error) || other.error == error));
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error));
}
@override
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,error);
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,duration,error);
@override
String toString() {
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, error: $error)';
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)';
}
@ -46,7 +46,7 @@ abstract mixin class $CallStateCopyWith<$Res> {
factory $CallStateCopyWith(CallState value, $Res Function(CallState) _then) = _$CallStateCopyWithImpl;
@useResult
$Res call({
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, String? error
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error
});
@ -63,13 +63,14 @@ class _$CallStateCopyWithImpl<$Res>
/// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? error = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? duration = null,Object? error = freezed,}) {
return _then(_self.copyWith(
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable
as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable
as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable
as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as bool,duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
as Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as String?,
));
}
@ -81,13 +82,14 @@ as String?,
class _CallState implements CallState {
const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, this.error});
const _CallState({required this.isConnected, required this.isMicrophoneEnabled, required this.isCameraEnabled, required this.isScreenSharing, this.duration = const Duration(seconds: 0), this.error});
@override final bool isConnected;
@override final bool isMicrophoneEnabled;
@override final bool isCameraEnabled;
@override final bool isScreenSharing;
@override@JsonKey() final Duration duration;
@override final String? error;
/// Create a copy of CallState
@ -100,16 +102,16 @@ _$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallSt
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.error, error) || other.error == error));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallState&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.isMicrophoneEnabled, isMicrophoneEnabled) || other.isMicrophoneEnabled == isMicrophoneEnabled)&&(identical(other.isCameraEnabled, isCameraEnabled) || other.isCameraEnabled == isCameraEnabled)&&(identical(other.isScreenSharing, isScreenSharing) || other.isScreenSharing == isScreenSharing)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.error, error) || other.error == error));
}
@override
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,error);
int get hashCode => Object.hash(runtimeType,isConnected,isMicrophoneEnabled,isCameraEnabled,isScreenSharing,duration,error);
@override
String toString() {
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, error: $error)';
return 'CallState(isConnected: $isConnected, isMicrophoneEnabled: $isMicrophoneEnabled, isCameraEnabled: $isCameraEnabled, isScreenSharing: $isScreenSharing, duration: $duration, error: $error)';
}
@ -120,7 +122,7 @@ abstract mixin class _$CallStateCopyWith<$Res> implements $CallStateCopyWith<$Re
factory _$CallStateCopyWith(_CallState value, $Res Function(_CallState) _then) = __$CallStateCopyWithImpl;
@override @useResult
$Res call({
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, String? error
bool isConnected, bool isMicrophoneEnabled, bool isCameraEnabled, bool isScreenSharing, Duration duration, String? error
});
@ -137,13 +139,14 @@ class __$CallStateCopyWithImpl<$Res>
/// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? error = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? isConnected = null,Object? isMicrophoneEnabled = null,Object? isCameraEnabled = null,Object? isScreenSharing = null,Object? duration = null,Object? error = freezed,}) {
return _then(_CallState(
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
as bool,isMicrophoneEnabled: null == isMicrophoneEnabled ? _self.isMicrophoneEnabled : isMicrophoneEnabled // ignore: cast_nullable_to_non_nullable
as bool,isCameraEnabled: null == isCameraEnabled ? _self.isCameraEnabled : isCameraEnabled // ignore: cast_nullable_to_non_nullable
as bool,isScreenSharing: null == isScreenSharing ? _self.isScreenSharing : isScreenSharing // ignore: cast_nullable_to_non_nullable
as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as bool,duration: null == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
as Duration,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as String?,
));
}

View File

@ -6,7 +6,7 @@ part of 'call.dart';
// RiverpodGenerator
// **************************************************************************
String _$callNotifierHash() => r'5512070f943d98e999d97549c73e4d5f6e7b3ddd';
String _$callNotifierHash() => r'2082a572b5cfb4bf929dc1ed492c52cd2735452e';
/// See also [CallNotifier].
@ProviderFor(CallNotifier)

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/pods/theme.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'config.freezed.dart';
part 'config.g.dart';
const kTokenPairStoreKey = 'dyn_user_tk';
@ -14,13 +17,8 @@ const kAppbarTransparentStoreKey = 'app_bar_transparent';
const kAppBackgroundStoreKey = 'app_has_background';
const kAppColorSchemeStoreKey = 'app_color_scheme';
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const kAppExpandPostLink = 'app_expand_post_link';
const kAppExpandChatLink = 'app_expand_chat_link';
const kAppRealmCompactView = 'app_realm_compact_view';
const kAppCustomFonts = 'app_custom_fonts';
const kAppMixedFeed = 'app_mixed_feed';
const kAppAutoTranslate = 'app_auto_translate';
const kAppHideBottomNav = 'app_hide_bottom_nav';
const kAppSoundEffects = 'app_sound_effects';
const kAppAprilFoolFeatures = 'app_april_fool_features';
const kAppWindowSize = 'app_window_size';
@ -57,48 +55,73 @@ sealed class AppSettings with _$AppSettings {
required bool soundEffects,
required bool aprilFoolFeatures,
required bool enterToSend,
required bool appBarTransparent,
required String? customFonts,
required int? appColorScheme, // The color stored via the int type
}) = _AppSettings;
}
class AppSettingsNotifier extends StateNotifier<AppSettings> {
final SharedPreferences prefs;
AppSettingsNotifier(this.prefs)
: super(
AppSettings(
autoTranslate: prefs.getBool(kAppAutoTranslate) ?? false,
soundEffects: prefs.getBool(kAppSoundEffects) ?? true,
aprilFoolFeatures: prefs.getBool(kAppAprilFoolFeatures) ?? true,
enterToSend: prefs.getBool(kAppEnterToSend) ?? true,
),
);
@riverpod
class AppSettingsNotifier extends _$AppSettingsNotifier {
@override
AppSettings build() {
final prefs = ref.watch(sharedPreferencesProvider);
return AppSettings(
autoTranslate: prefs.getBool(kAppAutoTranslate) ?? false,
soundEffects: prefs.getBool(kAppSoundEffects) ?? true,
aprilFoolFeatures: prefs.getBool(kAppAprilFoolFeatures) ?? true,
enterToSend: prefs.getBool(kAppEnterToSend) ?? true,
appBarTransparent: prefs.getBool(kAppbarTransparentStoreKey) ?? false,
customFonts: prefs.getString(kAppCustomFonts),
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
);
}
void setAutoTranslate(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppAutoTranslate, value);
state = state.copyWith(autoTranslate: value);
}
void setSoundEffects(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppSoundEffects, value);
state = state.copyWith(soundEffects: value);
}
void setAprilFoolFeatures(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppAprilFoolFeatures, value);
state = state.copyWith(aprilFoolFeatures: value);
}
void setEnterToSend(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppEnterToSend, value);
state = state.copyWith(enterToSend: value);
}
}
final appSettingsProvider =
StateNotifierProvider<AppSettingsNotifier, AppSettings>((ref) {
final prefs = ref.watch(sharedPreferencesProvider);
return AppSettingsNotifier(prefs);
});
void setAppBarTransparent(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppbarTransparentStoreKey, value);
state = state.copyWith(appBarTransparent: value);
ref.read(themeProvider.notifier).reloadTheme();
}
void setCustomFonts(String? value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setString(kAppCustomFonts, value ?? '');
state = state.copyWith(customFonts: value);
ref.read(themeProvider.notifier).reloadTheme();
}
void setAppColorScheme(int? value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setInt(kAppColorSchemeStoreKey, value ?? 0);
state = state.copyWith(appColorScheme: value);
ref.read(themeProvider.notifier).reloadTheme();
}
}
final updateInfoProvider =
StateNotifierProvider<UpdateInfoNotifier, (String?, String?)>((ref) {

View File

@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AppSettings {
bool get autoTranslate; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend;
bool get autoTranslate; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; String? get customFonts; int? get appColorScheme;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -26,16 +26,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend));
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme));
}
@override
int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend);
int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,customFonts,appColorScheme);
@override
String toString() {
return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend)';
return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, customFonts: $customFonts, appColorScheme: $appColorScheme)';
}
@ -46,7 +46,7 @@ abstract mixin class $AppSettingsCopyWith<$Res> {
factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
@useResult
$Res call({
bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend
bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, String? customFonts, int? appColorScheme
});
@ -63,13 +63,16 @@ class _$AppSettingsCopyWithImpl<$Res>
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? customFonts = freezed,Object? appColorScheme = freezed,}) {
return _then(_self.copyWith(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
as bool,soundEffects: null == soundEffects ? _self.soundEffects : soundEffects // ignore: cast_nullable_to_non_nullable
as bool,aprilFoolFeatures: null == aprilFoolFeatures ? _self.aprilFoolFeatures : aprilFoolFeatures // ignore: cast_nullable_to_non_nullable
as bool,enterToSend: null == enterToSend ? _self.enterToSend : enterToSend // ignore: cast_nullable_to_non_nullable
as bool,
as bool,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent : appBarTransparent // ignore: cast_nullable_to_non_nullable
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
as int?,
));
}
@ -80,13 +83,16 @@ as bool,
class _AppSettings implements AppSettings {
const _AppSettings({required this.autoTranslate, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend});
const _AppSettings({required this.autoTranslate, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.customFonts, required this.appColorScheme});
@override final bool autoTranslate;
@override final bool soundEffects;
@override final bool aprilFoolFeatures;
@override final bool enterToSend;
@override final bool appBarTransparent;
@override final String? customFonts;
@override final int? appColorScheme;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@ -98,16 +104,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme));
}
@override
int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend);
int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,customFonts,appColorScheme);
@override
String toString() {
return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend)';
return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, customFonts: $customFonts, appColorScheme: $appColorScheme)';
}
@ -118,7 +124,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
@override @useResult
$Res call({
bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend
bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, String? customFonts, int? appColorScheme
});
@ -135,13 +141,16 @@ class __$AppSettingsCopyWithImpl<$Res>
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? customFonts = freezed,Object? appColorScheme = freezed,}) {
return _then(_AppSettings(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
as bool,soundEffects: null == soundEffects ? _self.soundEffects : soundEffects // ignore: cast_nullable_to_non_nullable
as bool,aprilFoolFeatures: null == aprilFoolFeatures ? _self.aprilFoolFeatures : aprilFoolFeatures // ignore: cast_nullable_to_non_nullable
as bool,enterToSend: null == enterToSend ? _self.enterToSend : enterToSend // ignore: cast_nullable_to_non_nullable
as bool,
as bool,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent : appBarTransparent // ignore: cast_nullable_to_non_nullable
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
as int?,
));
}

28
lib/pods/config.g.dart Normal file
View File

@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'config.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$appSettingsNotifierHash() =>
r'4f727d448ee17a87b5698b8e36ef67521655406c';
/// See also [AppSettingsNotifier].
@ProviderFor(AppSettingsNotifier)
final appSettingsNotifierProvider =
AutoDisposeNotifierProvider<AppSettingsNotifier, AppSettings>.internal(
AppSettingsNotifier.new,
name: r'appSettingsNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$appSettingsNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppSettingsNotifier = AutoDisposeNotifier<AppSettings>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart';
@ -68,16 +67,9 @@ final apiClientProvider = Provider<Dio>((ref) {
RequestInterceptorHandler handler,
) async {
try {
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
ref.watch(serverUrlProvider),
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk != null) {
options.headers['Authorization'] = 'Bearer $atk';
final token = await getToken(ref.watch(tokenProvider));
if (token != null) {
options.headers['Authorization'] = 'AtField $token';
}
} catch (err) {
// ignore
@ -95,105 +87,21 @@ final apiClientProvider = Provider<Dio>((ref) {
return dio;
});
final tokenPairProvider = Provider<AppTokenPair?>((ref) {
final tokenProvider = Provider<AppToken?>((ref) {
final prefs = ref.watch(sharedPreferencesProvider);
final tkPairString = prefs.getString(kTokenPairStoreKey);
if (tkPairString == null) return null;
return AppTokenPair.fromJson(jsonDecode(tkPairString));
final tokenString = prefs.getString(kTokenPairStoreKey);
if (tokenString == null) return null;
return AppToken.fromJson(jsonDecode(tokenString));
});
Future<(String, String)?> refreshToken(String baseUrl, String? rtk) async {
if (rtk == null) return null;
// Token refresh functionality removed as per backend changes
final dio = Dio();
dio.options.baseUrl = baseUrl;
final resp = await dio.post(
'/auth/token',
data: {'grant_type': 'refresh_token', 'refresh_token': rtk},
);
final String atk = resp.data['access_token'];
final String nRtk = resp.data['refresh_token'];
return (atk, nRtk);
Future<String?> getToken(AppToken? token) async {
return token?.token;
}
Completer<String?>? _refreshCompleter;
Future<String?> getFreshAtk(
AppTokenPair? tkPair,
String baseUrl, {
Function(String, String)? onRefreshed,
}) async {
var atk = tkPair?.accessToken;
var rtk = tkPair?.refreshToken;
if (_refreshCompleter != null) {
return await _refreshCompleter!.future;
} else {
_refreshCompleter = Completer<String?>();
}
try {
if (atk != null) {
final atkParts = atk.split('.');
if (atkParts.length != 3) {
throw Exception('invalid format of access token');
}
var rawPayload = atkParts[1].replaceAll('-', '+').replaceAll('_', '/');
switch (rawPayload.length % 4) {
case 0:
break;
case 2:
rawPayload += '==';
break;
case 3:
rawPayload += '=';
break;
default:
throw Exception('illegal format of access token payload');
}
final b64 = utf8.fuse(base64Url);
final payload = b64.decode(rawPayload);
final exp = jsonDecode(payload)['exp'];
if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
log('[Auth] Access token need refresh, doing it at ${DateTime.now()}');
final result = await refreshToken(baseUrl, rtk);
if (result == null) {
atk = null;
} else {
onRefreshed?.call(result.$1, result.$2);
atk = result.$1;
}
}
if (atk != null) {
_refreshCompleter!.complete(atk);
return atk;
} else {
log('[Auth] Access token refresh failed...');
_refreshCompleter!.complete(null);
}
}
} catch (err) {
log('[Auth] Failed to authenticate user... $err');
_refreshCompleter!.completeError(err);
} finally {
_refreshCompleter = null;
}
return null;
}
Future<void> setTokenPair(
SharedPreferences prefs,
String atk,
String rtk,
) async {
final tkPair = AppTokenPair(accessToken: atk, refreshToken: rtk);
final tkPairString = jsonEncode(tkPair);
prefs.setString(kTokenPairStoreKey, tkPairString);
Future<void> setToken(SharedPreferences prefs, String token) async {
final appToken = AppToken(token: token);
final tokenString = jsonEncode(appToken);
prefs.setString(kTokenPairStoreKey, tokenString);
}

View File

@ -13,7 +13,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
Future<String?> getAccessToken() async {
final prefs = _ref.read(sharedPreferencesProvider);
return prefs.getString('dyn_user_atk');
return prefs.getString(kTokenPairStoreKey);
}
Future<void> fetchUser() async {

View File

@ -51,27 +51,21 @@ class WebSocketService {
Future<void> connect(Ref ref) async {
_ref = ref;
_statusStreamController.sink.add(WebSocketState.connecting());
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
final token = await getToken(ref.watch(tokenProvider));
final url = '$baseUrl/ws'.replaceFirst('http', 'ws');
log('[WebSocket] Trying connecting to $url');
try {
if (kIsWeb) {
_channel = WebSocketChannel.connect(Uri.parse('$url?tk=$atk'));
_channel = WebSocketChannel.connect(Uri.parse('$url?tk=$token'));
} else {
_channel = IOWebSocketChannel.connect(
Uri.parse(url),
headers: {'Authorization': 'Bearer $atk'},
headers: {'Authorization': 'Bearer $token'},
);
}
await _channel!.ready;
@ -140,23 +134,10 @@ class WebSocketStateNotifier extends StateNotifier<WebSocketState> {
state = const WebSocketState.connecting();
try {
final service = ref.read(websocketProvider);
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) {
state = const WebSocketState.error('Unauthorized');
return;
}
await service.connect(ref);
state = const WebSocketState.connected();
service.statusStream.listen((event) {
state = event;
if (mounted) state = event;
});
} catch (err) {
state = WebSocketState.error('Failed to connect: $err');

View File

@ -1071,10 +1071,17 @@ class PostComposeRoute extends _i27.PageRouteInfo<PostComposeRouteArgs> {
PostComposeRoute({
_i28.Key? key,
_i30.SnPost? originalPost,
_i30.SnPost? repliedPost,
_i30.SnPost? forwardedPost,
List<_i27.PageRouteInfo>? children,
}) : super(
PostComposeRoute.name,
args: PostComposeRouteArgs(key: key, originalPost: originalPost),
args: PostComposeRouteArgs(
key: key,
originalPost: originalPost,
repliedPost: repliedPost,
forwardedPost: forwardedPost,
),
initialChildren: children,
);
@ -1089,32 +1096,50 @@ class PostComposeRoute extends _i27.PageRouteInfo<PostComposeRouteArgs> {
return _i18.PostComposeScreen(
key: args.key,
originalPost: args.originalPost,
repliedPost: args.repliedPost,
forwardedPost: args.forwardedPost,
);
},
);
}
class PostComposeRouteArgs {
const PostComposeRouteArgs({this.key, this.originalPost});
const PostComposeRouteArgs({
this.key,
this.originalPost,
this.repliedPost,
this.forwardedPost,
});
final _i28.Key? key;
final _i30.SnPost? originalPost;
final _i30.SnPost? repliedPost;
final _i30.SnPost? forwardedPost;
@override
String toString() {
return 'PostComposeRouteArgs{key: $key, originalPost: $originalPost}';
return 'PostComposeRouteArgs{key: $key, originalPost: $originalPost, repliedPost: $repliedPost, forwardedPost: $forwardedPost}';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! PostComposeRouteArgs) return false;
return key == other.key && originalPost == other.originalPost;
return key == other.key &&
originalPost == other.originalPost &&
repliedPost == other.repliedPost &&
forwardedPost == other.forwardedPost;
}
@override
int get hashCode => key.hashCode ^ originalPost.hashCode;
int get hashCode =>
key.hashCode ^
originalPost.hashCode ^
repliedPost.hashCode ^
forwardedPost.hashCode;
}
/// generated route for

View File

@ -74,7 +74,7 @@ class AccountScreen extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (user.value?.profile.backgroundId != null)
if (user.value?.profile.background?.id != null)
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
@ -83,7 +83,7 @@ class AccountScreen extends HookConsumerWidget {
child: AspectRatio(
aspectRatio: 16 / 7,
child: CloudImageWidget(
fileId: user.value!.profile.backgroundId!,
fileId: user.value!.profile.background!.id,
fit: BoxFit.cover,
),
),
@ -94,7 +94,7 @@ class AccountScreen extends HookConsumerWidget {
children: [
GestureDetector(
child: ProfilePictureWidget(
fileId: user.value?.profile.pictureId,
fileId: user.value?.profile.picture?.id,
radius: 24,
),
onTap: () {
@ -235,6 +235,16 @@ class AccountScreen extends HookConsumerWidget {
context.router.push(SettingsRoute());
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.manage_accounts),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('accountSettings').tr(),
onTap: () {
context.router.push(AccountSettingsRoute());
},
),
if (kDebugMode) const Divider(height: 1).padding(vertical: 8),
if (kDebugMode)
ListTile(
@ -244,8 +254,8 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('Copy access token'),
onTap: () async {
final tk = ref.watch(tokenPairProvider);
Clipboard.setData(ClipboardData(text: tk!.accessToken));
final tk = ref.watch(tokenProvider);
Clipboard.setData(ClipboardData(text: tk!.token));
},
),
if (kDebugMode)
@ -284,7 +294,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: const Text('Account')),
appBar: AppBar(title: const Text('account').tr()),
body:
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),

View File

@ -202,7 +202,7 @@ class EventCalanderScreen extends HookConsumerWidget {
color: Colors.transparent,
child: ListTile(
leading: ProfilePictureWidget(
fileId: user.value!.profile.pictureId,
fileId: user.value!.profile.picture?.id,
),
title: Text(user.value!.nick).bold(),
subtitle: Text('@${user.value!.name}'),

View File

@ -1,8 +1,19 @@
import 'dart:io';
import 'package:auto_route/annotations.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:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/auth/captcha.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@RoutePage()
class AccountSettingsScreen extends HookConsumerWidget {
@ -10,9 +21,274 @@ class AccountSettingsScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDesktop =
!kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
final isWide = isWideScreen(context);
Future<void> requestAccountDeletion() async {
final confirm = await showConfirmAlert(
'accountDeletionHint'.tr(),
'accountDeletion'.tr(),
);
if (!confirm || !context.mounted) return;
try {
final client = ref.read(apiClientProvider);
await client.delete('/accounts/me');
if (context.mounted) {
showSnackBar(context, 'accountDeletionSent'.tr());
}
} catch (err) {
showErrorAlert(err);
}
}
Future<void> requestResetPassword() async {
final confirm = await showConfirmAlert(
'accountPasswordChangeDescription'.tr(),
'accountPassword'.tr(),
);
if (!confirm || !context.mounted) return;
final captchaTk = await Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
if (captchaTk == null) return;
try {
final userInfo = ref.read(userInfoProvider);
final client = ref.read(apiClientProvider);
await client.post(
'/accounts/recovery/password',
data: {'account': userInfo.value!.name, 'captcha_token': captchaTk},
);
if (context.mounted) {
showSnackBar(context, 'accountPasswordChangeSent'.tr());
}
} catch (err) {
showErrorAlert(err);
}
}
// Group settings into categories for better organization
final securitySettings = [
ListTile(
minLeadingWidth: 48,
title: Text('accountPassword').tr(),
subtitle: Text('accountPasswordDescription').tr().fontSize(12),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.password),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
requestResetPassword();
},
),
ListTile(
minLeadingWidth: 48,
title: Text('accountTwoFactor').tr(),
subtitle: Text('accountTwoFactorDescription').tr().fontSize(12),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.security),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
// Navigate to two-factor authentication settings
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('accountTwoFactor').tr(),
content: Text('accountTwoFactorSetupDescription').tr(),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close').tr(),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
// Add navigation to 2FA setup screen
},
child: Text('accountTwoFactorSetup').tr(),
),
],
),
);
},
),
];
final privacySettings = [
// ListTile(
// minLeadingWidth: 48,
// title: Text('accountPrivacy').tr(),
// subtitle: Text('accountPrivacyDescription').tr().fontSize(12),
// contentPadding: const EdgeInsets.only(left: 24, right: 17),
// leading: const Icon(Symbols.visibility),
// trailing: const Icon(Symbols.chevron_right),
// onTap: () {
// // Navigate to privacy settings
// },
// ),
ListTile(
minLeadingWidth: 48,
title: Text('accountDataExport').tr(),
subtitle: Text('accountDataExportDescription').tr().fontSize(12),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.download),
trailing: const Icon(Symbols.chevron_right),
onTap: () async {
final confirm = await showConfirmAlert(
'accountDataExportConfirmation'.tr(),
'accountDataExport'.tr(),
);
if (!confirm || !context.mounted) return;
// Add data export logic
showSnackBar(context, 'accountDataExportRequested'.tr());
},
),
];
final dangerZoneSettings = [
ListTile(
minLeadingWidth: 48,
title: Text('accountDeletion').tr(),
subtitle: Text('accountDeletionDescription').tr().fontSize(12),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.delete_forever, color: Colors.red),
trailing: const Icon(Symbols.chevron_right),
onTap: requestAccountDeletion,
),
];
// Create a responsive layout based on screen width
Widget buildSettingsList() {
if (isWide) {
// Two-column layout for wide screens
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'accountSecurityTitle',
children: securitySettings,
),
_SettingsSection(
title: 'accountPrivacyTitle',
children: privacySettings,
),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'accountDangerZoneTitle',
children: dangerZoneSettings,
),
],
),
),
],
).padding(horizontal: 16);
} else {
// Single column layout for narrow screens
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'accountSecurityTitle',
children: securitySettings,
),
_SettingsSection(
title: 'accountPrivacyTitle',
children: privacySettings,
),
_SettingsSection(
title: 'accountDangerZoneTitle',
children: dangerZoneSettings,
),
],
);
}
}
return AppScaffold(
appBar: AppBar(title: Text('accountSettings').tr()),
body: SingleChildScrollView(child: Column(children: [])),
appBar: AppBar(
title: Text('accountSettings').tr(),
actions:
isDesktop
? [
IconButton(
icon: const Icon(Symbols.help_outline),
onPressed: () {
// Show help dialog
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('accountSettingsHelp').tr(),
content: Text('accountSettingsHelpContent').tr(),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close').tr(),
),
],
),
);
},
),
]
: null,
),
body: Focus(
autofocus: true,
onKeyEvent: (node, event) {
// Add keyboard shortcuts for desktop
if (isDesktop &&
event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
Navigator.of(context).pop();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16),
child: buildSettingsList(),
),
),
);
}
}
// Helper widget for displaying settings sections with titles
class _SettingsSection extends StatelessWidget {
final String title;
final List<Widget> children;
const _SettingsSection({required this.title, required this.children});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Text(
title.tr(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
...children,
const SizedBox(height: 16),
],
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
@ -59,19 +60,15 @@ class UpdateProfileScreen extends HookConsumerWidget {
submitting.value = true;
try {
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile =
await putMediaToCloud(
fileData: result,
atk: atk,
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
@ -166,9 +163,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child:
user.value!.profile.backgroundId != null
user.value!.profile.background?.id != null
? CloudImageWidget(
fileId: user.value!.profile.backgroundId!,
fileId: user.value!.profile.background!.id,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
@ -182,7 +179,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: user.value!.profile.pictureId,
fileId: user.value!.profile.picture?.id,
radius: 40,
),
onTap: () {

View File

@ -67,9 +67,9 @@ class AccountProfileScreen extends HookConsumerWidget {
leading: PageBackButton(shadows: [iconShadow]),
flexibleSpace: FlexibleSpaceBar(
background:
data.profile.backgroundId != null
data.profile.background?.id != null
? CloudImageWidget(
fileId: data.profile.backgroundId!,
fileId: data.profile.background!.id,
)
: Container(
color:
@ -91,7 +91,7 @@ class AccountProfileScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(
fileId: data.profile.pictureId,
fileId: data.profile.picture?.id,
radius: 32,
),
const Gap(20),

View File

@ -100,7 +100,7 @@ class RelationshipListTile extends StatelessWidget {
return ListTile(
contentPadding: const EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(fileId: account.profile.pictureId),
leading: ProfilePictureWidget(fileId: account.profile.picture?.id),
title: Row(
spacing: 6,
children: [

View File

@ -19,6 +19,8 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'captcha.dart';
final Map<int, (String, String, IconData)> kFactorTypes = {
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
@ -140,10 +142,9 @@ class _LoginCheckScreen extends HookConsumerWidget {
'/auth/token',
data: {'grant_type': 'authorization_code', 'code': result.id},
);
final atk = tokenResp.data['access_token'];
final rtk = tokenResp.data['refresh_token'];
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
final token = tokenResp.data['token'];
setToken(ref.watch(sharedPreferencesProvider), token);
ref.invalidate(tokenProvider);
if (!context.mounted) return;
final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser().then((_) {
@ -354,15 +355,18 @@ class _LoginLookupScreen extends HookConsumerWidget {
showErrorAlert('loginResetPasswordHint'.tr());
return;
}
final captchaTk = await Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => CaptchaScreen()));
if (captchaTk == null) return;
isBusy.value = true;
try {
final client = ref.watch(apiClientProvider);
final lookupResp = await client.get('/users/lookup?probe=$uname');
await client.post(
'/users/me/password-reset',
data: {'user_id': lookupResp.data['id']},
'/accounts/recovery/password',
data: {'account': uname, 'captcha_token': captchaTk},
);
showInfoAlert('done'.tr(), 'signinResetPasswordSent'.tr());
showInfoAlert('loginResetPasswordSent'.tr(), 'done'.tr());
} catch (err) {
showErrorAlert(err);
} finally {

View File

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -17,6 +19,8 @@ class TabNavigationObserver extends AutoRouterObserver {
@override
void didPush(Route route, Route? previousRoute) {
log('pushed ${previousRoute?.settings.name} -> ${route.settings.name}');
if (route is DialogRoute) return;
Future(() {
onChange(route.settings.name);
});
@ -24,6 +28,8 @@ class TabNavigationObserver extends AutoRouterObserver {
@override
void didPop(Route route, Route? previousRoute) {
log('popped ${route.settings.name} -> ${previousRoute?.settings.name}');
if (route is DialogRoute) return;
Future(() {
onChange(previousRoute?.settings.name);
});
@ -43,7 +49,6 @@ class TabsNavigationWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final useHorizontalLayout = isWideScreen(context);
final useExpandableLayout = isWidestScreen(context);
final currentRoute = ref.watch(currentRouteProvider);
final notificationUnreadCount = ref.watch(
@ -80,6 +85,7 @@ class TabsNavigationWidget extends HookConsumerWidget {
];
final routeNames = [
ExploreRoute.name,
ExploreShellRoute.name,
ChatListRoute.name,
RealmListRoute.name,
AccountRoute.name,
@ -110,8 +116,6 @@ class TabsNavigationWidget extends HookConsumerWidget {
Gap(MediaQuery.of(context).padding.top + 8),
Expanded(
child: NavigationRail(
minExtendedWidth: 200,
extended: useExpandableLayout,
selectedIndex: activeIndex,
onDestinationSelected: (index) {
router.replace(routes[index]);

View File

@ -5,11 +5,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/call.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/chat/chat.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/call_button.dart';
import 'package:island/widgets/chat/call_overlay.dart';
import 'package:island/widgets/chat/call_participant_tile.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:styled_widget/styled_widget.dart';
@ -22,8 +21,6 @@ class CallScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final ongoingCall = ref.watch(ongoingCallProvider(roomId));
final userInfo = ref.watch(userInfoProvider);
final chatRoom = ref.watch(chatroomProvider(roomId));
final callState = ref.watch(callNotifierProvider);
final callNotifier = ref.read(callNotifierProvider.notifier);
@ -32,10 +29,6 @@ class CallScreen extends HookConsumerWidget {
return null;
}, []);
final actionButtonStyle = ButtonStyle(
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
);
final viewMode = useState<String>('grid');
return AppScaffold(
@ -74,20 +67,12 @@ class CallScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
chatRoom.whenOrNull()?.name ?? 'loading'.tr(),
ongoingCall.value?.room.name ?? 'call'.tr(),
style: const TextStyle(fontSize: 16),
),
Text(
callState.isConnected
? Duration(
milliseconds:
(DateTime.now().millisecondsSinceEpoch -
(ongoingCall
.value
?.createdAt
.millisecondsSinceEpoch ??
0)),
).toString()
? formatDuration(callState.duration)
: 'Connecting',
style: const TextStyle(fontSize: 14),
),
@ -131,78 +116,6 @@ class CallScreen extends HookConsumerWidget {
)
: Column(
children: [
Card(
margin: const EdgeInsets.only(left: 12, right: 12, top: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Builder(
builder: (context) {
if (callNotifier.localParticipant == null) {
return CircularProgressIndicator().center();
}
return SizedBox(
width: 40,
height: 40,
child:
SpeakingRippleAvatar(
isSpeaking:
callNotifier
.localParticipant!
.isSpeaking,
audioLevel:
callNotifier
.localParticipant!
.audioLevel,
pictureId:
userInfo.value?.profile.pictureId,
size: 36,
).center(),
);
},
),
],
),
),
IconButton(
icon: Icon(
callState.isMicrophoneEnabled
? Icons.mic
: Icons.mic_off,
),
onPressed: () {
callNotifier.toggleMicrophone();
},
style: actionButtonStyle,
),
IconButton(
icon: Icon(
callState.isCameraEnabled
? Icons.videocam
: Icons.videocam_off,
),
onPressed: () {
callNotifier.toggleCamera();
},
style: actionButtonStyle,
),
IconButton(
icon: Icon(
callState.isScreenSharing
? Icons.stop_screen_share
: Icons.screen_share,
),
onPressed: () {
callNotifier.toggleScreenShare();
},
style: actionButtonStyle,
),
],
).padding(all: 16),
),
Expanded(
child: Builder(
builder: (context) {
@ -253,7 +166,8 @@ class CallScreen extends HookConsumerWidget {
.profile
?.account
.profile
.pictureId,
.picture
?.id,
size: 72,
),
),
@ -374,6 +288,8 @@ class CallScreen extends HookConsumerWidget {
},
),
),
CallControlsBar(),
Gap(MediaQuery.of(context).padding.bottom + 16),
],
),
);

View File

@ -11,6 +11,7 @@ import 'package:image_picker/image_picker.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/file.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/call.dart';
import 'package:island/pods/chat_summary.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
@ -21,6 +22,7 @@ import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/call_overlay.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/realms/selection_dropdown.dart';
import 'package:island/widgets/response.dart';
@ -107,7 +109,7 @@ class ChatRoomListTile extends HookConsumerWidget {
},
loading: () => const SizedBox.shrink(),
error:
(_, __) =>
(_, _) =>
isDirect && room.description == null
? Text(
room.members!.map((e) => '@${e.account.name}').join(', '),
@ -125,19 +127,19 @@ class ChatRoomListTile extends HookConsumerWidget {
isLabelVisible: summary.when(
data: (data) => (data?.unreadCount ?? 0) > 0,
loading: () => false,
error: (_, __) => false,
error: (_, _) => false,
),
child:
(isDirect && room.pictureId == null)
(isDirect && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map((e) => e.account.profile.pictureId)
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.pictureId == null
: room.picture?.id == null
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
: ProfilePictureWidget(fileId: room.pictureId),
: ProfilePictureWidget(fileId: room.picture?.id),
),
title: Text(
(isDirect && room.name == null)
@ -145,14 +147,14 @@ class ChatRoomListTile extends HookConsumerWidget {
: room.name ?? '',
),
subtitle: buildSubtitle(),
trailing: trailing, // Add this line
onTap: () async {
// Clear unread count if there are unread messages
final summary = await ref.read(chatSummaryProvider.future);
if ((summary[room.id]?.unreadCount ?? 0) > 0) {
await ref
.read(chatSummaryProvider.notifier)
.clearUnreadCount(room.id);
}
ref.read(chatSummaryProvider.future).then((summary) {
if ((summary[room.id]?.unreadCount ?? 0) > 0) {
ref.read(chatSummaryProvider.notifier).clearUnreadCount(room.id);
}
});
onTap?.call();
},
);
@ -213,6 +215,8 @@ class ChatListScreen extends HookConsumerWidget {
0,
); // 0 for All, 1 for Direct Messages, 2 for Group Chats
final callState = ref.watch(callNotifierProvider);
useEffect(() {
tabController.addListener(() {
selectedTab.value = tabController.index;
@ -276,13 +280,13 @@ class ChatListScreen extends HookConsumerWidget {
label: Text(
chatInvites.when(
data: (invites) => invites.length.toString(),
error: (_, __) => '0',
error: (_, _) => '0',
loading: () => '0',
),
),
isLabelVisible: chatInvites.when(
data: (invites) => invites.isNotEmpty,
error: (_, __) => false,
error: (_, _) => false,
loading: () => false,
),
child: const Icon(Symbols.email),
@ -334,76 +338,93 @@ class ChatListScreen extends HookConsumerWidget {
},
child: const Icon(Symbols.add),
),
body: Column(
body: Stack(
children: [
Consumer(
builder: (context, ref, _) {
final summaryState = ref.watch(chatSummaryProvider);
return summaryState.maybeWhen(
loading: () => const LinearProgressIndicator(),
orElse: () => const SizedBox.shrink(),
);
},
),
Expanded(
child: chats.when(
data:
(items) => RefreshIndicator(
onRefresh:
() => Future.sync(() {
ref.invalidate(chatroomsJoinedProvider);
}),
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount:
items
.where(
(item) =>
selectedTab.value == 0 ||
(selectedTab.value == 1 &&
item.type == 1) ||
(selectedTab.value == 2 && item.type != 1),
)
.length,
itemBuilder: (context, index) {
final filteredItems =
items
.where(
(item) =>
selectedTab.value == 0 ||
(selectedTab.value == 1 &&
item.type == 1) ||
(selectedTab.value == 2 &&
item.type != 1),
)
.toList();
final item = filteredItems[index];
return ChatRoomListTile(
room: item,
isDirect: item.type == 1,
onTap: () {
if (context.router.topRoute.name ==
ChatRoomRoute.name) {
context.router.replace(
ChatRoomRoute(id: item.id),
);
} else {
context.router.push(ChatRoomRoute(id: item.id));
}
Column(
children: [
Consumer(
builder: (context, ref, _) {
final summaryState = ref.watch(chatSummaryProvider);
return summaryState.maybeWhen(
loading: () => const LinearProgressIndicator(),
orElse: () => const SizedBox.shrink(),
);
},
),
Expanded(
child: chats.when(
data:
(items) => RefreshIndicator(
onRefresh:
() => Future.sync(() {
ref.invalidate(chatroomsJoinedProvider);
}),
child: ListView.builder(
padding:
callState.isConnected
? EdgeInsets.only(bottom: 96)
: EdgeInsets.zero,
itemCount:
items
.where(
(item) =>
selectedTab.value == 0 ||
(selectedTab.value == 1 &&
item.type == 1) ||
(selectedTab.value == 2 &&
item.type != 1),
)
.length,
itemBuilder: (context, index) {
final filteredItems =
items
.where(
(item) =>
selectedTab.value == 0 ||
(selectedTab.value == 1 &&
item.type == 1) ||
(selectedTab.value == 2 &&
item.type != 1),
)
.toList();
final item = filteredItems[index];
return ChatRoomListTile(
room: item,
isDirect: item.type == 1,
onTap: () {
if (context.router.topRoute.name ==
ChatRoomRoute.name) {
context.router.replace(
ChatRoomRoute(id: item.id),
);
} else {
context.router.push(
ChatRoomRoute(id: item.id),
);
}
},
);
},
);
},
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () {
ref.invalidate(chatroomsJoinedProvider);
},
),
),
),
),
loading:
() => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () {
ref.invalidate(chatroomsJoinedProvider);
},
),
),
),
],
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: const CallOverlayBar().padding(horizontal: 16, vertical: 12),
),
],
),
@ -502,19 +523,15 @@ class EditChatScreen extends HookConsumerWidget {
submitting.value = true;
try {
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile =
await putMediaToCloud(
fileData: result,
atk: atk,
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
@ -575,7 +592,7 @@ class EditChatScreen extends HookConsumerWidget {
realms: joinedRealms.when(
data: (realms) => realms,
loading: () => [],
error: (_, __) => [],
error: (_, _) => [],
),
onChanged: (SnRealm? value) {
currentRealm.value = value;

View File

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -18,20 +17,21 @@ import 'package:island/pods/database.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/call_overlay.dart';
import 'package:island/widgets/chat/message_item.dart';
import 'package:island/widgets/content/attachment_preview.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:uuid/uuid.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:super_clipboard/super_clipboard.dart';
import 'chat.dart';
import 'package:island/widgets/chat/call_button.dart';
@ -116,19 +116,12 @@ class MessagesNotifier extends _$MessagesNotifier {
messageRepositoryProvider(_roomId).future,
);
final baseUrl = ref.read(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Access token is null');
final currentMessages = state.value ?? [];
await repository.sendMessage(
atk,
token,
baseUrl,
_roomId,
content,
@ -352,6 +345,10 @@ class ChatRoomScreen extends HookConsumerWidget {
if (message.chatRoomId != chatRoom.value?.id) return;
switch (pkt.type) {
case 'messages.new':
if (message.type.startsWith('call')) {
// Handle the ongoing call.
ref.invalidate(ongoingCallProvider(message.chatRoomId));
}
messagesNotifier.receiveMessage(message);
// Send read receipt for new message
sendReadReceipt();
@ -436,19 +433,23 @@ class ChatRoomScreen extends HookConsumerWidget {
height: 26,
width: 26,
child:
(room!.type == 1 && room.pictureId == null)
(room!.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map(
(e) =>
e.account.profile.pictureId,
e
.account
.profile
.picture
?.id,
)
.toList(),
)
: room.pictureId != null
: room.picture?.id != null
? ProfilePictureWidget(
fileId: room.pictureId,
fileId: room.picture?.id,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
@ -476,19 +477,23 @@ class ChatRoomScreen extends HookConsumerWidget {
height: 26,
width: 26,
child:
(room!.type == 1 && room.pictureId == null)
(room!.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map(
(e) =>
e.account.profile.pictureId,
e
.account
.profile
.picture
?.id,
)
.toList(),
)
: room.pictureId != null
: room.picture?.id != null
? ProfilePictureWidget(
fileId: room.pictureId,
fileId: room.picture?.id,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
@ -509,7 +514,7 @@ class ChatRoomScreen extends HookConsumerWidget {
),
loading: () => const Text('Loading...'),
error:
(err, __) => ResponseErrorWidget(
(err, _) => ResponseErrorWidget(
error: err,
onRetry: () => messagesNotifier.loadInitial(),
),
@ -525,152 +530,166 @@ class ChatRoomScreen extends HookConsumerWidget {
const Gap(8),
],
),
body: Column(
body: Stack(
children: [
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: SuperListView.builder(
padding: EdgeInsets.symmetric(vertical: 16),
controller: scrollController,
reverse: true, // Show newest messages at the bottom
itemCount: messageList.length,
itemBuilder: (context, index) {
final message = messageList[index];
final nextMessage =
index < messageList.length - 1
? messageList[index + 1]
: null;
final isLastInGroup =
nextMessage == null ||
nextMessage.senderId != message.senderId ||
nextMessage.createdAt
.difference(message.createdAt)
.inMinutes
.abs() >
3;
Column(
children: [
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: SuperListView.builder(
padding: EdgeInsets.symmetric(vertical: 16),
controller: scrollController,
reverse:
true, // Show newest messages at the bottom
itemCount: messageList.length,
itemBuilder: (context, index) {
final message = messageList[index];
final nextMessage =
index < messageList.length - 1
? messageList[index + 1]
: null;
final isLastInGroup =
nextMessage == null ||
nextMessage.senderId !=
message.senderId ||
nextMessage.createdAt
.difference(message.createdAt)
.inMinutes
.abs() >
3;
return chatIdentity.when(
skipError: true,
data:
(identity) => MessageItem(
message: message,
isCurrentUser:
identity?.id == message.senderId,
onAction: (action) {
switch (action) {
case MessageItemAction.delete:
messagesNotifier.deleteMessage(
message.id,
);
case MessageItemAction.edit:
messageEditingTo.value =
message.toRemoteMessage();
messageController.text =
messageEditingTo
.value
?.content ??
'';
attachments.value =
messageEditingTo
.value!
.attachments
.map(
(e) =>
UniversalFile.fromAttachment(
e,
),
)
.toList();
case MessageItemAction.forward:
messageForwardingTo.value =
message.toRemoteMessage();
case MessageItemAction.reply:
messageReplyingTo.value =
message.toRemoteMessage();
}
},
progress:
attachmentProgress.value[message.id],
showAvatar: isLastInGroup,
),
loading:
() => MessageItem(
message: message,
isCurrentUser: false,
onAction: null,
progress: null,
showAvatar: false,
),
error: (_, __) => const SizedBox.shrink(),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
),
chatRoom.when(
data:
(room) => _ChatInput(
messageController: messageController,
chatRoom: room!,
onSend: sendMessage,
onClear: () {
if (messageEditingTo.value != null) {
attachments.value.clear();
messageController.clear();
}
messageEditingTo.value = null;
messageReplyingTo.value = null;
messageForwardingTo.value = null;
},
messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value,
onPickFile: (bool isPhoto) {
if (isPhoto) {
pickPhotoMedia();
} else {
pickVideoMedia();
}
},
attachments: attachments.value,
onUploadAttachment: (_) {
// not going to do anything, only upload when send the message
},
onDeleteAttachment: (index) async {
final attachment = attachments.value[index];
if (attachment.isOnCloud) {
final client = ref.watch(apiClientProvider);
await client.delete('/files/${attachment.data.id}');
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
},
onMoveAttachment: (idx, delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(idx + delta, clone.removeAt(idx));
attachments.value = clone;
},
onAttachmentsChanged: (newAttachments) {
attachments.value = newAttachments;
},
return chatIdentity.when(
skipError: true,
data:
(identity) => MessageItem(
message: message,
isCurrentUser:
identity?.id == message.senderId,
onAction: (action) {
switch (action) {
case MessageItemAction.delete:
messagesNotifier.deleteMessage(
message.id,
);
case MessageItemAction.edit:
messageEditingTo.value =
message.toRemoteMessage();
messageController.text =
messageEditingTo
.value
?.content ??
'';
attachments.value =
messageEditingTo
.value!
.attachments
.map(
(e) =>
UniversalFile.fromAttachment(
e,
),
)
.toList();
case MessageItemAction.forward:
messageForwardingTo.value =
message.toRemoteMessage();
case MessageItemAction.reply:
messageReplyingTo.value =
message.toRemoteMessage();
}
},
progress:
attachmentProgress.value[message
.id],
showAvatar: isLastInGroup,
),
loading:
() => MessageItem(
message: message,
isCurrentUser: false,
onAction: null,
progress: null,
showAvatar: false,
),
error: (_, _) => const SizedBox.shrink(),
);
},
),
loading:
() => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
error: (_, __) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
chatRoom.when(
data:
(room) => _ChatInput(
messageController: messageController,
chatRoom: room!,
onSend: sendMessage,
onClear: () {
if (messageEditingTo.value != null) {
attachments.value.clear();
messageController.clear();
}
messageEditingTo.value = null;
messageReplyingTo.value = null;
messageForwardingTo.value = null;
},
messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value,
onPickFile: (bool isPhoto) {
if (isPhoto) {
pickPhotoMedia();
} else {
pickVideoMedia();
}
},
attachments: attachments.value,
onUploadAttachment: (_) {
// not going to do anything, only upload when send the message
},
onDeleteAttachment: (index) async {
final attachment = attachments.value[index];
if (attachment.isOnCloud) {
final client = ref.watch(apiClientProvider);
await client.delete('/files/${attachment.data.id}');
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
},
onMoveAttachment: (idx, delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(idx + delta, clone.removeAt(idx));
attachments.value = clone;
},
onAttachmentsChanged: (newAttachments) {
attachments.value = newAttachments;
},
),
error: (_, _) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
],
),
Positioned(
left: 0,
right: 0,
top: 0,
child: CallOverlayBar().padding(horizontal: 8, top: 12),
),
],
),
@ -720,7 +739,7 @@ class _ChatInput extends ConsumerWidget {
return;
}
final enterToSend = ref.read(appSettingsProvider).enterToSend;
final enterToSend = ref.read(appSettingsNotifierProvider).enterToSend;
final isEnter = event.logicalKey == LogicalKeyboardKey.enter;
if (isEnter) {
@ -733,38 +752,21 @@ class _ChatInput extends ConsumerWidget {
}
Future<void> _handlePaste() async {
final clipboard = SystemClipboard.instance;
final clipboard = await Pasteboard.image;
if (clipboard == null) return;
final reader = await clipboard.read();
if (reader.canProvide(Formats.png)) {
reader.getFile(Formats.png, (file) async {
final stream = file.getStream();
final bytes = await stream.toList();
final imageBytes = bytes.expand((e) => e).toList();
// Create a temporary file to store the image
final tempDir = Directory.systemTemp;
final tempFile = File(
'${tempDir.path}/pasted_image_${DateTime.now().millisecondsSinceEpoch}.png',
);
await tempFile.writeAsBytes(imageBytes);
// Add the file to attachments
onAttachmentsChanged([
...attachments,
UniversalFile(
data: XFile(tempFile.path),
type: UniversalFileType.image,
),
]);
});
}
onAttachmentsChanged([
...attachments,
UniversalFile(
data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
type: UniversalFileType.image,
),
]);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final enterToSend = ref.watch(appSettingsProvider).enterToSend;
final enterToSend = ref.watch(appSettingsNotifierProvider).enterToSend;
return Material(
elevation: 8,
@ -786,7 +788,7 @@ class _ChatInput extends ConsumerWidget {
onMove: (delta) => onMoveAttachment(idx, delta),
);
},
separatorBuilder: (_, __) => const Gap(8),
separatorBuilder: (_, _) => const Gap(8),
),
).padding(top: 12),
if (messageReplyingTo != null ||
@ -870,6 +872,7 @@ class _ChatInput extends ConsumerWidget {
onKey: (event) => _handleKeyPress(context, ref, event),
child: TextField(
controller: messageController,
onSubmitted: enterToSend ? (_) => onSend() : null,
inputFormatters: [
if (enterToSend)
TextInputFormatter.withFunction((oldValue, newValue) {

View File

@ -6,7 +6,7 @@ part of 'room.dart';
// RiverpodGenerator
// **************************************************************************
String _$messagesNotifierHash() => r'71a9fc1c6d024f6203f06225384c19335b9b6f2c';
String _$messagesNotifierHash() => r'afc4d43f4948ec571118cef0321838a6cefc89c0';
/// Copied from Dart SDK
class _SystemHash {

View File

@ -48,9 +48,9 @@ class ChatDetailScreen extends HookConsumerWidget {
flexibleSpace: FlexibleSpaceBar(
background:
(currentRoom!.type == 1 &&
currentRoom.backgroundId != null)
currentRoom.background?.id != null)
? CloudImageWidget(
fileId: currentRoom.backgroundId!,
fileId: currentRoom.background!.id,
)
: (currentRoom.type == 1 &&
currentRoom.members!.length == 1 &&
@ -59,7 +59,8 @@ class ChatDetailScreen extends HookConsumerWidget {
.first
.account
.profile
.backgroundId !=
.background
?.id !=
null)
? CloudImageWidget(
fileId:
@ -68,11 +69,12 @@ class ChatDetailScreen extends HookConsumerWidget {
.first
.account
.profile
.backgroundId!,
.background!
.id,
)
: currentRoom.backgroundId != null
: currentRoom.background?.id != null
? CloudImageWidget(
fileId: currentRoom.backgroundId!,
fileId: currentRoom.background!.id,
fit: BoxFit.cover,
)
: Container(
@ -390,7 +392,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(
fileId: member.account.profile.pictureId,
fileId: member.account.profile.picture?.id,
),
title: Row(
spacing: 6,

View File

@ -101,7 +101,7 @@ class CreatorHubScreen extends HookConsumerWidget {
minTileHeight: 48,
leading: ProfilePictureWidget(
radius: 16,
fileId: item.pictureId,
fileId: item.picture?.id,
),
title: Text(item.nick),
subtitle: Text('@${item.name}'),
@ -115,7 +115,7 @@ class CreatorHubScreen extends HookConsumerWidget {
)
.toList(),
loading: () => [],
error: (_, __) => [],
error: (_, _) => [],
);
final publisherStats = ref.watch(
@ -150,7 +150,7 @@ class CreatorHubScreen extends HookConsumerWidget {
...publishersMenu.map(
(e) => ProfilePictureWidget(
radius: 16,
fileId: e.value?.pictureId,
fileId: e.value?.picture?.id,
).center().padding(right: 8),
),
];
@ -204,7 +204,7 @@ class CreatorHubScreen extends HookConsumerWidget {
...(publishers.value?.map(
(publisher) => ListTile(
leading: ProfilePictureWidget(
fileId: publisher.pictureId,
fileId: publisher.picture?.id,
),
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
@ -293,7 +293,7 @@ class CreatorHubScreen extends HookConsumerWidget {
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const SizedBox.shrink(),
error: (_, _) => const SizedBox.shrink(),
),
);
}

View File

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/config.dart';
@ -95,19 +96,15 @@ class EditPublisherScreen extends HookConsumerWidget {
submitting.value = true;
try {
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile =
await putMediaToCloud(
fileData: result,
atk: atk,
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
@ -145,8 +142,8 @@ class EditPublisherScreen extends HookConsumerWidget {
useEffect(() {
if (publisher.value != null) {
picture.value = publisher.value!.pictureId;
background.value = publisher.value!.backgroundId;
picture.value = publisher.value!.picture?.id;
background.value = publisher.value!.background?.id;
nameController.text = publisher.value!.name;
nickController.text = publisher.value!.nick;
bioController.text = publisher.value!.bio;
@ -200,7 +197,7 @@ class EditPublisherScreen extends HookConsumerWidget {
realms: joinedRealms.when(
data: (realms) => realms,
loading: () => [],
error: (_, __) => [],
error: (_, _) => [],
),
onChanged: (SnRealm? value) {
currentRealm.value = value;
@ -286,14 +283,14 @@ class EditPublisherScreen extends HookConsumerWidget {
nameController.text = user.value!.name;
nickController.text = user.value!.nick;
bioController.text = user.value!.profile.bio;
picture.value = user.value!.profile.pictureId;
background.value = user.value!.profile.backgroundId;
picture.value = user.value!.profile.picture?.id;
background.value = user.value!.profile.background?.id;
} else {
nameController.text = currentRealm.value!.slug;
nickController.text = currentRealm.value!.name;
bioController.text = currentRealm.value!.description;
picture.value = currentRealm.value!.pictureId;
background.value = currentRealm.value!.backgroundId;
picture.value = currentRealm.value!.picture?.id;
background.value = currentRealm.value!.background?.id;
}
},
label:

View File

@ -298,7 +298,7 @@ class _StickerPackActionMenu extends HookConsumerWidget {
if (confirm) {
final client = ref.watch(apiClientProvider);
client.delete('/stickers/$packId');
ref.invalidate(stickerPacksProvider);
ref.invalidate(stickerPacksNotifierProvider);
if (context.mounted) context.router.maybePop(true);
}
});

View File

@ -13,7 +13,7 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
part 'stickers.g.dart';
@ -24,9 +24,6 @@ class StickersScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final stickersState = ref.watch(stickerPacksProvider);
final stickersNotifier = ref.watch(stickerPacksProvider.notifier);
return AppScaffold(
appBar: AppBar(
title: const Text('stickers').tr(),
@ -37,7 +34,7 @@ class StickersScreen extends HookConsumerWidget {
value,
) {
if (value != null) {
stickersNotifier.refresh();
ref.invalidate(stickerPacksNotifierProvider(pubName));
}
});
},
@ -46,104 +43,90 @@ class StickersScreen extends HookConsumerWidget {
const Gap(8),
],
),
body: stickersState.when(
data:
(stickers) => RefreshIndicator(
onRefresh: stickersNotifier.refresh,
child: InfiniteList(
padding: EdgeInsets.zero,
itemCount: stickers.length,
hasReachedMax: stickersNotifier.isReachedMax,
isLoading: stickersNotifier.isLoading,
onFetchData: stickersNotifier.fetchMore,
itemBuilder: (context, index) {
return ListTile(
title: Text(stickers[index].name),
subtitle: Text(stickers[index].description),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context.router.push(
StickerPackDetailRoute(
pubName: pubName,
id: stickers[index].id,
),
);
},
);
},
),
),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
),
body: SliverStickerPacksList(pubName: pubName),
);
}
}
final stickerPacksProvider = StateNotifierProvider<
StickerPacksNotifier,
AsyncValue<List<SnStickerPack>>
>((ref) {
return StickerPacksNotifier(ref.watch(apiClientProvider));
});
class SliverStickerPacksList extends HookConsumerWidget {
final String pubName;
const SliverStickerPacksList({super.key, required this.pubName});
class StickerPacksNotifier
extends StateNotifier<AsyncValue<List<SnStickerPack>>> {
final Dio _apiClient;
StickerPacksNotifier(this._apiClient) : super(const AsyncValue.loading()) {
fetchStickers();
@override
Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperView(
provider: stickerPacksNotifierProvider(pubName),
futureRefreshable: stickerPacksNotifierProvider(pubName).future,
notifierRefreshable: stickerPacksNotifierProvider(pubName).notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final sticker = data.items[index];
return ListTile(
title: Text(sticker.name),
subtitle: Text(sticker.description),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context.router.push(
StickerPackDetailRoute(pubName: pubName, id: sticker.id),
);
},
);
},
),
);
}
}
@riverpod
class StickerPacksNotifier extends _$StickerPacksNotifier
with CursorPagingNotifierMixin<SnStickerPack> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnStickerPack>> build(String pubName) {
return fetch(cursor: null);
}
int offset = 0;
int take = 20;
int total = 0;
bool isLoading = false;
bool get isReachedMax =>
state.valueOrNull != null && state.valueOrNull!.length >= total;
Future<void> fetchStickers() async {
if (isLoading) return;
isLoading = true;
@override
Future<CursorPagingData<SnStickerPack>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
try {
final response = await _apiClient.get(
'/stickers?offset=$offset&take=$take',
final response = await client.get(
'/stickers',
queryParameters: {
'offset': offset,
'take': _pageSize,
'pubName': pubName,
},
);
if (response.statusCode == 200) {
total = int.parse(response.headers.value('X-Total') ?? '0');
final newStickers =
response.data
.map((e) => SnStickerPack.fromJson(e))
.cast<SnStickerPack>()
.toList();
state = AsyncValue.data(
state.valueOrNull != null
? [...state.value!, ...newStickers]
: newStickers,
);
offset += take;
} else {
state = AsyncValue.error('Failed to load stickers', StackTrace.current);
}
} catch (err, stackTrace) {
state = AsyncValue.error(err, stackTrace);
} finally {
isLoading = false;
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final stickers = data.map((e) => SnStickerPack.fromJson(e)).toList();
final hasMore = offset + stickers.length < total;
final nextCursor = hasMore ? (offset + stickers.length).toString() : null;
return CursorPagingData(
items: stickers,
hasMore: hasMore,
nextCursor: nextCursor,
);
} catch (err) {
rethrow;
}
}
Future<void> fetchMore() async {
if (state.valueOrNull == null || state.valueOrNull!.length >= total) return;
await fetchStickers();
}
Future<void> refresh() async {
offset = 0;
state = const AsyncValue.loading();
await fetchStickers();
}
}
@riverpod

View File

@ -147,5 +147,154 @@ class _StickerPackProviderElement
String? get packId => (origin as StickerPackProvider).packId;
}
String _$stickerPacksNotifierHash() =>
r'2feff50a7896eb8759fe91e9626b0409354d9fee';
abstract class _$StickerPacksNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnStickerPack>> {
late final String pubName;
FutureOr<CursorPagingData<SnStickerPack>> build(String pubName);
}
/// See also [StickerPacksNotifier].
@ProviderFor(StickerPacksNotifier)
const stickerPacksNotifierProvider = StickerPacksNotifierFamily();
/// See also [StickerPacksNotifier].
class StickerPacksNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnStickerPack>>> {
/// See also [StickerPacksNotifier].
const StickerPacksNotifierFamily();
/// See also [StickerPacksNotifier].
StickerPacksNotifierProvider call(String pubName) {
return StickerPacksNotifierProvider(pubName);
}
@override
StickerPacksNotifierProvider getProviderOverride(
covariant StickerPacksNotifierProvider provider,
) {
return call(provider.pubName);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'stickerPacksNotifierProvider';
}
/// See also [StickerPacksNotifier].
class StickerPacksNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
StickerPacksNotifier,
CursorPagingData<SnStickerPack>
> {
/// See also [StickerPacksNotifier].
StickerPacksNotifierProvider(String pubName)
: this._internal(
() => StickerPacksNotifier()..pubName = pubName,
from: stickerPacksNotifierProvider,
name: r'stickerPacksNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$stickerPacksNotifierHash,
dependencies: StickerPacksNotifierFamily._dependencies,
allTransitiveDependencies:
StickerPacksNotifierFamily._allTransitiveDependencies,
pubName: pubName,
);
StickerPacksNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.pubName,
}) : super.internal();
final String pubName;
@override
FutureOr<CursorPagingData<SnStickerPack>> runNotifierBuild(
covariant StickerPacksNotifier notifier,
) {
return notifier.build(pubName);
}
@override
Override overrideWith(StickerPacksNotifier Function() create) {
return ProviderOverride(
origin: this,
override: StickerPacksNotifierProvider._internal(
() => create()..pubName = pubName,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
pubName: pubName,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
StickerPacksNotifier,
CursorPagingData<SnStickerPack>
>
createElement() {
return _StickerPacksNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is StickerPacksNotifierProvider && other.pubName == pubName;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, pubName.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin StickerPacksNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnStickerPack>> {
/// The parameter `pubName` of this provider.
String get pubName;
}
class _StickerPacksNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
StickerPacksNotifier,
CursorPagingData<SnStickerPack>
>
with StickerPacksNotifierRef {
_StickerPacksNotifierProviderElement(super.provider);
@override
String get pubName => (origin as StickerPacksNotifierProvider).pubName;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -17,6 +17,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/pods/network.dart';
import 'package:styled_widget/styled_widget.dart';
part 'explore.g.dart';
@ -131,10 +132,16 @@ class _ActivityListView extends HookConsumerWidget {
switch (item.type) {
case 'posts.new':
case 'posts.new.replies':
final isReply = item.type == 'posts.new.replies';
itemWidget = PostItem(
backgroundColor:
isWideScreen(context) ? Colors.transparent : null,
item: SnPost.fromJson(item.data),
padding:
isReply
? EdgeInsets.only(left: 16, right: 16, bottom: 16)
: null,
onRefresh: (_) {
activitiesNotifier.forceRefresh();
},
@ -145,6 +152,21 @@ class _ActivityListView extends HookConsumerWidget {
);
},
);
if (isReply) {
itemWidget = Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const Icon(Symbols.reply),
const Gap(8),
Text('Replying your post'),
],
).padding(horizontal: 20, vertical: 8),
itemWidget,
],
);
}
break;
case 'accounts.check-in':
itemWidget = CheckInActivityWidget(item: item);

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
@ -7,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/markdown.dart';
@ -21,8 +23,18 @@ part 'notification.g.dart';
@riverpod
class NotificationUnreadCountNotifier
extends _$NotificationUnreadCountNotifier {
StreamSubscription<WebSocketPacket>? _subscription;
@override
Future<int> build() async {
// Subscribe to websocket events when this provider is built
_subscribeToWebSocket();
// Dispose the subscription when this provider is disposed
ref.onDispose(() {
_subscription?.cancel();
});
try {
final client = ref.read(apiClientProvider);
final response = await client.get('/notifications/count');
@ -32,9 +44,23 @@ class NotificationUnreadCountNotifier
}
}
void _subscribeToWebSocket() {
final webSocketService = ref.read(websocketProvider);
_subscription = webSocketService.dataStream.listen((packet) {
if (packet.type == 'notifications.new') {
_incrementCounter();
}
});
}
Future<void> _incrementCounter() async {
final current = await future;
state = AsyncData(current + 1);
}
Future<void> decrement(int count) async {
final current = await future;
state = AsyncData(math.min(current - count, 0));
state = AsyncData(math.max(current - count, 0));
}
}
@ -94,6 +120,7 @@ class NotificationScreen extends HookConsumerWidget {
notifierRefreshable: notificationListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
@ -142,7 +169,7 @@ class NotificationScreen extends HookConsumerWidget {
],
),
trailing:
notification.viewedAt == null
notification.viewedAt != null
? null
: Container(
width: 12,

View File

@ -7,7 +7,7 @@ part of 'notification.dart';
// **************************************************************************
String _$notificationUnreadCountNotifierHash() =>
r'074143cf208a3afe1495be405198532a23ef77c8';
r'372a2cc259d7d838cd4f33a9129f7396ef31dbb9';
/// See also [NotificationUnreadCountNotifier].
@ProviderFor(NotificationUnreadCountNotifier)

View File

@ -1,6 +1,3 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
@ -18,13 +15,15 @@ import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/services/file.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/attachment_preview.dart';
import 'package:island/widgets/post/publishers_modal.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:super_clipboard/super_clipboard.dart';
@RoutePage()
class PostEditScreen extends HookConsumerWidget {
@ -53,7 +52,14 @@ class PostEditScreen extends HookConsumerWidget {
@RoutePage()
class PostComposeScreen extends HookConsumerWidget {
final SnPost? originalPost;
const PostComposeScreen({super.key, this.originalPost});
final SnPost? repliedPost;
final SnPost? forwardedPost;
const PostComposeScreen({
super.key,
this.originalPost,
this.repliedPost,
this.forwardedPost,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -90,9 +96,14 @@ class PostComposeScreen extends HookConsumerWidget {
text: originalPost?.description,
);
final contentController = useTextEditingController(
text: originalPost?.content,
text:
originalPost?.content ??
(forwardedPost != null ? '> ${forwardedPost!.content}\n\n' : null),
);
// Add visibility state with default value from original post or 0 (public)
final visibility = useState<int>(originalPost?.visibility ?? 0);
final submitting = useState(false);
Future<void> pickPhotoMedia() async {
@ -125,21 +136,14 @@ class PostComposeScreen extends HookConsumerWidget {
final attachment = attachments.value[index];
if (attachment is SnCloudFile) return;
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
try {
attachmentProgress.value = {...attachmentProgress.value, index: 0};
final cloudFile =
await putMediaToCloud(
fileData: attachment.data,
atk: atk,
fileData: attachment,
atk: token,
baseUrl: baseUrl,
filename: attachment.data.name ?? 'Post media',
mimetype:
@ -195,12 +199,18 @@ class PostComposeScreen extends HookConsumerWidget {
await client.request(
originalPost == null ? '/posts' : '/posts/${originalPost!.id}',
data: {
'title': titleController.text,
'description': descriptionController.text,
'content': contentController.text,
'visibility':
visibility.value, // Add visibility field to API request
'attachments':
attachments.value
.where((e) => e.isOnCloud)
.map((e) => e.data.id)
.toList(),
if (repliedPost != null) 'replied_post_id': repliedPost!.id,
if (forwardedPost != null) 'forwarded_post_id': forwardedPost!.id,
},
options: Options(
headers: {'X-Pub': currentPublisher.value?.name},
@ -217,51 +227,149 @@ class PostComposeScreen extends HookConsumerWidget {
}
}
Future<void> _handlePaste() async {
final clipboard = SystemClipboard.instance;
Future<void> handlePaste() async {
final clipboard = await Pasteboard.image;
if (clipboard == null) return;
final reader = await clipboard.read();
if (reader.canProvide(Formats.png)) {
reader.getFile(Formats.png, (file) async {
final stream = file.getStream();
final bytes = await stream.toList();
final imageBytes = bytes.expand((e) => e).toList();
// Create a temporary file to store the image
final tempDir = Directory.systemTemp;
final tempFile = File(
'${tempDir.path}/pasted_image_${DateTime.now().millisecondsSinceEpoch}.png',
);
await tempFile.writeAsBytes(imageBytes);
// Add the file to attachments
attachments.value = [
...attachments.value,
UniversalFile(
data: XFile(tempFile.path),
type: UniversalFileType.image,
),
];
});
}
attachments.value = [
...attachments.value,
UniversalFile(
data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
type: UniversalFileType.image,
),
];
}
void _handleKeyPress(RawKeyEvent event) {
void handleKeyPress(RawKeyEvent event) {
if (event is! RawKeyDownEvent) return;
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
if (isPaste && isModifierPressed) {
_handlePaste();
handlePaste();
}
}
void showVisibilityModal() {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('postVisibility'.tr()),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(Symbols.public),
title: Text('postVisibilityPublic'.tr()),
onTap: () {
visibility.value = 0;
Navigator.pop(context);
},
selected: visibility.value == 0,
),
ListTile(
leading: Icon(Symbols.group),
title: Text('postVisibilityFriends'.tr()),
onTap: () {
visibility.value = 1;
Navigator.pop(context);
},
selected: visibility.value == 1,
),
ListTile(
leading: Icon(Symbols.link_off),
title: Text('postVisibilityUnlisted'.tr()),
onTap: () {
visibility.value = 2;
Navigator.pop(context);
},
selected: visibility.value == 2,
),
ListTile(
leading: Icon(Symbols.lock),
title: Text('postVisibilityPrivate'.tr()),
onTap: () {
visibility.value = 3;
Navigator.pop(context);
},
selected: visibility.value == 3,
),
],
),
),
);
}
// Helper method to get the appropriate icon for each visibility status
IconData getVisibilityIcon(int visibilityValue) {
switch (visibilityValue) {
case 1: // Friends
return Symbols.group;
case 2: // Unlisted
return Symbols.link_off;
case 3: // Private
return Symbols.lock;
default: // Public (0) or unknown
return Symbols.public;
}
}
// Helper method to get the translation key for each visibility status
String getVisibilityText(int visibilityValue) {
switch (visibilityValue) {
case 1: // Friends
return 'postVisibilityFriends';
case 2: // Unlisted
return 'postVisibilityUnlisted';
case 3: // Private
return 'postVisibilityPrivate';
default: // Public (0) or unknown
return 'postVisibilityPublic';
}
}
return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title:
isWideScreen(context)
? Text(originalPost != null ? 'editPost'.tr() : 'newPost'.tr())
: null,
actions: [
if (isWideScreen(context))
Tooltip(
message: 'keyboard_shortcuts'.tr(),
child: IconButton(
icon: const Icon(Symbols.keyboard),
onPressed: () {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('keyboard_shortcuts'.tr()),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Ctrl/Cmd + Enter: ${'submit'.tr()}'),
Text('Ctrl/Cmd + V: ${'paste'.tr()}'),
Text('Ctrl/Cmd + I: ${'add_image'.tr()}'),
Text('Ctrl/Cmd + Shift + V: ${'add_video'.tr()}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('close'.tr()),
),
],
),
);
},
),
),
IconButton(
onPressed: submitting.value ? null : performAction,
icon:
@ -284,6 +392,48 @@ class PostComposeScreen extends HookConsumerWidget {
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (repliedPost != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Theme.of(
context,
).colorScheme.surfaceVariant.withOpacity(0.5),
child: Row(
children: [
const Icon(Symbols.reply, size: 16),
const Gap(8),
Expanded(
child: Text(
'${'reply'.tr()}: ${repliedPost!.publisher.nick}',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
if (forwardedPost != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Theme.of(
context,
).colorScheme.surfaceVariant.withOpacity(0.5),
child: Row(
children: [
const Icon(Symbols.forward, size: 16),
const Gap(8),
Expanded(
child: Text(
'${'forward'.tr()}: ${forwardedPost!.publisher.nick}',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Expanded(
child: Row(
spacing: 12,
@ -291,7 +441,7 @@ class PostComposeScreen extends HookConsumerWidget {
children: [
GestureDetector(
child: ProfilePictureWidget(
fileId: currentPublisher.value?.pictureId,
fileId: currentPublisher.value?.picture?.id,
radius: 20,
fallbackIcon:
currentPublisher.value == null
@ -312,11 +462,56 @@ class PostComposeScreen extends HookConsumerWidget {
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
OutlinedButton(
onPressed: () {
showVisibilityModal();
},
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
side: BorderSide(
color: Theme.of(
context,
).colorScheme.primary.withOpacity(0.5),
),
padding: EdgeInsets.symmetric(horizontal: 16),
visualDensity: const VisualDensity(
vertical: -2,
horizontal: -4,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
getVisibilityIcon(visibility.value),
size: 16,
color:
Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 6),
Text(
getVisibilityText(visibility.value).tr(),
style: TextStyle(
fontSize: 14,
color:
Theme.of(context).colorScheme.primary,
),
),
],
),
),
],
).padding(bottom: 6),
TextField(
controller: titleController,
decoration: InputDecoration.collapsed(
hintText: 'Title',
hintText: 'postTitle'.tr(),
),
style: TextStyle(fontSize: 16),
onTapOutside:
@ -326,7 +521,7 @@ class PostComposeScreen extends HookConsumerWidget {
TextField(
controller: descriptionController,
decoration: InputDecoration.collapsed(
hintText: 'Description',
hintText: 'postDescription'.tr(),
),
style: TextStyle(fontSize: 16),
onTapOutside:
@ -336,7 +531,7 @@ class PostComposeScreen extends HookConsumerWidget {
const Gap(8),
RawKeyboardListener(
focusNode: FocusNode(),
onKey: _handleKeyPress,
onKey: handleKeyPress,
child: TextField(
controller: contentController,
style: TextStyle(fontSize: 14),
@ -345,6 +540,7 @@ class PostComposeScreen extends HookConsumerWidget {
hintText: 'postPlaceholder'.tr(),
isDense: true,
),
maxLines: null,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
@ -352,34 +548,81 @@ class PostComposeScreen extends HookConsumerWidget {
),
),
const Gap(8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
for (
var idx = 0;
idx < attachments.value.length;
idx++
)
AttachmentPreview(
item: attachments.value[idx],
progress: attachmentProgress.value[idx],
onRequestUpload: () => uploadAttachment(idx),
onDelete: () => deleteAttachment(idx),
onMove: (delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(
idx + delta,
clone.removeAt(idx),
);
attachments.value = clone;
},
),
],
LayoutBuilder(
builder: (context, constraints) {
final isWide = isWideScreen(context);
return isWide
? Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (
var idx = 0;
idx < attachments.value.length;
idx++
)
SizedBox(
width: constraints.maxWidth / 2 - 4,
child: AttachmentPreview(
item: attachments.value[idx],
progress:
attachmentProgress.value[idx],
onRequestUpload:
() => uploadAttachment(idx),
onDelete: () => deleteAttachment(idx),
onMove: (delta) {
if (idx + delta < 0 ||
idx + delta >=
attachments.value.length) {
return;
}
final clone = List.of(
attachments.value,
);
clone.insert(
idx + delta,
clone.removeAt(idx),
);
attachments.value = clone;
},
),
),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
for (
var idx = 0;
idx < attachments.value.length;
idx++
)
AttachmentPreview(
item: attachments.value[idx],
progress: attachmentProgress.value[idx],
onRequestUpload:
() => uploadAttachment(idx),
onDelete: () => deleteAttachment(idx),
onMove: (delta) {
if (idx + delta < 0 ||
idx + delta >=
attachments.value.length) {
return;
}
final clone = List.of(
attachments.value,
);
clone.insert(
idx + delta,
clone.removeAt(idx),
);
attachments.value = clone;
},
),
],
);
},
),
],
),
@ -414,204 +657,3 @@ class PostComposeScreen extends HookConsumerWidget {
);
}
}
class AttachmentPreview extends StatelessWidget {
final UniversalFile item;
final double? progress;
final Function(int)? onMove;
final Function? onDelete;
final Function? onRequestUpload;
const AttachmentPreview({
super.key,
required this.item,
this.progress,
this.onRequestUpload,
this.onMove,
this.onDelete,
});
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio:
(item.isOnCloud ? (item.data.fileMeta?['ratio'] ?? 1) : 1).toDouble(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
fit: StackFit.expand,
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Builder(
builder: (context) {
if (item.isOnCloud) {
return CloudFileWidget(item: item.data);
} else if (item.data is XFile) {
if (item.type == UniversalFileType.image) {
return Image.file(File(item.data.path));
} else {
return Center(
child: Text(
'Preview is not supported for ${item.type}',
textAlign: TextAlign.center,
),
);
}
} else if (item is List<int> || item is Uint8List) {
if (item.type == UniversalFileType.image) {
return Image.memory(item.data);
} else {
return Center(
child: Text(
'Preview is not supported for ${item.type}',
textAlign: TextAlign.center,
),
);
}
}
return Placeholder();
},
),
),
if (progress != null)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.3),
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (progress != null)
Text(
'${progress!.toStringAsFixed(2)}%',
style: TextStyle(color: Colors.white),
)
else
Text(
'uploading'.tr(),
style: TextStyle(color: Colors.white),
),
Gap(6),
Center(child: LinearProgressIndicator(value: progress)),
],
),
),
),
Positioned(
left: 8,
top: 8,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.black.withOpacity(0.5),
child: Material(
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (onDelete != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.delete,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onDelete?.call();
},
),
if (onDelete != null && onMove != null)
SizedBox(
height: 26,
child: const VerticalDivider(
width: 0.3,
color: Colors.white,
thickness: 0.3,
),
).padding(horizontal: 2),
if (onMove != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.keyboard_arrow_up,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(-1);
},
),
if (onMove != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.keyboard_arrow_down,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(1);
},
),
],
),
),
),
),
),
if (onRequestUpload != null)
Positioned(
top: 8,
right: 8,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => onRequestUpload?.call(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.black.withOpacity(0.5),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child:
(item.isOnCloud)
? Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.cloud,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-cloud',
style: TextStyle(color: Colors.white),
),
],
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.cloud_off,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-device',
style: TextStyle(color: Colors.white),
),
],
),
),
),
),
),
],
),
),
);
}
}

View File

@ -1,4 +1,4 @@
import 'package:auto_route/annotations.dart';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -29,9 +29,9 @@ Future<SnPublisher> publisher(Ref ref, String uname) async {
@riverpod
Future<List<SnAccountBadge>> publisherBadges(Ref ref, String pubName) async {
final pub = await ref.watch(publisherProvider(pubName).future);
if (pub.type != 0) return [];
if (pub.type != 0 || pub.account == null) return [];
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get("/accounts/${pub.name}/badges");
final resp = await apiClient.get("/accounts/${pub.account!.name}/badges");
return List<SnAccountBadge>.from(
resp.data.map((x) => SnAccountBadge.fromJson(x)),
);
@ -108,8 +108,8 @@ class PublisherProfileScreen extends HookConsumerWidget {
leading: PageBackButton(shadows: [iconShadow]),
flexibleSpace: FlexibleSpaceBar(
background:
data.backgroundId != null
? CloudImageWidget(fileId: data.backgroundId!)
data.background?.id != null
? CloudImageWidget(fileId: data.background!.id)
: Container(
color:
Theme.of(context).appBarTheme.backgroundColor,
@ -139,7 +139,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
shadows: [iconShadow],
),
),
error: (_, __) => const SizedBox(),
error: (_, _) => const SizedBox(),
loading:
() => const SizedBox(
width: 48,
@ -163,7 +163,10 @@ class PublisherProfileScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20,
children: [
ProfilePictureWidget(fileId: data.pictureId!, radius: 32),
ProfilePictureWidget(
fileId: data.picture!.id,
radius: 32,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@ -177,9 +180,29 @@ class PublisherProfileScreen extends HookConsumerWidget {
).fontSize(14).opacity(0.85),
],
),
if (data.type == 0)
if (data.type == 0 && data.account != null)
InkWell(
onTap: () {
context.router.pushPath(
'/account/${data.account!.name}',
);
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 4,
children: [
Text(
'publisherVisitAccountPage'.tr(
args: ['@${data.account!.name}'],
),
).fontSize(14),
Icon(Icons.launch, size: 14),
],
).opacity(0.85),
).padding(bottom: 6),
if (data.type == 0 && data.account != null)
AccountStatusWidget(
uname: name,
uname: data.account!.name,
padding: EdgeInsets.zero,
),
],

View File

@ -145,7 +145,7 @@ class _PublisherProviderElement
String get uname => (origin as PublisherProvider).uname;
}
String _$publisherBadgesHash() => r'b26d8804ddc9734c453bdf76af0a9336f166542c';
String _$publisherBadgesHash() => r'a5781deded7e682a781ccd7854418f050438e3f4';
/// See also [publisherBadges].
@ProviderFor(publisherBadges)

View File

@ -54,8 +54,8 @@ class RealmDetailScreen extends HookConsumerWidget {
leading: PageBackButton(shadows: [iconShadow]),
flexibleSpace: FlexibleSpaceBar(
background:
realm!.backgroundId != null
? CloudImageWidget(fileId: realm.backgroundId!)
realm!.background?.id != null
? CloudImageWidget(fileId: realm.background!.id)
: Container(
color:
Theme.of(context).appBarTheme.backgroundColor,
@ -118,7 +118,7 @@ class _RealmActionMenu extends HookConsumerWidget {
final isModerator = realmIdentityAsync.when(
data: (identity) => (identity?.role ?? 0) >= 50,
loading: () => false,
error: (_, __) => false,
error: (_, _) => false,
);
return PopupMenuButton(
@ -212,7 +212,7 @@ class _RealmActionMenu extends HookConsumerWidget {
child: Center(child: CircularProgressIndicator()),
),
error:
(_, __) => PopupMenuItem(
(_, _) => PopupMenuItem(
child: Row(
children: [
Icon(
@ -403,7 +403,7 @@ class _RealmMemberListSheet extends HookConsumerWidget {
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(
fileId: member.account!.profile.pictureId,
fileId: member.account!.profile.picture?.id,
),
title: Row(
spacing: 6,

View File

@ -49,13 +49,13 @@ class RealmListScreen extends HookConsumerWidget {
label: Text(
realmInvites.when(
data: (invites) => invites.length.toString(),
error: (_, __) => '0',
error: (_, _) => '0',
loading: () => '0',
),
),
isLabelVisible: realmInvites.when(
data: (invites) => invites.isNotEmpty,
error: (_, __) => false,
error: (_, _) => false,
loading: () => false,
),
child: const Icon(Symbols.email),
@ -97,7 +97,7 @@ class RealmListScreen extends HookConsumerWidget {
return ListTile(
isThreeLine: true,
leading: ProfilePictureWidget(
fileId: value[item].pictureId,
fileId: value[item].picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(value[item].name),
@ -211,19 +211,15 @@ class EditRealmScreen extends HookConsumerWidget {
submitting.value = true;
try {
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Access token is null');
final cloudFile =
await putMediaToCloud(
fileData: result,
atk: atk,
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
@ -455,7 +451,7 @@ class _RealmInviteSheet extends HookConsumerWidget {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.realm!.pictureId,
fileId: invite.realm!.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(invite.realm!.name),

View File

@ -6,10 +6,13 @@ import 'package:dropdown_button2/dropdown_button2.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:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
@ -26,7 +29,10 @@ class SettingsScreen extends HookConsumerWidget {
final serverUrl = ref.watch(serverUrlProvider);
final prefs = ref.watch(sharedPreferencesProvider);
final controller = TextEditingController(text: serverUrl);
final settings = ref.watch(appSettingsProvider);
final settings = ref.watch(appSettingsNotifierProvider);
final isDesktop =
!kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
final isWide = isWideScreen(context);
final docBasepath = useState<String?>(null);
@ -37,200 +43,565 @@ class SettingsScreen extends HookConsumerWidget {
return null;
}, []);
return AppScaffold(
noBackground: false,
appBar: AppBar(title: const Text('Settings')),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
minLeadingWidth: 48,
title: Text('settingsDisplayLanguage').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.translate),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<Locale?>(
isExpanded: true,
items: [
...EasyLocalization.of(
context,
)!.supportedLocales.mapIndexed((idx, ele) {
return DropdownMenuItem<Locale?>(
value: ele,
child: Text(
'${ele.languageCode}-${ele.countryCode}',
).fontSize(14),
);
}),
DropdownMenuItem<Locale?>(
value: null,
child: Text('languageFollowSystem').tr().fontSize(14),
),
],
value: EasyLocalization.of(context)!.currentLocale,
onChanged: (Locale? value) {
if (value != null) {
EasyLocalization.of(context)!.setLocale(value);
} else {
EasyLocalization.of(context)!.resetLocale();
}
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
height: 40,
width: 160,
),
menuItemStyleData: const MenuItemStyleData(height: 40),
),
// Group settings into categories for better organization
final appearanceSettings = [
// Language settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsDisplayLanguage').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.translate),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<Locale?>(
isExpanded: true,
items: [
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((
idx,
ele,
) {
return DropdownMenuItem<Locale?>(
value: ele,
child: Text(
'${ele.languageCode}-${ele.countryCode}',
).fontSize(14),
);
}),
DropdownMenuItem<Locale?>(
value: null,
child: Text('languageFollowSystem').tr().fontSize(14),
),
],
value: EasyLocalization.of(context)!.currentLocale,
onChanged: (Locale? value) {
if (value != null) {
EasyLocalization.of(context)!.setLocale(value);
} else {
EasyLocalization.of(context)!.resetLocale();
}
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
height: 40,
width: 160,
),
ListTile(
isThreeLine: true,
minLeadingWidth: 48,
title: Text('settingsServerUrl').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.link),
subtitle: Padding(
padding: const EdgeInsets.only(top: 6),
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: kNetworkServerDefault,
suffixIcon: IconButton(
icon: const Icon(Symbols.restart_alt),
onPressed: () {
controller.text = kNetworkServerDefault;
prefs.setString(
kNetworkServerStoreKey,
kNetworkServerDefault,
);
ref.invalidate(serverUrlProvider);
showSnackBar(context, 'settingsApplied'.tr());
menuItemStyleData: const MenuItemStyleData(height: 40),
),
),
),
// Custom fonts settings
ListTile(
isThreeLine: true,
minLeadingWidth: 48,
title: Text('settingsCustomFonts').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.font_download),
subtitle: Padding(
padding: const EdgeInsets.only(top: 6),
child: TextField(
controller: TextEditingController(text: settings.customFonts),
decoration: InputDecoration(
hintText: 'Nunito, Arial, sans-serif',
helperText: 'settingsCustomFontsHelper'.tr(),
suffixIcon: IconButton(
icon: const Icon(Symbols.restart_alt),
onPressed: () {
ref
.read(appSettingsNotifierProvider.notifier)
.setCustomFonts(null);
showSnackBar(context, 'settingsApplied'.tr());
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
isDense: true,
),
onSubmitted: (value) {
ref
.read(appSettingsNotifierProvider.notifier)
.setCustomFonts(value.isEmpty ? null : value);
showSnackBar(context, 'settingsApplied'.tr());
},
),
),
),
// Color scheme settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsColorScheme').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.palette),
trailing: GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) {
Color selectedColor =
settings.appColorScheme != null
? Color(settings.appColorScheme!)
: Colors.indigo;
return AlertDialog(
title: Text('settingsColorScheme').tr(),
content: SingleChildScrollView(
child: ColorPicker(
enableAlpha: false,
pickerColor: selectedColor,
onColorChanged: (color) {
selectedColor = color;
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
isDense: true,
),
onSubmitted: (value) {
if (value.isNotEmpty) {
prefs.setString(kNetworkServerStoreKey, value);
ref.invalidate(serverUrlProvider);
showSnackBar(context, 'settingsApplied'.tr());
}
},
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel').tr(),
),
TextButton(
onPressed: () {
ref
.read(appSettingsNotifierProvider.notifier)
.setAppColorScheme(selectedColor.value);
Navigator.of(context).pop();
},
child: Text('Confirm').tr(),
),
],
);
},
);
},
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color:
settings.appColorScheme != null
? Color(settings.appColorScheme!)
: Colors.indigo,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
width: 2,
),
),
if (!kIsWeb && docBasepath.value != null)
),
),
),
// Background image settings (only for non-web platforms)
if (!kIsWeb && docBasepath.value != null)
ListTile(
minLeadingWidth: 48,
title: Text('settingsBackgroundImage').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.image),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isDesktop)
Tooltip(
message: 'settingsBackgroundImageTooltip'.tr(),
padding: EdgeInsets.only(left: 8),
child: const Icon(Symbols.info, size: 18),
),
const Icon(Symbols.chevron_right),
],
),
onTap: () async {
final imagePicker = ref.read(imagePickerProvider);
final image = await imagePicker.pickImage(
source: ImageSource.gallery,
);
if (image == null) return;
await File(
image.path,
).copy('${docBasepath.value}/$kAppBackgroundImagePath');
prefs.setBool(kAppBackgroundStoreKey, true);
ref.invalidate(backgroundImageFileProvider);
if (context.mounted) {
showSnackBar(context, 'settingsApplied'.tr());
}
},
),
// Clear background image option
if (!kIsWeb && docBasepath.value != null)
FutureBuilder<bool>(
future:
File('${docBasepath.value}/$kAppBackgroundImagePath').exists(),
builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) {
return const SizedBox.shrink();
}
return ListTile(
minLeadingWidth: 48,
title: Text('settingsBackgroundImageClear').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.texture),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
File(
'${docBasepath.value}/$kAppBackgroundImagePath',
).deleteSync();
prefs.remove(kAppBackgroundStoreKey);
ref.invalidate(backgroundImageFileProvider);
if (context.mounted) {
showSnackBar(context, 'settingsApplied'.tr());
}
},
);
},
),
];
final serverSettings = [
// Server URL settings
ListTile(
isThreeLine: true,
minLeadingWidth: 48,
title: Text('settingsServerUrl').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.link),
subtitle: Padding(
padding: const EdgeInsets.only(top: 6),
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: kNetworkServerDefault,
suffixIcon: IconButton(
icon: const Icon(Symbols.restart_alt),
onPressed: () {
controller.text = kNetworkServerDefault;
prefs.setString(
kNetworkServerStoreKey,
kNetworkServerDefault,
);
ref.invalidate(serverUrlProvider);
showSnackBar(context, 'settingsApplied'.tr());
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
isDense: true,
),
onSubmitted: (value) {
if (value.isNotEmpty) {
prefs.setString(kNetworkServerStoreKey, value);
ref.invalidate(serverUrlProvider);
showSnackBar(context, 'settingsApplied'.tr());
}
},
),
),
),
];
final behaviorSettings = [
// Auto translate settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsAutoTranslate').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.translate),
trailing: Switch(
value: settings.autoTranslate,
onChanged: (value) {
ref
.read(appSettingsNotifierProvider.notifier)
.setAutoTranslate(value);
},
),
),
// Sound effects settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsSoundEffects').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.volume_up),
trailing: Switch(
value: settings.soundEffects,
onChanged: (value) {
ref
.read(appSettingsNotifierProvider.notifier)
.setSoundEffects(value);
},
),
),
// April Fool features settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsAprilFoolFeatures').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.celebration),
trailing: Switch(
value: settings.aprilFoolFeatures,
onChanged: (value) {
ref
.read(appSettingsNotifierProvider.notifier)
.setAprilFoolFeatures(value);
},
),
),
// Enter to send settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsEnterToSend').tr(),
subtitle:
isDesktop
? Text('settingsEnterToSendDesktopHint').tr().fontSize(12)
: null,
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.send),
trailing: Switch(
value: settings.enterToSend,
onChanged: (value) {
ref
.read(appSettingsNotifierProvider.notifier)
.setEnterToSend(value);
},
),
),
// Transparent app bar settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsTransparentAppBar').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.blur_on),
trailing: Switch(
value: settings.appBarTransparent,
onChanged: (value) {
ref
.read(appSettingsNotifierProvider.notifier)
.setAppBarTransparent(value);
},
),
),
];
// Desktop-specific settings
final desktopSettings =
!isDesktop
? <Widget>[]
: <Widget>[
ListTile(
minLeadingWidth: 48,
title: Text('settingsBackgroundImage').tr(),
title: Text('settingsKeyboardShortcuts').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.image),
leading: const Icon(Symbols.keyboard),
onTap: () {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('settingsKeyboardShortcuts').tr(),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ShortcutRow(
shortcut: 'Ctrl+F',
description: 'Search',
),
_ShortcutRow(
shortcut: 'Ctrl+,',
description: 'Settings',
),
_ShortcutRow(
shortcut: 'Ctrl+N',
description: 'New Message',
),
_ShortcutRow(
shortcut: 'Esc',
description: 'Close Dialog',
),
// Add more shortcuts as needed
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close').tr(),
),
],
),
);
},
trailing: const Icon(Symbols.chevron_right),
onTap: () async {
final imagePicker = ref.read(imagePickerProvider);
final image = await imagePicker.pickImage(
source: ImageSource.gallery,
);
if (image == null) return;
),
];
await File(
image.path,
).copy('${docBasepath.value}/$kAppBackgroundImagePath');
prefs.setBool(kAppBackgroundStoreKey, true);
ref.invalidate(backgroundImageFileProvider);
if (context.mounted) {
showSnackBar(context, 'settingsApplied'.tr());
}
},
),
if (!kIsWeb && docBasepath.value != null)
FutureBuilder<bool>(
future:
File('${docBasepath.value}/app_background_image').exists(),
builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) {
return const SizedBox.shrink();
}
return ListTile(
title: Text('settingsBackgroundImageClear').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.texture),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
File(
'${docBasepath.value}/$kAppBackgroundImagePath',
).deleteSync();
prefs.remove(kAppBackgroundStoreKey);
ref.invalidate(backgroundImageFileProvider);
if (context.mounted) {
showSnackBar(context, 'settingsApplied'.tr());
}
},
);
},
),
const Divider(),
ListTile(
minLeadingWidth: 48,
title: Text('settingsAutoTranslate').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.translate),
trailing: Switch(
value: settings.autoTranslate,
onChanged: (value) {
ref
.read(appSettingsProvider.notifier)
.setAutoTranslate(value);
},
// Create a responsive layout based on screen width
Widget buildSettingsList() {
if (isWide) {
// Two-column layout for wide screens
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'Appearance',
children: appearanceSettings,
),
_SettingsSection(title: 'Server', children: serverSettings),
],
),
),
ListTile(
minLeadingWidth: 48,
title: Text('settingsSoundEffects').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.volume_up),
trailing: Switch(
value: settings.soundEffects,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setSoundEffects(value);
},
),
),
ListTile(
minLeadingWidth: 48,
title: Text('settingsAprilFoolFeatures').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.celebration),
trailing: Switch(
value: settings.aprilFoolFeatures,
onChanged: (value) {
ref
.read(appSettingsProvider.notifier)
.setAprilFoolFeatures(value);
},
),
),
ListTile(
minLeadingWidth: 48,
title: Text('settingsEnterToSend').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.send),
trailing: Switch(
value: settings.enterToSend,
onChanged: (value) {
ref.read(appSettingsProvider.notifier).setEnterToSend(value);
},
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(
title: 'Behavior',
children: behaviorSettings,
),
if (desktopSettings.isNotEmpty)
_SettingsSection(
title: 'Desktop',
children: desktopSettings,
),
],
),
),
],
).padding(horizontal: 16);
} else {
// Single column layout for narrow screens
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SettingsSection(title: 'Appearance', children: appearanceSettings),
_SettingsSection(title: 'Server', children: serverSettings),
_SettingsSection(title: 'Behavior', children: behaviorSettings),
if (desktopSettings.isNotEmpty)
_SettingsSection(title: 'Desktop', children: desktopSettings),
],
);
}
}
return AppScaffold(
noBackground: false,
appBar: AppBar(
title: Text('Settings').tr(),
actions:
isDesktop
? [
IconButton(
icon: const Icon(Symbols.help_outline),
onPressed: () {
// Show help dialog
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('settingsHelp').tr(),
content: Text('settingsHelpContent').tr(),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close').tr(),
),
],
),
);
},
),
]
: null,
),
body: Focus(
autofocus: true,
onKeyEvent: (node, event) {
// Add keyboard shortcuts for desktop
if (isDesktop &&
event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
context.router.pop();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(vertical: 16),
child: buildSettingsList(),
),
),
);
}
}
// Helper widget for displaying settings sections with titles
class _SettingsSection extends StatelessWidget {
final String title;
final List<Widget> children;
const _SettingsSection({required this.title, required this.children});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
...children,
const SizedBox(height: 16),
],
);
}
}
// Helper widget for displaying keyboard shortcuts
class _ShortcutRow extends StatelessWidget {
final String shortcut;
final String description;
const _ShortcutRow({required this.shortcut, required this.description});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
),
),
child: Text(shortcut, style: TextStyle(fontFamily: 'monospace')),
),
SizedBox(width: 16),
Text(description),
],
),
);
}
}

View File

@ -39,7 +39,7 @@ Future<XFile?> cropImage(
}
Completer<SnCloudFile?> putMediaToCloud({
required dynamic fileData, // Can be XFile or List<int> (Uint8List)
required UniversalFile fileData,
required String atk,
required String baseUrl,
String? filename,
@ -51,21 +51,27 @@ Completer<SnCloudFile?> putMediaToCloud({
String actualMimetype = mimetype ?? '';
Uint8List? byteData;
if (fileData is XFile) {
file = fileData;
actualFilename = filename ?? fileData.name;
actualMimetype = mimetype ?? fileData.mimeType ?? '';
} else if (fileData is List<int> || fileData is Uint8List) {
byteData = fileData is List<int> ? Uint8List.fromList(fileData) : fileData;
// Handle the data based on what's in the UniversalFile
final data = fileData.data;
if (data is XFile) {
file = data;
actualFilename = filename ?? data.name;
actualMimetype = mimetype ?? data.mimeType ?? '';
} else if (data is List<int> || data is Uint8List) {
byteData = data is List<int> ? Uint8List.fromList(data) : data;
actualFilename = filename ?? 'uploaded_file';
actualMimetype = mimetype ?? 'application/octet-stream';
if (mimetype == null) {
throw ArgumentError('Mimetype is required when providing raw bytes.');
}
file = XFile.fromData(byteData!, mimeType: actualMimetype);
} else if (data is SnCloudFile) {
// If the file is already on the cloud, just return it
return Completer<SnCloudFile?>()..complete(data);
} else {
throw ArgumentError(
'Invalid fileData type. Expected XFile or List<int> (Uint8List).',
'Invalid fileData type. Expected data to be XFile, List<int>, Uint8List, or SnCloudFile.',
);
}

View File

@ -81,7 +81,7 @@ class AccountPickerSheet extends HookConsumerWidget {
final account = accounts[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: account.profile.pictureId,
fileId: account.profile.picture?.id,
),
title: Text(account.nick),
subtitle: Text('@${account.name}'),

View File

@ -146,7 +146,7 @@ class StatusActivityWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(
fileId: item.account.profile.pictureId,
fileId: item.account.profile.picture?.id,
radius: 12,
),
Expanded(

View File

@ -13,8 +13,6 @@ import 'package:island/services/responsive.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:path_provider/path_provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/chat/call_overlay.dart';
import 'package:island/pods/call.dart';
class WindowScaffold extends HookConsumerWidget {
final Widget child;
@ -152,22 +150,8 @@ class AppScaffold extends StatelessWidget {
noBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor,
body: Stack(
children: [
SizedBox.expand(
child:
noBackground
? content
: AppBackground(isRoot: true, child: content),
),
Positioned(
left: 16,
right: 16,
bottom: 8,
child: const _GlobalCallOverlay(),
),
],
),
body:
noBackground ? content : AppBackground(isRoot: true, child: content),
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
@ -206,23 +190,6 @@ class PageBackButton extends StatelessWidget {
const kAppBackgroundImagePath = 'island_app_background';
/// Global call overlay bar (appears when in a call but not on the call screen)
class _GlobalCallOverlay extends HookConsumerWidget {
const _GlobalCallOverlay();
@override
Widget build(BuildContext context, WidgetRef ref) {
final callState = ref.watch(callNotifierProvider);
// Find current route name
final modalRoute = ModalRoute.of(context);
final isOnCallScreen = modalRoute?.settings.name?.contains('call') ?? false;
// You may want to store roomId in callState for more robust navigation
if (callState.isConnected && !isOnCallScreen) {
return CallOverlayBar();
}
return const SizedBox.shrink();
}
}
final backgroundImageFileProvider = FutureProvider<File?>((ref) async {
if (kIsWeb) return null;
final dir = await getApplicationSupportDirectory();
@ -268,7 +235,7 @@ class AppBackground extends ConsumerWidget {
},
loading: () => const SizedBox(),
error:
(_, __) => Material(
(_, _) => Material(
color: Theme.of(context).colorScheme.surface,
child: child,
),

View File

@ -14,6 +14,7 @@ part 'call_button.g.dart';
@riverpod
Future<SnRealtimeCall?> ongoingCall(Ref ref, String roomId) async {
if (roomId.isEmpty) return null;
try {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/chat/realtime/$roomId');

View File

@ -6,7 +6,7 @@ part of 'call_button.dart';
// RiverpodGenerator
// **************************************************************************
String _$ongoingCallHash() => r'd8a942e6695a7da702daeaa452464c16761ef6e7';
String _$ongoingCallHash() => r'ab7337bcd4d766897bd6d6a38f418c6bdd15eb94';
/// Copied from Dart SDK
class _SystemHash {

View File

@ -1,10 +1,94 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/call.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart';
import 'package:island/widgets/chat/call_participant_tile.dart';
import 'package:styled_widget/styled_widget.dart';
class CallControlsBar extends HookConsumerWidget {
const CallControlsBar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final callState = ref.watch(callNotifierProvider);
final callNotifier = ref.read(callNotifierProvider.notifier);
final userInfo = ref.watch(userInfoProvider);
final actionButtonStyle = ButtonStyle(
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
);
return Card(
margin: const EdgeInsets.only(left: 12, right: 12, top: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Builder(
builder: (context) {
if (callNotifier.localParticipant == null) {
return CircularProgressIndicator().center();
}
return SizedBox(
width: 40,
height: 40,
child:
SpeakingRippleAvatar(
isSpeaking:
callNotifier.localParticipant!.isSpeaking,
audioLevel:
callNotifier.localParticipant!.audioLevel,
pictureId: userInfo.value?.profile.picture?.id,
size: 36,
).center(),
);
},
),
],
),
),
IconButton(
icon: Icon(
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
),
onPressed: () {
callNotifier.toggleMicrophone();
},
style: actionButtonStyle,
),
IconButton(
icon: Icon(
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
),
onPressed: () {
callNotifier.toggleCamera();
},
style: actionButtonStyle,
),
IconButton(
icon: Icon(
callState.isScreenSharing
? Icons.stop_screen_share
: Icons.screen_share,
),
onPressed: () {
callNotifier.toggleScreenShare();
},
style: actionButtonStyle,
),
],
).padding(all: 16),
);
}
}
/// A floating bar that appears when user is in a call but not on the call screen.
class CallOverlayBar extends HookConsumerWidget {
const CallOverlayBar({super.key});
@ -15,48 +99,124 @@ class CallOverlayBar extends HookConsumerWidget {
// Only show if connected and not on the call screen
if (!callState.isConnected) return const SizedBox.shrink();
return Positioned(
left: 16,
right: 16,
bottom: 32,
child: GestureDetector(
onTap: () {
if (callNotifier.roomId == null) return;
context.router.push(CallRoute(roomId: callNotifier.roomId!));
},
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(24),
color: Theme.of(context).colorScheme.primary,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.call, color: Colors.white),
const SizedBox(width: 12),
const Text(
'In call',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
final lastSpeaker =
callNotifier.participants
.where(
(element) => element.remoteParticipant.lastSpokeAt != null,
)
.isEmpty
? callNotifier.participants.first
: callNotifier.participants
.where(
(element) => element.remoteParticipant.lastSpokeAt != null,
)
.fold(
callNotifier.participants.first,
(value, element) =>
element.remoteParticipant.lastSpokeAt != null &&
(value.remoteParticipant.lastSpokeAt == null ||
element.remoteParticipant.lastSpokeAt!
.compareTo(
value
.remoteParticipant
.lastSpokeAt!,
) >
0)
? element
: value,
);
final actionButtonStyle = ButtonStyle(
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
);
return GestureDetector(
child: Card(
margin: EdgeInsets.zero,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Builder(
builder: (context) {
if (callNotifier.localParticipant == null) {
return CircularProgressIndicator().center();
}
return SizedBox(
width: 40,
height: 40,
child:
SpeakingRippleAvatar(
isSpeaking: lastSpeaker.isSpeaking,
audioLevel:
lastSpeaker.remoteParticipant.audioLevel,
pictureId:
lastSpeaker
.participant
.profile
?.account
.profile
.picture
?.id,
size: 36,
).center(),
);
},
),
const Gap(8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
lastSpeaker.participant.profile?.account.nick ??
'unknown'.tr(),
).bold(),
Text(
formatDuration(callState.duration),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
const Icon(
Icons.arrow_forward_ios,
color: Colors.white,
size: 18,
),
],
],
),
],
),
),
),
),
IconButton(
icon: Icon(
callState.isMicrophoneEnabled ? Icons.mic : Icons.mic_off,
),
onPressed: () {
callNotifier.toggleMicrophone();
},
style: actionButtonStyle,
),
IconButton(
icon: Icon(
callState.isCameraEnabled ? Icons.videocam : Icons.videocam_off,
),
onPressed: () {
callNotifier.toggleCamera();
},
style: actionButtonStyle,
),
IconButton(
icon: Icon(
callState.isScreenSharing
? Icons.stop_screen_share
: Icons.screen_share,
),
onPressed: () {
callNotifier.toggleScreenShare();
},
style: actionButtonStyle,
),
],
).padding(all: 16),
),
onTap: () {
context.router.push(CallRoute(roomId: callNotifier.roomId!));
},
);
}
}

View File

@ -106,7 +106,7 @@ class CallParticipantTile extends StatelessWidget {
return SpeakingRippleAvatar(
isSpeaking: live.isSpeaking,
audioLevel: audioLevel,
pictureId: live.participant.profile?.account.profile.pictureId,
pictureId: live.participant.profile?.account.profile.picture?.id,
size: 84,
);
}

View File

@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/database/message.dart';
import 'package:island/models/chat.dart';
import 'package:island/pods/call.dart';
import 'package:island/screens/chat/room.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
@ -110,7 +111,7 @@ class MessageItem extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
ProfilePictureWidget(
fileId: sender.account.profile.pictureId,
fileId: sender.account.profile.picture?.id,
radius: 16,
),
Column(
@ -413,7 +414,7 @@ class _MessageItemContent extends StatelessWidget {
);
case 'text':
default:
return MarkdownTextContent(content: item.content!);
return MarkdownTextContent(content: item.content!, isSelectable: true);
}
}
@ -429,14 +430,6 @@ class _MessageContentCall extends StatelessWidget {
@override
Widget build(BuildContext context) {
String formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
return '${hours == 0 ? '' : '$hours hours '}'
'${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [

View File

@ -151,7 +151,7 @@ class CheckInWidget extends HookConsumerWidget {
key: ValueKey(result != null),
),
loading: () => const Icon(Symbols.refresh),
error: (_, __) => const Icon(Symbols.error),
error: (_, _) => const Icon(Symbols.error),
),
),
),
@ -188,7 +188,7 @@ class CheckInActivityWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(
fileId: result.account!.profile.pictureId,
fileId: result.account!.profile.picture?.id,
radius: 12,
),
Expanded(

View File

@ -0,0 +1,228 @@
import 'dart:io';
import 'package:cross_file/cross_file.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:island/models/file.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class AttachmentPreview extends StatelessWidget {
final UniversalFile item;
final double? progress;
final Function(int)? onMove;
final Function? onDelete;
final Function? onRequestUpload;
const AttachmentPreview({
super.key,
required this.item,
this.progress,
this.onRequestUpload,
this.onMove,
this.onDelete,
});
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio:
(item.isOnCloud ? (item.data.fileMeta?['ratio'] ?? 1) : 1).toDouble(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
fit: StackFit.expand,
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Builder(
builder: (context) {
if (item.isOnCloud) {
return CloudFileWidget(item: item.data);
} else if (item.data is XFile) {
if (item.type == UniversalFileType.image) {
final file = item.data as XFile;
if (file.path.isEmpty) {
return FutureBuilder<Uint8List>(
future: file.readAsBytes(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.memory(snapshot.data!);
}
return const Center(
child: CircularProgressIndicator(),
);
},
);
}
return kIsWeb
? Image.network(file.path)
: Image.file(File(file.path));
} else {
return Center(
child: Text(
'Preview is not supported for ${item.type}',
textAlign: TextAlign.center,
),
);
}
} else if (item is List<int> || item is Uint8List) {
if (item.type == UniversalFileType.image) {
return Image.memory(item.data);
} else {
return Center(
child: Text(
'Preview is not supported for ${item.type}',
textAlign: TextAlign.center,
),
);
}
}
return Placeholder();
},
),
),
if (progress != null)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.3),
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (progress != null)
Text(
'${progress!.toStringAsFixed(2)}%',
style: TextStyle(color: Colors.white),
)
else
Text(
'uploading'.tr(),
style: TextStyle(color: Colors.white),
),
Gap(6),
Center(child: LinearProgressIndicator(value: progress)),
],
),
),
),
Positioned(
left: 8,
top: 8,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.black.withOpacity(0.5),
child: Material(
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (onDelete != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.delete,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onDelete?.call();
},
),
if (onDelete != null && onMove != null)
SizedBox(
height: 26,
child: const VerticalDivider(
width: 0.3,
color: Colors.white,
thickness: 0.3,
),
).padding(horizontal: 2),
if (onMove != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.keyboard_arrow_up,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(-1);
},
),
if (onMove != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.keyboard_arrow_down,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(1);
},
),
],
),
),
),
),
),
if (onRequestUpload != null)
Positioned(
top: 8,
right: 8,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => onRequestUpload?.call(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.black.withOpacity(0.5),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child:
(item.isOnCloud)
? Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.cloud,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-cloud',
style: TextStyle(color: Colors.white),
),
],
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.cloud_off,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-device',
style: TextStyle(color: Colors.white),
),
],
),
),
),
),
),
],
),
),
);
}
}

View File

@ -6,13 +6,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:gal/gal.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:path/path.dart' show extension;
import 'package:path_provider/path_provider.dart';
import 'package:photo_view/photo_view.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:uuid/uuid.dart';
import 'package:dio/dio.dart';
class CloudFileList extends HookConsumerWidget {
final List<SnCloudFile> files;
@ -110,6 +114,7 @@ class CloudFileList extends HookConsumerWidget {
heroTag: heroTags[i],
isImage: files[i].mimeType?.startsWith('image') ?? false,
disableZoomIn: disableZoomIn,
fit: BoxFit.cover,
),
],
onTap: (i) {
@ -159,7 +164,7 @@ class CloudFileList extends HookConsumerWidget {
),
);
},
separatorBuilder: (_, __) => const Gap(8),
separatorBuilder: (_, _) => const Gap(8),
),
),
);
@ -175,6 +180,47 @@ class CloudFileZoomIn extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final serverUrl = ref.watch(serverUrlProvider);
final photoViewController = useMemoized(() => PhotoViewController(), []);
final rotation = useState(0);
Future<void> saveToGallery() async {
try {
// Show loading indicator
final scaffold = ScaffoldMessenger.of(context);
scaffold.showSnackBar(
const SnackBar(
content: Text('Saving image to gallery...'),
duration: Duration(seconds: 1),
),
);
// Get the image URL
final imageUrl = '$serverUrl/files/${item.id}?original=true';
// Create a temporary file to save the image
final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/${item.id}.${extension(item.name)}';
await Dio().download(imageUrl, filePath);
await Gal.putImage(filePath);
// Show success message
scaffold.showSnackBar(
const SnackBar(
content: Text('Image saved to gallery'),
duration: Duration(seconds: 2),
),
);
} catch (e) {
// Show error message
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to save image: $e'),
duration: const Duration(seconds: 2),
),
);
}
}
return DismissiblePage(
isFullScreen: true,
@ -195,16 +241,119 @@ class CloudFileZoomIn extends HookConsumerWidget {
imageProvider: CloudImageWidget.provider(
fileId: item.id,
serverUrl: serverUrl,
original: true,
),
// Apply rotation transformation
customSize: MediaQuery.of(context).size,
basePosition: Alignment.center,
filterQuality: FilterQuality.high,
),
),
// Close button
// Close button and save button
Positioned(
top: MediaQuery.of(context).padding.top + 20,
right: 20,
child: IconButton(
icon: Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
top: MediaQuery.of(context).padding.top + 16,
right: 16,
left: 16,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
icon: Icon(
Icons.save_alt,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
onPressed: () async {
saveToGallery();
},
),
],
),
IconButton(
icon: Icon(
Icons.close,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
// Rotation controls
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 16,
left: 16,
right: 16,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(Icons.remove, color: Colors.white),
onPressed: () {
photoViewController.scale =
(photoViewController.scale ?? 1) - 0.05;
},
),
IconButton(
icon: Icon(Icons.add, color: Colors.white),
onPressed: () {
photoViewController.scale =
(photoViewController.scale ?? 1) + 0.05;
},
),
const Gap(8),
IconButton(
icon: Icon(
Icons.rotate_left,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
onPressed: () {
rotation.value = (rotation.value - 1) % 4;
photoViewController.rotation =
rotation.value * -math.pi / 2;
},
),
IconButton(
icon: Icon(
Icons.rotate_right,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
onPressed: () {
rotation.value = (rotation.value + 1) % 4;
photoViewController.rotation =
rotation.value * -math.pi / 2;
},
),
],
),
),
],
@ -219,6 +368,7 @@ class _CloudFileListEntry extends StatelessWidget {
final bool isImage;
final bool disableZoomIn;
final VoidCallback? onTap;
final BoxFit fit;
const _CloudFileListEntry({
required this.file,
@ -226,11 +376,13 @@ class _CloudFileListEntry extends StatelessWidget {
required this.isImage,
required this.disableZoomIn,
this.onTap,
this.fit = BoxFit.contain,
});
@override
Widget build(BuildContext context) {
final content = Stack(
fit: StackFit.expand,
children: [
if (isImage)
Positioned.fill(
@ -247,9 +399,10 @@ class _CloudFileListEntry extends StatelessWidget {
item: file,
heroTag: heroTag,
noBlurhash: true,
).center()
fit: fit,
)
else
CloudFileWidget(item: file, heroTag: heroTag),
CloudFileWidget(item: file, heroTag: heroTag, fit: fit),
],
);

View File

@ -8,9 +8,9 @@ import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/services/file.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/attachment_preview.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@ -43,15 +43,8 @@ class CloudFilePicker extends HookConsumerWidget {
if (files.value.isEmpty) return;
final baseUrl = ref.read(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw Exception("Unauthorized");
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw Exception("Unauthorized");
List<SnCloudFile> result = List.empty(growable: true);
@ -63,8 +56,8 @@ class CloudFilePicker extends HookConsumerWidget {
final file = files.value[idx];
final cloudFile =
await putMediaToCloud(
fileData: file.data,
atk: atk,
fileData: file,
atk: token,
baseUrl: baseUrl,
filename: file.data.name ?? 'Post media',
mimetype:
@ -266,7 +259,7 @@ class CloudFilePicker extends HookConsumerWidget {
progress: null,
);
},
separatorBuilder: (_, __) => const Gap(8),
separatorBuilder: (_, _) => const Gap(8),
),
),
Card(

View File

@ -79,8 +79,9 @@ class CloudImageWidget extends ConsumerWidget {
static ImageProvider provider({
required String fileId,
required String serverUrl,
bool original = false,
}) {
final uri = '$serverUrl/files/$fileId';
final uri = '$serverUrl/files/$fileId?original=$original';
return CachedNetworkImageProvider(uri);
}
}

View File

@ -1 +1,53 @@
export 'image.native.dart' if (dart.library.html) 'image.web.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
class UniversalImage extends StatelessWidget {
final String uri;
final String? blurHash;
final BoxFit fit;
final double? width;
final double? height;
final bool noCacheOptimization;
const UniversalImage({
super.key,
required this.uri,
this.blurHash,
this.fit = BoxFit.cover,
this.width,
this.height,
this.noCacheOptimization = false,
});
@override
Widget build(BuildContext context) {
int? cacheWidth;
int? cacheHeight;
if (width != null && height != null && !noCacheOptimization) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
cacheWidth = width != null ? (width! * devicePixelRatio).round() : null;
cacheHeight =
height != null ? (height! * devicePixelRatio).round() : null;
}
return SizedBox(
width: width,
height: height,
child: Stack(
fit: StackFit.expand,
children: [
if (blurHash != null) BlurHash(hash: blurHash!),
CachedNetworkImage(
imageUrl: uri,
fit: fit,
width: width,
height: height,
memCacheHeight: cacheHeight,
memCacheWidth: cacheWidth,
),
],
),
);
}
}

View File

@ -1,53 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
class UniversalImage extends StatelessWidget {
final String uri;
final String? blurHash;
final BoxFit fit;
final double? width;
final double? height;
final bool noCacheOptimization;
const UniversalImage({
super.key,
required this.uri,
this.blurHash,
this.fit = BoxFit.cover,
this.width,
this.height,
this.noCacheOptimization = false,
});
@override
Widget build(BuildContext context) {
int? cacheWidth;
int? cacheHeight;
if (width != null && height != null && !noCacheOptimization) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
cacheWidth = width != null ? (width! * devicePixelRatio).round() : null;
cacheHeight =
height != null ? (height! * devicePixelRatio).round() : null;
}
return SizedBox(
width: width,
height: height,
child: Stack(
fit: StackFit.expand,
children: [
if (blurHash != null) BlurHash(hash: blurHash!),
CachedNetworkImage(
imageUrl: uri,
fit: fit,
width: width,
height: height,
memCacheHeight: cacheHeight,
memCacheWidth: cacheWidth,
),
],
),
);
}
}

View File

@ -1,42 +0,0 @@
import 'package:web/web.dart' as web;
import 'package:flutter/material.dart';
class UniversalImage extends StatelessWidget {
final String uri;
final String? blurHash;
final BoxFit fit;
final double? width;
final double? height;
// No cache optimization for web
final bool noCacheOptimization;
const UniversalImage({
super.key,
required this.uri,
this.blurHash,
this.fit = BoxFit.cover,
this.width,
this.height,
this.noCacheOptimization = false,
});
@override
Widget build(BuildContext context) {
return HtmlElementView.fromTagName(
tagName: 'img',
onElementCreated: (element) {
element as web.HTMLImageElement;
element.src = uri;
element.style.width = width?.toString() ?? '100%';
element.style.height = height?.toString() ?? '100%';
element.style.objectFit = switch (fit) {
BoxFit.cover || BoxFit.fitWidth || BoxFit.fitHeight => 'cover',
BoxFit.fill => 'fill',
BoxFit.contain => 'contain',
BoxFit.none => 'none',
_ => 'cover',
};
},
);
}
}

Some files were not shown because too many files have changed in this diff Show More