Compare commits

..

18 Commits

Author SHA1 Message Date
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
104 changed files with 3663 additions and 1743 deletions

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

@ -0,0 +1 @@
{}

View File

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

View File

@ -34,19 +34,22 @@
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. --> to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider <provider
android:name="com.superlist.super_native_extensions.DataProvider" android:name="androidx.core.content.FileProvider"
android:authorities="dev.solsynth.solian.SuperClipboardDataProvider" android:authorities="dev.solsynth.solian.provider"
android:exported="true" android:exported="false"
android:grantUriPermissions="true" > android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider> </provider>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> 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. --> In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain" />
</intent> </intent>
</queries> </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 distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

View File

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

View File

@ -278,5 +278,42 @@
"settingsHideBottomNav": "Hide Bottom Navigation", "settingsHideBottomNav": "Hide Bottom Navigation",
"settingsSoundEffects": "Sound Effects", "settingsSoundEffects": "Sound Effects",
"settingsAprilFoolFeatures": "April Fool Features", "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 {}"
} }

View File

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

View File

@ -1,4 +1,5 @@
PODS: PODS:
- Alamofire (5.10.2)
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- croppy (0.0.1): - croppy (0.0.1):
@ -137,6 +138,8 @@ PODS:
- OrderedSet (6.0.3) - OrderedSet (6.0.3)
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- pasteboard (0.0.1):
- Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@ -185,6 +188,7 @@ PODS:
- WebRTC-SDK (125.6422.07) - WebRTC-SDK (125.6422.07)
DEPENDENCIES: DEPENDENCIES:
- Alamofire
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- croppy (from `.symlinks/plugins/croppy/ios`) - croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
@ -204,6 +208,7 @@ DEPENDENCIES:
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
@ -215,6 +220,7 @@ DEPENDENCIES:
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- Alamofire
- DKImagePickerController - DKImagePickerController
- DKPhotoGallery - DKPhotoGallery
- Firebase - Firebase
@ -271,6 +277,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/media_kit_video/ios" :path: ".symlinks/plugins/media_kit_video/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
pasteboard:
:path: ".symlinks/plugins/pasteboard/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
shared_preferences_foundation: shared_preferences_foundation:
@ -289,6 +297,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios" :path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30 croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
@ -319,6 +328,7 @@ SPEC CHECKSUMS:
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
@ -334,6 +344,6 @@ SPEC CHECKSUMS:
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
WebRTC-SDK: dff00a3892bc570b6014e046297782084071657e WebRTC-SDK: dff00a3892bc570b6014e046297782084071657e
PODFILE CHECKSUM: 2608312fddeea6d787ca3aa478c756da8f4ca66d PODFILE CHECKSUM: 0c13198c20d0416ef589aeb2e1dac5c50262254f
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View File

@ -10,12 +10,15 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 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 */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
755557017FD1B99AFC4F9127 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */; }; 755557017FD1B99AFC4F9127 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B87C0E607033790E71B54D73 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6D834CA86410B09796B312B /* Pods_Runner.framework */; }; 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 */; }; E7A0B456EF7AAA71D1397081 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9AE244813FCDFAA941430393 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -27,9 +30,27 @@
remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner; remoteInfo = Runner;
}; };
73CDD67F2DEC00480059D95D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 73CDD6792DEC00480059D95D;
remoteInfo = SolianNotificationService;
};
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase 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 */ = { 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -47,16 +68,23 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -66,10 +94,49 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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 */ /* 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,
);
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 */ /* Begin PBXFrameworksBuildPhase section */
1DFF8FEBBD0CF5A990600776 /* Frameworks */ = { 1DFF8FEBBD0CF5A990600776 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
@ -79,6 +146,14 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
73CDD6772DEC00480059D95D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D1772CE196985AE8E8C9F2E5 /* Pods_SolianNotificationService.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = { 97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -103,6 +178,8 @@
children = ( children = (
F6D834CA86410B09796B312B /* Pods_Runner.framework */, F6D834CA86410B09796B312B /* Pods_Runner.framework */,
29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */, 29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */,
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */,
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -116,6 +193,12 @@
14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */, 14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */,
14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */, 14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */,
E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.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; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@ -136,6 +219,7 @@
children = ( children = (
9740EEB11CF90186004384FC /* Flutter */, 9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
91E124CE95BCB4DCD890160D /* Pods */, 91E124CE95BCB4DCD890160D /* Pods */,
@ -149,6 +233,7 @@
children = ( children = (
97C146EE1CF9000F007C117D /* Runner.app */, 97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@ -156,6 +241,7 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
73268D272DEB012A0076E970 /* Services */,
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */, 737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FD1CF9000F007C117D /* Assets.xcassets */,
@ -165,6 +251,7 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
73D4264A2DEB815D006C0AAE /* NotifyDelegate.swift */,
); );
path = Runner; path = Runner;
sourceTree = "<group>"; sourceTree = "<group>";
@ -191,6 +278,27 @@
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test"; 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 */ = { 97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
@ -199,6 +307,7 @@
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
@ -208,6 +317,10 @@
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
73CDD6802DEC00480059D95D /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
73268D272DEB012A0076E970 /* Services */,
); );
name = Runner; name = Runner;
productName = Runner; productName = Runner;
@ -221,6 +334,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1640;
LastUpgradeCheck = 1510; LastUpgradeCheck = 1510;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
@ -228,6 +342,9 @@
CreatedOnToolsVersion = 14.0; CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D; TestTargetID = 97C146ED1CF9000F007C117D;
}; };
73CDD6792DEC00480059D95D = {
CreatedOnToolsVersion = 16.4;
};
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1; CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100; LastSwiftMigration = 1100;
@ -235,7 +352,6 @@
}; };
}; };
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en; developmentRegion = en;
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
@ -243,12 +359,14 @@
Base, Base,
); );
mainGroup = 97C146E51CF9000F007C117D; mainGroup = 97C146E51CF9000F007C117D;
preferredProjectObjectVersion = 77;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */; productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
targets = ( targets = (
97C146ED1CF9000F007C117D /* Runner */, 97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */, 331C8080294A63A400263BE5 /* RunnerTests */,
73CDD6792DEC00480059D95D /* SolianNotificationService */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@ -261,6 +379,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
73CDD6782DEC00480059D95D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = { 97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -331,6 +456,28 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0; 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 */ = { 8C0351B03869BBF493808288 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -396,12 +543,20 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
73CDD6762DEC00480059D95D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = { 97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
73D4264B2DEB815D006C0AAE /* NotifyDelegate.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -413,6 +568,11 @@
target = 97C146ED1CF9000F007C117D /* Runner */; target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
}; };
73CDD6802DEC00480059D95D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 73CDD6792DEC00480059D95D /* SolianNotificationService */;
targetProxy = 73CDD67F2DEC00480059D95D /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
@ -562,6 +722,123 @@
}; };
name = Profile; 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 */ = { 97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@ -737,6 +1014,16 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; 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" */ = { 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (

View File

@ -3,11 +3,14 @@ import UIKit
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
let notifyDelegate = NotifyDelegate()
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self) UNUserNotificationCenter.current().delegate = notifyDelegate
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) 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"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>

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,54 @@
//
// 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 = ""
if let tokenJson = UserDefaults.standard.string(forKey: "dyn_user_tk"),
let tokenData = tokenJson.data(using: String.Encoding.utf8),
let tokenDict = try? JSONSerialization.jsonObject(with: tokenData) as? [String: Any],
let tokenValue = tokenDict["token"] as? String {
token = tokenValue
} else {
return
}
let serverUrl = "https://nt.solian.app"
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> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.usernotifications.communication</key>
<true/>
</dict> </dict>
</plist> </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,13 @@
<?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>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( Future<LocalChatMessage> sendMessage(
String atk, String token,
String baseUrl, String baseUrl,
String roomId, String roomId,
String content, String content,
@ -232,7 +232,7 @@ class MessageRepository {
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: attachments[idx].data, fileData: attachments[idx].data,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: attachments[idx].data.name ?? 'Post media', filename: attachments[idx].data.name ?? 'Post media',
mimetype: mimetype:

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$SnChatRoom { 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 /// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -29,16 +29,16 @@ $SnChatRoomCopyWith<SnChatRoom> get copyWith => _$SnChatRoomCopyWithImpl<SnChatR
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory $SnChatRoomCopyWith(SnChatRoom value, $Res Function(SnChatRoom) _then) = _$SnChatRoomCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @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( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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,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?,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 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 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 bool,picture: freezed == picture ? _self.picture : picture // 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?,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?,realmId: freezed == realmId ? _self.realmId : realmId // 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 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 as SnRealm?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
@ -130,7 +128,7 @@ $SnRealmCopyWith<$Res>? get realm {
@JsonSerializable() @JsonSerializable()
class _SnChatRoom implements SnChatRoom { 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); factory _SnChatRoom.fromJson(Map<String, dynamic> json) => _$SnChatRoomFromJson(json);
@override final String id; @override final String id;
@ -138,9 +136,7 @@ class _SnChatRoom implements SnChatRoom {
@override final String? description; @override final String? description;
@override final int type; @override final int type;
@override final bool isPublic; @override final bool isPublic;
@override final String? pictureId;
@override final SnCloudFile? picture; @override final SnCloudFile? picture;
@override final String? backgroundId;
@override final SnCloudFile? background; @override final SnCloudFile? background;
@override final String? realmId; @override final String? realmId;
@override final SnRealm? realm; @override final SnRealm? realm;
@ -170,16 +166,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory _$SnChatRoomCopyWith(_SnChatRoom value, $Res Function(_SnChatRoom) _then) = __$SnChatRoomCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_SnChatRoom(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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,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?,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 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 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 bool,picture: freezed == picture ? _self.picture : picture // 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?,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?,realmId: freezed == realmId ? _self.realmId : realmId // 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 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 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?, description: json['description'] as String?,
type: (json['type'] as num).toInt(), type: (json['type'] as num).toInt(),
isPublic: json['is_public'] as bool, isPublic: json['is_public'] as bool,
pictureId: json['picture_id'] as String?,
picture: picture:
json['picture'] == null json['picture'] == null
? null ? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
backgroundId: json['background_id'] as String?,
background: background:
json['background'] == null json['background'] == null
? null ? null
@ -46,9 +44,7 @@ Map<String, dynamic> _$SnChatRoomToJson(_SnChatRoom instance) =>
'description': instance.description, 'description': instance.description,
'type': instance.type, 'type': instance.type,
'is_public': instance.isPublic, 'is_public': instance.isPublic,
'picture_id': instance.pictureId,
'picture': instance.picture?.toJson(), 'picture': instance.picture?.toJson(),
'background_id': instance.backgroundId,
'background': instance.background?.toJson(), 'background': instance.background?.toJson(),
'realm_id': instance.realmId, 'realm_id': instance.realmId,
'realm': instance.realm?.toJson(), 'realm': instance.realm?.toJson(),

View File

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

View File

@ -146,7 +146,7 @@ as UniversalFileType,
/// @nodoc /// @nodoc
mixin _$SnCloudFile { 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 /// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -159,16 +159,16 @@ $SnCloudFileCopyWith<SnCloudFile> get copyWith => _$SnCloudFileCopyWithImpl<SnCl
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @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( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 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 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 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 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 String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,
@ -223,7 +222,7 @@ as DateTime?,
@JsonSerializable() @JsonSerializable()
class _SnCloudFile implements SnCloudFile { 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); factory _SnCloudFile.fromJson(Map<String, dynamic> json) => _$SnCloudFileFromJson(json);
@override final String id; @override final String id;
@ -252,7 +251,6 @@ class _SnCloudFile implements SnCloudFile {
@override final int size; @override final int size;
@override final DateTime? uploadedAt; @override final DateTime? uploadedAt;
@override final String? uploadedTo; @override final String? uploadedTo;
@override final int usedCount;
@override final DateTime createdAt; @override final DateTime createdAt;
@override final DateTime updatedAt; @override final DateTime updatedAt;
@override final DateTime? deletedAt; @override final DateTime? deletedAt;
@ -270,16 +268,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory _$SnCloudFileCopyWith(_SnCloudFile value, $Res Function(_SnCloudFile) _then) = __$SnCloudFileCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_SnCloudFile(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 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 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 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 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 String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,

View File

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

View File

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

View File

@ -370,7 +370,7 @@ $SnPublisherCopyWith<$Res> get publisher {
/// @nodoc /// @nodoc
mixin _$SnPublisher { 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 /// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -383,16 +383,16 @@ $SnPublisherCopyWith<SnPublisher> get copyWith => _$SnPublisherCopyWithImpl<SnPu
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory $SnPublisherCopyWith(SnPublisher value, $Res Function(SnPublisher) _then) = _$SnPublisherCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// @nodoc
@ -420,18 +420,17 @@ class _$SnPublisherCopyWithImpl<$Res>
/// Create a copy of SnPublisher /// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @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( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 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 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,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,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 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?,backgroundId: freezed == backgroundId ? _self.backgroundId : backgroundId // ignore: cast_nullable_to_non_nullable as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as String?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,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 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,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,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 $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(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() @JsonSerializable()
class _SnPublisher implements SnPublisher { 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); factory _SnPublisher.fromJson(Map<String, dynamic> json) => _$SnPublisherFromJson(json);
@override final String id; @override final String id;
@ -479,10 +490,9 @@ class _SnPublisher implements SnPublisher {
@override final String name; @override final String name;
@override final String nick; @override final String nick;
@override@JsonKey() final String bio; @override@JsonKey() final String bio;
@override final String? pictureId;
@override final SnCloudFile? picture; @override final SnCloudFile? picture;
@override final String? backgroundId;
@override final SnCloudFile? background; @override final SnCloudFile? background;
@override final SnAccount? account;
@override final String? accountId; @override final String? accountId;
@override final DateTime createdAt; @override final DateTime createdAt;
@override final DateTime updatedAt; @override final DateTime updatedAt;
@ -502,16 +512,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory _$SnPublisherCopyWith(_SnPublisher value, $Res Function(_SnPublisher) _then) = __$SnPublisherCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// @nodoc
@ -539,18 +549,17 @@ class __$SnPublisherCopyWithImpl<$Res>
/// Create a copy of SnPublisher /// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_SnPublisher(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 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 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,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,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 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?,backgroundId: freezed == backgroundId ? _self.backgroundId : backgroundId // ignore: cast_nullable_to_non_nullable as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as String?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,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 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,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,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 $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(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, name: json['name'] as String,
nick: json['nick'] as String, nick: json['nick'] as String,
bio: json['bio'] as String? ?? '', bio: json['bio'] as String? ?? '',
pictureId: json['picture_id'] as String?,
picture: picture:
json['picture'] == null json['picture'] == null
? null ? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
backgroundId: json['background_id'] as String?,
background: background:
json['background'] == null json['background'] == null
? null ? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>), : 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?, accountId: json['account_id'] as String?,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
@ -127,10 +129,9 @@ Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
'name': instance.name, 'name': instance.name,
'nick': instance.nick, 'nick': instance.nick,
'bio': instance.bio, 'bio': instance.bio,
'picture_id': instance.pictureId,
'picture': instance.picture?.toJson(), 'picture': instance.picture?.toJson(),
'background_id': instance.backgroundId,
'background': instance.background?.toJson(), 'background': instance.background?.toJson(),
'account': instance.account?.toJson(),
'account_id': instance.accountId, 'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),

View File

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

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$SnRealm { 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 /// Create a copy of SnRealm
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -29,16 +29,16 @@ $SnRealmCopyWith<SnRealm> get copyWith => _$SnRealmCopyWithImpl<SnRealm>(this as
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory $SnRealmCopyWith(SnRealm value, $Res Function(SnRealm) _then) = _$SnRealmCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of SnRealm
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @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( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 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 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 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,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 bool,picture: freezed == picture ? _self.picture : picture // 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?,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: null == accountId ? _self.accountId : accountId // 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 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,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
@ -119,7 +117,7 @@ $SnCloudFileCopyWith<$Res>? get background {
@JsonSerializable() @JsonSerializable()
class _SnRealm implements SnRealm { 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); factory _SnRealm.fromJson(Map<String, dynamic> json) => _$SnRealmFromJson(json);
@override final String id; @override final String id;
@ -130,9 +128,7 @@ class _SnRealm implements SnRealm {
@override final DateTime? verifiedAt; @override final DateTime? verifiedAt;
@override final bool isCommunity; @override final bool isCommunity;
@override final bool isPublic; @override final bool isPublic;
@override final String? pictureId;
@override final SnCloudFile? picture; @override final SnCloudFile? picture;
@override final String? backgroundId;
@override final SnCloudFile? background; @override final SnCloudFile? background;
@override final String accountId; @override final String accountId;
@override final DateTime createdAt; @override final DateTime createdAt;
@ -152,16 +148,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory _$SnRealmCopyWith(_SnRealm value, $Res Function(_SnRealm) _then) = __$SnRealmCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of SnRealm
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_SnRealm(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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 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 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 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,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 bool,picture: freezed == picture ? _self.picture : picture // 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?,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: null == accountId ? _self.accountId : accountId // 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 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,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), : DateTime.parse(json['verified_at'] as String),
isCommunity: json['is_community'] as bool, isCommunity: json['is_community'] as bool,
isPublic: json['is_public'] as bool, isPublic: json['is_public'] as bool,
pictureId: json['picture_id'] as String?,
picture: picture:
json['picture'] == null json['picture'] == null
? null ? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
backgroundId: json['background_id'] as String?,
background: background:
json['background'] == null json['background'] == null
? null ? null
@ -46,9 +44,7 @@ Map<String, dynamic> _$SnRealmToJson(_SnRealm instance) => <String, dynamic>{
'verified_at': instance.verifiedAt?.toIso8601String(), 'verified_at': instance.verifiedAt?.toIso8601String(),
'is_community': instance.isCommunity, 'is_community': instance.isCommunity,
'is_public': instance.isPublic, 'is_public': instance.isPublic,
'picture_id': instance.pictureId,
'picture': instance.picture?.toJson(), 'picture': instance.picture?.toJson(),
'background_id': instance.backgroundId,
'background': instance.background?.toJson(), 'background': instance.background?.toJson(),
'account_id': instance.accountId, 'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),

View File

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

View File

@ -200,7 +200,7 @@ $SnAccountProfileCopyWith<$Res> get profile {
/// @nodoc /// @nodoc
mixin _$SnAccountProfile { 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 /// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -213,16 +213,16 @@ $SnAccountProfileCopyWith<SnAccountProfile> get copyWith => _$SnAccountProfileCo
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @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( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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,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?,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?,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?,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,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 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 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 SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as String?,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 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,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
@ -302,7 +300,7 @@ $SnCloudFileCopyWith<$Res>? get background {
@JsonSerializable() @JsonSerializable()
class _SnAccountProfile implements SnAccountProfile { 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); factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
@override final String id; @override final String id;
@ -310,12 +308,10 @@ class _SnAccountProfile implements SnAccountProfile {
@override final String? middleName; @override final String? middleName;
@override final String? lastName; @override final String? lastName;
@override@JsonKey() final String bio; @override@JsonKey() final String bio;
@override final String? pictureId;
@override final int experience; @override final int experience;
@override final int level; @override final int level;
@override final double levelingProgress; @override final double levelingProgress;
@override final SnCloudFile? picture; @override final SnCloudFile? picture;
@override final String? backgroundId;
@override final SnCloudFile? background; @override final SnCloudFile? background;
@override final DateTime createdAt; @override final DateTime createdAt;
@override final DateTime updatedAt; @override final DateTime updatedAt;
@ -334,16 +330,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_SnAccountProfile(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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,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?,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?,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?,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,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 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 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 SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as String?,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 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,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,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?, middleName: json['middle_name'] as String?,
lastName: json['last_name'] as String?, lastName: json['last_name'] as String?,
bio: json['bio'] as String? ?? '', bio: json['bio'] as String? ?? '',
pictureId: json['picture_id'] as String?,
experience: (json['experience'] as num).toInt(), experience: (json['experience'] as num).toInt(),
level: (json['level'] as num).toInt(), level: (json['level'] as num).toInt(),
levelingProgress: (json['leveling_progress'] as num).toDouble(), levelingProgress: (json['leveling_progress'] as num).toDouble(),
@ -55,7 +54,6 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
json['picture'] == null json['picture'] == null
? null ? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
backgroundId: json['background_id'] as String?,
background: background:
json['background'] == null json['background'] == null
? null ? null
@ -77,12 +75,10 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
'middle_name': instance.middleName, 'middle_name': instance.middleName,
'last_name': instance.lastName, 'last_name': instance.lastName,
'bio': instance.bio, 'bio': instance.bio,
'picture_id': instance.pictureId,
'experience': instance.experience, 'experience': instance.experience,
'level': instance.level, 'level': instance.level,
'leveling_progress': instance.levelingProgress, 'leveling_progress': instance.levelingProgress,
'picture': instance.picture?.toJson(), 'picture': instance.picture?.toJson(),
'background_id': instance.backgroundId,
'background': instance.background?.toJson(), 'background': instance.background?.toJson(),
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),

View File

@ -1,5 +1,6 @@
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/screens/chat/chat.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:livekit_client/livekit_client.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';
@ -11,6 +12,14 @@ import 'package:island/pods/websocket.dart';
part 'call.g.dart'; part 'call.g.dart';
part 'call.freezed.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 @freezed
sealed class CallState with _$CallState { sealed class CallState with _$CallState {
const factory CallState({ const factory CallState({
@ -18,6 +27,7 @@ sealed class CallState with _$CallState {
required bool isMicrophoneEnabled, required bool isMicrophoneEnabled,
required bool isCameraEnabled, required bool isCameraEnabled,
required bool isScreenSharing, required bool isScreenSharing,
@Default(Duration(seconds: 0)) Duration duration,
String? error, String? error,
}) = _CallState; }) = _CallState;
} }
@ -54,6 +64,8 @@ class CallNotifier extends _$CallNotifier {
List.unmodifiable(_participants); List.unmodifiable(_participants);
LocalParticipant? get localParticipant => _localParticipant; LocalParticipant? get localParticipant => _localParticipant;
Timer? _durationTimer;
@override @override
CallState build() { CallState build() {
// Subscribe to websocket updates // Subscribe to websocket updates
@ -219,8 +231,16 @@ class CallNotifier extends _$CallNotifier {
Future<void> joinRoom(String roomId) async { Future<void> joinRoom(String roomId) async {
_roomId = roomId; _roomId = roomId;
if (_room != null) {
await _room!.disconnect();
await _room!.dispose();
_room = null;
_localParticipant = null;
_participants = [];
}
try { try {
final apiClient = ref.read(apiClientProvider); final apiClient = ref.read(apiClientProvider);
final ongoingCall = await ref.read(ongoingCallProvider(roomId).future);
final response = await apiClient.get('/chat/realtime/$roomId/join'); final response = await apiClient.get('/chat/realtime/$roomId/join');
if (response.statusCode == 200 && response.data != null) { if (response.statusCode == 200 && response.data != null) {
final data = response.data; final data = response.data;
@ -229,6 +249,19 @@ class CallNotifier extends _$CallNotifier {
final participants = joinResponse.participants; final participants = joinResponse.participants;
final String endpoint = joinResponse.endpoint; final String endpoint = joinResponse.endpoint;
final String token = joinResponse.token; 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 // Connect to LiveKit
_room = Room(); _room = Room();
@ -314,5 +347,6 @@ class CallNotifier extends _$CallNotifier {
_roomListener?.dispose(); _roomListener?.dispose();
_room?.removeListener(_onRoomChange); _room?.removeListener(_onRoomChange);
_room?.dispose(); _room?.dispose();
_durationTimer?.cancel();
} }
} }

View File

@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$CallState { 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 /// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -26,16 +26,16 @@ $CallStateCopyWith<CallState> get copyWith => _$CallStateCopyWithImpl<CallState>
@override @override
bool operator ==(Object other) { 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 @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 @override
String toString() { 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; factory $CallStateCopyWith(CallState value, $Res Function(CallState) _then) = _$CallStateCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @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( return _then(_self.copyWith(
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable 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,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,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,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?, as String?,
)); ));
} }
@ -81,13 +82,14 @@ as String?,
class _CallState implements CallState { 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 isConnected;
@override final bool isMicrophoneEnabled; @override final bool isMicrophoneEnabled;
@override final bool isCameraEnabled; @override final bool isCameraEnabled;
@override final bool isScreenSharing; @override final bool isScreenSharing;
@override@JsonKey() final Duration duration;
@override final String? error; @override final String? error;
/// Create a copy of CallState /// Create a copy of CallState
@ -100,16 +102,16 @@ _$CallStateCopyWith<_CallState> get copyWith => __$CallStateCopyWithImpl<_CallSt
@override @override
bool operator ==(Object other) { 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 @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 @override
String toString() { 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; factory _$CallStateCopyWith(_CallState value, $Res Function(_CallState) _then) = __$CallStateCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of CallState
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_CallState(
isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable 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,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,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,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?, as String?,
)); ));
} }

View File

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

View File

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

View File

@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$AppSettings { 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 /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -26,16 +26,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
@override @override
bool operator ==(Object other) { 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 @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 @override
String toString() { 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; factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @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( return _then(_self.copyWith(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable 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,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,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,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 { 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 autoTranslate;
@override final bool soundEffects; @override final bool soundEffects;
@override final bool aprilFoolFeatures; @override final bool aprilFoolFeatures;
@override final bool enterToSend; @override final bool enterToSend;
@override final bool appBarTransparent;
@override final String? customFonts;
@override final int? appColorScheme;
/// Create a copy of AppSettings /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -98,16 +104,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
@override @override
bool operator ==(Object other) { 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 @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 @override
String toString() { 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; factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_AppSettings(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable 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,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,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,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:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -68,16 +67,9 @@ final apiClientProvider = Provider<Dio>((ref) {
RequestInterceptorHandler handler, RequestInterceptorHandler handler,
) async { ) async {
try { try {
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token != null) {
ref.watch(serverUrlProvider), options.headers['Authorization'] = 'AtField $token';
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk != null) {
options.headers['Authorization'] = 'Bearer $atk';
} }
} catch (err) { } catch (err) {
// ignore // ignore
@ -95,105 +87,21 @@ final apiClientProvider = Provider<Dio>((ref) {
return dio; return dio;
}); });
final tokenPairProvider = Provider<AppTokenPair?>((ref) { final tokenProvider = Provider<AppToken?>((ref) {
final prefs = ref.watch(sharedPreferencesProvider); final prefs = ref.watch(sharedPreferencesProvider);
final tkPairString = prefs.getString(kTokenPairStoreKey); final tokenString = prefs.getString(kTokenPairStoreKey);
if (tkPairString == null) return null; if (tokenString == null) return null;
return AppTokenPair.fromJson(jsonDecode(tkPairString)); return AppToken.fromJson(jsonDecode(tokenString));
}); });
Future<(String, String)?> refreshToken(String baseUrl, String? rtk) async { // Token refresh functionality removed as per backend changes
if (rtk == null) return null;
final dio = Dio(); Future<String?> getToken(AppToken? token) async {
dio.options.baseUrl = baseUrl; return token?.token;
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);
} }
Completer<String?>? _refreshCompleter; Future<void> setToken(SharedPreferences prefs, String token) async {
final appToken = AppToken(token: token);
Future<String?> getFreshAtk( final tokenString = jsonEncode(appToken);
AppTokenPair? tkPair, prefs.setString(kTokenPairStoreKey, tokenString);
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);
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,19 @@
import 'dart:io';
import 'package:auto_route/annotations.dart'; import 'package:auto_route/annotations.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.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:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@RoutePage() @RoutePage()
class AccountSettingsScreen extends HookConsumerWidget { class AccountSettingsScreen extends HookConsumerWidget {
@ -10,9 +21,274 @@ class AccountSettingsScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { 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( return AppScaffold(
appBar: AppBar(title: Text('accountSettings').tr()), appBar: AppBar(
body: SingleChildScrollView(child: Column(children: [])), 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

@ -59,19 +59,12 @@ class UpdateProfileScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider); final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw ArgumentError('Token is null');
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: result, fileData: result,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: result.name, filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg', mimetype: result.mimeType ?? 'image/jpeg',
@ -166,9 +159,9 @@ class UpdateProfileScreen extends HookConsumerWidget {
child: Container( child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: child:
user.value!.profile.backgroundId != null user.value!.profile.background?.id != null
? CloudImageWidget( ? CloudImageWidget(
fileId: user.value!.profile.backgroundId!, fileId: user.value!.profile.background!.id,
fit: BoxFit.cover, fit: BoxFit.cover,
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
@ -182,7 +175,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
bottom: -32, bottom: -32,
child: GestureDetector( child: GestureDetector(
child: ProfilePictureWidget( child: ProfilePictureWidget(
fileId: user.value!.profile.pictureId, fileId: user.value!.profile.picture?.id,
radius: 40, radius: 40,
), ),
onTap: () { onTap: () {

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -17,6 +19,8 @@ class TabNavigationObserver extends AutoRouterObserver {
@override @override
void didPush(Route route, Route? previousRoute) { void didPush(Route route, Route? previousRoute) {
log('pushed ${previousRoute?.settings.name} -> ${route.settings.name}');
if (route is DialogRoute) return;
Future(() { Future(() {
onChange(route.settings.name); onChange(route.settings.name);
}); });
@ -24,6 +28,8 @@ class TabNavigationObserver extends AutoRouterObserver {
@override @override
void didPop(Route route, Route? previousRoute) { void didPop(Route route, Route? previousRoute) {
log('popped ${route.settings.name} -> ${previousRoute?.settings.name}');
if (route is DialogRoute) return;
Future(() { Future(() {
onChange(previousRoute?.settings.name); onChange(previousRoute?.settings.name);
}); });
@ -43,7 +49,6 @@ class TabsNavigationWidget extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final useHorizontalLayout = isWideScreen(context); final useHorizontalLayout = isWideScreen(context);
final useExpandableLayout = isWidestScreen(context);
final currentRoute = ref.watch(currentRouteProvider); final currentRoute = ref.watch(currentRouteProvider);
final notificationUnreadCount = ref.watch( final notificationUnreadCount = ref.watch(
@ -80,6 +85,7 @@ class TabsNavigationWidget extends HookConsumerWidget {
]; ];
final routeNames = [ final routeNames = [
ExploreRoute.name, ExploreRoute.name,
ExploreShellRoute.name,
ChatListRoute.name, ChatListRoute.name,
RealmListRoute.name, RealmListRoute.name,
AccountRoute.name, AccountRoute.name,
@ -110,8 +116,6 @@ class TabsNavigationWidget extends HookConsumerWidget {
Gap(MediaQuery.of(context).padding.top + 8), Gap(MediaQuery.of(context).padding.top + 8),
Expanded( Expanded(
child: NavigationRail( child: NavigationRail(
minExtendedWidth: 200,
extended: useExpandableLayout,
selectedIndex: activeIndex, selectedIndex: activeIndex,
onDestinationSelected: (index) { onDestinationSelected: (index) {
router.replace(routes[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:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/call.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/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/call_button.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:island/widgets/chat/call_participant_tile.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@ -22,8 +21,6 @@ class CallScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final ongoingCall = ref.watch(ongoingCallProvider(roomId)); final ongoingCall = ref.watch(ongoingCallProvider(roomId));
final userInfo = ref.watch(userInfoProvider);
final chatRoom = ref.watch(chatroomProvider(roomId));
final callState = ref.watch(callNotifierProvider); final callState = ref.watch(callNotifierProvider);
final callNotifier = ref.read(callNotifierProvider.notifier); final callNotifier = ref.read(callNotifierProvider.notifier);
@ -32,10 +29,6 @@ class CallScreen extends HookConsumerWidget {
return null; return null;
}, []); }, []);
final actionButtonStyle = ButtonStyle(
minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
);
final viewMode = useState<String>('grid'); final viewMode = useState<String>('grid');
return AppScaffold( return AppScaffold(
@ -74,20 +67,12 @@ class CallScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Text(
chatRoom.whenOrNull()?.name ?? 'loading'.tr(), ongoingCall.value?.room.name ?? 'call'.tr(),
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
), ),
Text( Text(
callState.isConnected callState.isConnected
? Duration( ? formatDuration(callState.duration)
milliseconds:
(DateTime.now().millisecondsSinceEpoch -
(ongoingCall
.value
?.createdAt
.millisecondsSinceEpoch ??
0)),
).toString()
: 'Connecting', : 'Connecting',
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
), ),
@ -131,78 +116,6 @@ class CallScreen extends HookConsumerWidget {
) )
: Column( : Column(
children: [ 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( Expanded(
child: Builder( child: Builder(
builder: (context) { builder: (context) {
@ -253,7 +166,8 @@ class CallScreen extends HookConsumerWidget {
.profile .profile
?.account ?.account
.profile .profile
.pictureId, .picture
?.id,
size: 72, 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/chat.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/call.dart';
import 'package:island/pods/chat_summary.dart'; import 'package:island/pods/chat_summary.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.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/account/account_picker.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.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/content/cloud_files.dart';
import 'package:island/widgets/realms/selection_dropdown.dart'; import 'package:island/widgets/realms/selection_dropdown.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
@ -107,7 +109,7 @@ class ChatRoomListTile extends HookConsumerWidget {
}, },
loading: () => const SizedBox.shrink(), loading: () => const SizedBox.shrink(),
error: error:
(_, __) => (_, _) =>
isDirect && room.description == null isDirect && room.description == null
? Text( ? Text(
room.members!.map((e) => '@${e.account.name}').join(', '), room.members!.map((e) => '@${e.account.name}').join(', '),
@ -125,19 +127,19 @@ class ChatRoomListTile extends HookConsumerWidget {
isLabelVisible: summary.when( isLabelVisible: summary.when(
data: (data) => (data?.unreadCount ?? 0) > 0, data: (data) => (data?.unreadCount ?? 0) > 0,
loading: () => false, loading: () => false,
error: (_, __) => false, error: (_, _) => false,
), ),
child: child:
(isDirect && room.pictureId == null) (isDirect && room.picture?.id == null)
? SplitAvatarWidget( ? SplitAvatarWidget(
filesId: filesId:
room.members! room.members!
.map((e) => e.account.profile.pictureId) .map((e) => e.account.profile.picture?.id)
.toList(), .toList(),
) )
: room.pictureId == null : room.picture?.id == null
? CircleAvatar(child: Text(room.name![0].toUpperCase())) ? CircleAvatar(child: Text(room.name![0].toUpperCase()))
: ProfilePictureWidget(fileId: room.pictureId), : ProfilePictureWidget(fileId: room.picture?.id),
), ),
title: Text( title: Text(
(isDirect && room.name == null) (isDirect && room.name == null)
@ -145,14 +147,14 @@ class ChatRoomListTile extends HookConsumerWidget {
: room.name ?? '', : room.name ?? '',
), ),
subtitle: buildSubtitle(), subtitle: buildSubtitle(),
trailing: trailing, // Add this line
onTap: () async { onTap: () async {
// Clear unread count if there are unread messages // Clear unread count if there are unread messages
final summary = await ref.read(chatSummaryProvider.future); ref.read(chatSummaryProvider.future).then((summary) {
if ((summary[room.id]?.unreadCount ?? 0) > 0) { if ((summary[room.id]?.unreadCount ?? 0) > 0) {
await ref ref.read(chatSummaryProvider.notifier).clearUnreadCount(room.id);
.read(chatSummaryProvider.notifier) }
.clearUnreadCount(room.id); });
}
onTap?.call(); onTap?.call();
}, },
); );
@ -213,6 +215,8 @@ class ChatListScreen extends HookConsumerWidget {
0, 0,
); // 0 for All, 1 for Direct Messages, 2 for Group Chats ); // 0 for All, 1 for Direct Messages, 2 for Group Chats
final callState = ref.watch(callNotifierProvider);
useEffect(() { useEffect(() {
tabController.addListener(() { tabController.addListener(() {
selectedTab.value = tabController.index; selectedTab.value = tabController.index;
@ -276,13 +280,13 @@ class ChatListScreen extends HookConsumerWidget {
label: Text( label: Text(
chatInvites.when( chatInvites.when(
data: (invites) => invites.length.toString(), data: (invites) => invites.length.toString(),
error: (_, __) => '0', error: (_, _) => '0',
loading: () => '0', loading: () => '0',
), ),
), ),
isLabelVisible: chatInvites.when( isLabelVisible: chatInvites.when(
data: (invites) => invites.isNotEmpty, data: (invites) => invites.isNotEmpty,
error: (_, __) => false, error: (_, _) => false,
loading: () => false, loading: () => false,
), ),
child: const Icon(Symbols.email), child: const Icon(Symbols.email),
@ -334,76 +338,93 @@ class ChatListScreen extends HookConsumerWidget {
}, },
child: const Icon(Symbols.add), child: const Icon(Symbols.add),
), ),
body: Column( body: Stack(
children: [ children: [
Consumer( Column(
builder: (context, ref, _) { children: [
final summaryState = ref.watch(chatSummaryProvider); Consumer(
return summaryState.maybeWhen( builder: (context, ref, _) {
loading: () => const LinearProgressIndicator(), final summaryState = ref.watch(chatSummaryProvider);
orElse: () => const SizedBox.shrink(), return summaryState.maybeWhen(
); loading: () => const LinearProgressIndicator(),
}, orElse: () => const SizedBox.shrink(),
), );
Expanded( },
child: chats.when( ),
data: Expanded(
(items) => RefreshIndicator( child: chats.when(
onRefresh: data:
() => Future.sync(() { (items) => RefreshIndicator(
ref.invalidate(chatroomsJoinedProvider); onRefresh:
}), () => Future.sync(() {
child: ListView.builder( ref.invalidate(chatroomsJoinedProvider);
padding: EdgeInsets.zero, }),
itemCount: child: ListView.builder(
items padding:
.where( callState.isConnected
(item) => ? EdgeInsets.only(bottom: 96)
selectedTab.value == 0 || : EdgeInsets.zero,
(selectedTab.value == 1 && itemCount:
item.type == 1) || items
(selectedTab.value == 2 && item.type != 1), .where(
) (item) =>
.length, selectedTab.value == 0 ||
itemBuilder: (context, index) { (selectedTab.value == 1 &&
final filteredItems = item.type == 1) ||
items (selectedTab.value == 2 &&
.where( item.type != 1),
(item) => )
selectedTab.value == 0 || .length,
(selectedTab.value == 1 && itemBuilder: (context, index) {
item.type == 1) || final filteredItems =
(selectedTab.value == 2 && items
item.type != 1), .where(
) (item) =>
.toList(); selectedTab.value == 0 ||
final item = filteredItems[index]; (selectedTab.value == 1 &&
return ChatRoomListTile( item.type == 1) ||
room: item, (selectedTab.value == 2 &&
isDirect: item.type == 1, item.type != 1),
onTap: () { )
if (context.router.topRoute.name == .toList();
ChatRoomRoute.name) { final item = filteredItems[index];
context.router.replace( return ChatRoomListTile(
ChatRoomRoute(id: item.id), room: item,
); isDirect: item.type == 1,
} else { onTap: () {
context.router.push(ChatRoomRoute(id: item.id)); 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()),
loading: () => const Center(child: CircularProgressIndicator()), error:
error: (error, stack) => ResponseErrorWidget(
(error, stack) => ResponseErrorWidget( error: error,
error: error, onRetry: () {
onRetry: () { ref.invalidate(chatroomsJoinedProvider);
ref.invalidate(chatroomsJoinedProvider); },
}, ),
), ),
), ),
],
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: const CallOverlayBar().padding(horizontal: 16, vertical: 12),
), ),
], ],
), ),
@ -502,19 +523,12 @@ class EditChatScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider); final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw ArgumentError('Token is null');
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: result, fileData: result,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: result.name, filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg', mimetype: result.mimeType ?? 'image/jpeg',
@ -575,7 +589,7 @@ class EditChatScreen extends HookConsumerWidget {
realms: joinedRealms.when( realms: joinedRealms.when(
data: (realms) => realms, data: (realms) => realms,
loading: () => [], loading: () => [],
error: (_, __) => [], error: (_, _) => [],
), ),
onChanged: (SnRealm? value) { onChanged: (SnRealm? value) {
currentRealm.value = value; currentRealm.value = value;

View File

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -22,16 +21,17 @@ import 'package:island/screens/posts/compose.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.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/chat/message_item.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:super_sliver_list/super_sliver_list.dart'; import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:super_clipboard/super_clipboard.dart';
import 'chat.dart'; import 'chat.dart';
import 'package:island/widgets/chat/call_button.dart'; import 'package:island/widgets/chat/call_button.dart';
@ -116,19 +116,12 @@ class MessagesNotifier extends _$MessagesNotifier {
messageRepositoryProvider(_roomId).future, messageRepositoryProvider(_roomId).future,
); );
final baseUrl = ref.read(serverUrlProvider); final baseUrl = ref.read(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw ArgumentError('Access token is null');
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final currentMessages = state.value ?? []; final currentMessages = state.value ?? [];
await repository.sendMessage( await repository.sendMessage(
atk, token,
baseUrl, baseUrl,
_roomId, _roomId,
content, content,
@ -352,6 +345,10 @@ class ChatRoomScreen extends HookConsumerWidget {
if (message.chatRoomId != chatRoom.value?.id) return; if (message.chatRoomId != chatRoom.value?.id) return;
switch (pkt.type) { switch (pkt.type) {
case 'messages.new': case 'messages.new':
if (message.type.startsWith('call')) {
// Handle the ongoing call.
ref.invalidate(ongoingCallProvider(message.chatRoomId));
}
messagesNotifier.receiveMessage(message); messagesNotifier.receiveMessage(message);
// Send read receipt for new message // Send read receipt for new message
sendReadReceipt(); sendReadReceipt();
@ -436,19 +433,23 @@ class ChatRoomScreen extends HookConsumerWidget {
height: 26, height: 26,
width: 26, width: 26,
child: child:
(room!.type == 1 && room.pictureId == null) (room!.type == 1 && room.picture?.id == null)
? SplitAvatarWidget( ? SplitAvatarWidget(
filesId: filesId:
room.members! room.members!
.map( .map(
(e) => (e) =>
e.account.profile.pictureId, e
.account
.profile
.picture
?.id,
) )
.toList(), .toList(),
) )
: room.pictureId != null : room.picture?.id != null
? ProfilePictureWidget( ? ProfilePictureWidget(
fileId: room.pictureId, fileId: room.picture?.id,
fallbackIcon: Symbols.chat, fallbackIcon: Symbols.chat,
) )
: CircleAvatar( : CircleAvatar(
@ -476,19 +477,23 @@ class ChatRoomScreen extends HookConsumerWidget {
height: 26, height: 26,
width: 26, width: 26,
child: child:
(room!.type == 1 && room.pictureId == null) (room!.type == 1 && room.picture?.id == null)
? SplitAvatarWidget( ? SplitAvatarWidget(
filesId: filesId:
room.members! room.members!
.map( .map(
(e) => (e) =>
e.account.profile.pictureId, e
.account
.profile
.picture
?.id,
) )
.toList(), .toList(),
) )
: room.pictureId != null : room.picture?.id != null
? ProfilePictureWidget( ? ProfilePictureWidget(
fileId: room.pictureId, fileId: room.picture?.id,
fallbackIcon: Symbols.chat, fallbackIcon: Symbols.chat,
) )
: CircleAvatar( : CircleAvatar(
@ -525,152 +530,166 @@ class ChatRoomScreen extends HookConsumerWidget {
const Gap(8), const Gap(8),
], ],
), ),
body: Column( body: Stack(
children: [ children: [
Expanded( Column(
child: messages.when( children: [
data: Expanded(
(messageList) => child: messages.when(
messageList.isEmpty data:
? Center(child: Text('No messages yet'.tr())) (messageList) =>
: SuperListView.builder( messageList.isEmpty
padding: EdgeInsets.symmetric(vertical: 16), ? Center(child: Text('No messages yet'.tr()))
controller: scrollController, : SuperListView.builder(
reverse: true, // Show newest messages at the bottom padding: EdgeInsets.symmetric(vertical: 16),
itemCount: messageList.length, controller: scrollController,
itemBuilder: (context, index) { reverse:
final message = messageList[index]; true, // Show newest messages at the bottom
final nextMessage = itemCount: messageList.length,
index < messageList.length - 1 itemBuilder: (context, index) {
? messageList[index + 1] final message = messageList[index];
: null; final nextMessage =
final isLastInGroup = index < messageList.length - 1
nextMessage == null || ? messageList[index + 1]
nextMessage.senderId != message.senderId || : null;
nextMessage.createdAt final isLastInGroup =
.difference(message.createdAt) nextMessage == null ||
.inMinutes nextMessage.senderId !=
.abs() > message.senderId ||
3; nextMessage.createdAt
.difference(message.createdAt)
.inMinutes
.abs() >
3;
return chatIdentity.when( return chatIdentity.when(
skipError: true, skipError: true,
data: data:
(identity) => MessageItem( (identity) => MessageItem(
message: message, message: message,
isCurrentUser: isCurrentUser:
identity?.id == message.senderId, identity?.id == message.senderId,
onAction: (action) { onAction: (action) {
switch (action) { switch (action) {
case MessageItemAction.delete: case MessageItemAction.delete:
messagesNotifier.deleteMessage( messagesNotifier.deleteMessage(
message.id, message.id,
); );
case MessageItemAction.edit: case MessageItemAction.edit:
messageEditingTo.value = messageEditingTo.value =
message.toRemoteMessage(); message.toRemoteMessage();
messageController.text = messageController.text =
messageEditingTo messageEditingTo
.value .value
?.content ?? ?.content ??
''; '';
attachments.value = attachments.value =
messageEditingTo messageEditingTo
.value! .value!
.attachments .attachments
.map( .map(
(e) => (e) =>
UniversalFile.fromAttachment( UniversalFile.fromAttachment(
e, e,
), ),
) )
.toList(); .toList();
case MessageItemAction.forward: case MessageItemAction.forward:
messageForwardingTo.value = messageForwardingTo.value =
message.toRemoteMessage(); message.toRemoteMessage();
case MessageItemAction.reply: case MessageItemAction.reply:
messageReplyingTo.value = messageReplyingTo.value =
message.toRemoteMessage(); message.toRemoteMessage();
} }
}, },
progress: progress:
attachmentProgress.value[message.id], attachmentProgress.value[message
showAvatar: isLastInGroup, .id],
), showAvatar: isLastInGroup,
loading: ),
() => MessageItem( loading:
message: message, () => MessageItem(
isCurrentUser: false, message: message,
onAction: null, isCurrentUser: false,
progress: null, onAction: null,
showAvatar: false, progress: null,
), showAvatar: false,
error: (_, __) => const SizedBox.shrink(), ),
); error: (_, __) => const SizedBox.shrink(),
}, );
), },
loading: () => const Center(child: CircularProgressIndicator()), ),
error: loading:
(error, _) => ResponseErrorWidget( () => const Center(child: CircularProgressIndicator()),
error: error, error:
onRetry: () => messagesNotifier.loadInitial(), (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;
},
), ),
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; return;
} }
final enterToSend = ref.read(appSettingsProvider).enterToSend; final enterToSend = ref.read(appSettingsNotifierProvider).enterToSend;
final isEnter = event.logicalKey == LogicalKeyboardKey.enter; final isEnter = event.logicalKey == LogicalKeyboardKey.enter;
if (isEnter) { if (isEnter) {
@ -733,38 +752,21 @@ class _ChatInput extends ConsumerWidget {
} }
Future<void> _handlePaste() async { Future<void> _handlePaste() async {
final clipboard = SystemClipboard.instance; final clipboard = await Pasteboard.image;
if (clipboard == null) return; if (clipboard == null) return;
final reader = await clipboard.read(); onAttachmentsChanged([
if (reader.canProvide(Formats.png)) { ...attachments,
reader.getFile(Formats.png, (file) async { UniversalFile(
final stream = file.getStream(); data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
final bytes = await stream.toList(); type: UniversalFileType.image,
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,
),
]);
});
}
} }
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final enterToSend = ref.watch(appSettingsProvider).enterToSend; final enterToSend = ref.watch(appSettingsNotifierProvider).enterToSend;
return Material( return Material(
elevation: 8, elevation: 8,
@ -870,6 +872,7 @@ class _ChatInput extends ConsumerWidget {
onKey: (event) => _handleKeyPress(context, ref, event), onKey: (event) => _handleKeyPress(context, ref, event),
child: TextField( child: TextField(
controller: messageController, controller: messageController,
onSubmitted: enterToSend ? (_) => onSend() : null,
inputFormatters: [ inputFormatters: [
if (enterToSend) if (enterToSend)
TextInputFormatter.withFunction((oldValue, newValue) { TextInputFormatter.withFunction((oldValue, newValue) {

View File

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

View File

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

View File

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

View File

@ -95,19 +95,12 @@ class EditPublisherScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider); final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw ArgumentError('Token is null');
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: result, fileData: result,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: result.name, filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg', mimetype: result.mimeType ?? 'image/jpeg',
@ -145,8 +138,8 @@ class EditPublisherScreen extends HookConsumerWidget {
useEffect(() { useEffect(() {
if (publisher.value != null) { if (publisher.value != null) {
picture.value = publisher.value!.pictureId; picture.value = publisher.value!.picture?.id;
background.value = publisher.value!.backgroundId; background.value = publisher.value!.background?.id;
nameController.text = publisher.value!.name; nameController.text = publisher.value!.name;
nickController.text = publisher.value!.nick; nickController.text = publisher.value!.nick;
bioController.text = publisher.value!.bio; bioController.text = publisher.value!.bio;
@ -200,7 +193,7 @@ class EditPublisherScreen extends HookConsumerWidget {
realms: joinedRealms.when( realms: joinedRealms.when(
data: (realms) => realms, data: (realms) => realms,
loading: () => [], loading: () => [],
error: (_, __) => [], error: (_, _) => [],
), ),
onChanged: (SnRealm? value) { onChanged: (SnRealm? value) {
currentRealm.value = value; currentRealm.value = value;
@ -286,14 +279,14 @@ class EditPublisherScreen extends HookConsumerWidget {
nameController.text = user.value!.name; nameController.text = user.value!.name;
nickController.text = user.value!.nick; nickController.text = user.value!.nick;
bioController.text = user.value!.profile.bio; bioController.text = user.value!.profile.bio;
picture.value = user.value!.profile.pictureId; picture.value = user.value!.profile.picture?.id;
background.value = user.value!.profile.backgroundId; background.value = user.value!.profile.background?.id;
} else { } else {
nameController.text = currentRealm.value!.slug; nameController.text = currentRealm.value!.slug;
nickController.text = currentRealm.value!.name; nickController.text = currentRealm.value!.name;
bioController.text = currentRealm.value!.description; bioController.text = currentRealm.value!.description;
picture.value = currentRealm.value!.pictureId; picture.value = currentRealm.value!.picture?.id;
background.value = currentRealm.value!.backgroundId; background.value = currentRealm.value!.background?.id;
} }
}, },
label: label:

View File

@ -17,6 +17,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:styled_widget/styled_widget.dart';
part 'explore.g.dart'; part 'explore.g.dart';
@ -131,10 +132,16 @@ class _ActivityListView extends HookConsumerWidget {
switch (item.type) { switch (item.type) {
case 'posts.new': case 'posts.new':
case 'posts.new.replies':
final isReply = item.type == 'posts.new.replies';
itemWidget = PostItem( itemWidget = PostItem(
backgroundColor: backgroundColor:
isWideScreen(context) ? Colors.transparent : null, isWideScreen(context) ? Colors.transparent : null,
item: SnPost.fromJson(item.data), item: SnPost.fromJson(item.data),
padding:
isReply
? EdgeInsets.only(left: 16, right: 16, bottom: 16)
: null,
onRefresh: (_) { onRefresh: (_) {
activitiesNotifier.forceRefresh(); 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; break;
case 'accounts.check-in': case 'accounts.check-in':
itemWidget = CheckInActivityWidget(item: item); itemWidget = CheckInActivityWidget(item: item);

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:auto_route/auto_route.dart'; 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:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/user.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/markdown.dart'; import 'package:island/widgets/content/markdown.dart';
@ -21,8 +23,18 @@ part 'notification.g.dart';
@riverpod @riverpod
class NotificationUnreadCountNotifier class NotificationUnreadCountNotifier
extends _$NotificationUnreadCountNotifier { extends _$NotificationUnreadCountNotifier {
StreamSubscription<WebSocketPacket>? _subscription;
@override @override
Future<int> build() async { 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 { try {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
final response = await client.get('/notifications/count'); 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 { Future<void> decrement(int count) async {
final current = await future; 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, notifierRefreshable: notificationListNotifierProvider.notifier,
contentBuilder: contentBuilder:
(data, widgetCount, endItemView) => ListView.builder( (data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount, itemCount: widgetCount,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == widgetCount - 1) { if (index == widgetCount - 1) {
@ -142,7 +169,7 @@ class NotificationScreen extends HookConsumerWidget {
], ],
), ),
trailing: trailing:
notification.viewedAt == null notification.viewedAt != null
? null ? null
: Container( : Container(
width: 12, width: 12,

View File

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

View File

@ -1,5 +1,4 @@
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -18,13 +17,14 @@ import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart'; import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/posts/detail.dart'; import 'package:island/screens/posts/detail.dart';
import 'package:island/services/file.dart'; import 'package:island/services/file.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/publishers_modal.dart'; import 'package:island/widgets/post/publishers_modal.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:super_clipboard/super_clipboard.dart';
@RoutePage() @RoutePage()
class PostEditScreen extends HookConsumerWidget { class PostEditScreen extends HookConsumerWidget {
@ -125,21 +125,14 @@ class PostComposeScreen extends HookConsumerWidget {
final attachment = attachments.value[index]; final attachment = attachments.value[index];
if (attachment is SnCloudFile) return; if (attachment is SnCloudFile) return;
final baseUrl = ref.watch(serverUrlProvider); final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw ArgumentError('Token is null');
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
try { try {
attachmentProgress.value = {...attachmentProgress.value, index: 0}; attachmentProgress.value = {...attachmentProgress.value, index: 0};
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: attachment.data, fileData: attachment.data,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: attachment.data.name ?? 'Post media', filename: attachment.data.name ?? 'Post media',
mimetype: mimetype:
@ -218,33 +211,16 @@ class PostComposeScreen extends HookConsumerWidget {
} }
Future<void> _handlePaste() async { Future<void> _handlePaste() async {
final clipboard = SystemClipboard.instance; final clipboard = await Pasteboard.image;
if (clipboard == null) return; if (clipboard == null) return;
final reader = await clipboard.read(); attachments.value = [
if (reader.canProvide(Formats.png)) { ...attachments.value,
reader.getFile(Formats.png, (file) async { UniversalFile(
final stream = file.getStream(); data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
final bytes = await stream.toList(); type: UniversalFileType.image,
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,
),
];
});
}
} }
void _handleKeyPress(RawKeyEvent event) { void _handleKeyPress(RawKeyEvent event) {
@ -261,7 +237,43 @@ class PostComposeScreen extends HookConsumerWidget {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title:
isWideScreen(context)
? Text(originalPost != null ? 'editPost'.tr() : 'newPost'.tr())
: null,
actions: [ 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( IconButton(
onPressed: submitting.value ? null : performAction, onPressed: submitting.value ? null : performAction,
icon: icon:
@ -291,7 +303,7 @@ class PostComposeScreen extends HookConsumerWidget {
children: [ children: [
GestureDetector( GestureDetector(
child: ProfilePictureWidget( child: ProfilePictureWidget(
fileId: currentPublisher.value?.pictureId, fileId: currentPublisher.value?.picture?.id,
radius: 20, radius: 20,
fallbackIcon: fallbackIcon:
currentPublisher.value == null currentPublisher.value == null
@ -316,7 +328,7 @@ class PostComposeScreen extends HookConsumerWidget {
TextField( TextField(
controller: titleController, controller: titleController,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
hintText: 'Title', hintText: 'postTitle'.tr(),
), ),
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
onTapOutside: onTapOutside:
@ -326,7 +338,7 @@ class PostComposeScreen extends HookConsumerWidget {
TextField( TextField(
controller: descriptionController, controller: descriptionController,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
hintText: 'Description', hintText: 'postDescription'.tr(),
), ),
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
onTapOutside: onTapOutside:
@ -345,6 +357,7 @@ class PostComposeScreen extends HookConsumerWidget {
hintText: 'postPlaceholder'.tr(), hintText: 'postPlaceholder'.tr(),
isDense: true, isDense: true,
), ),
maxLines: null,
onTapOutside: onTapOutside:
(_) => (_) =>
FocusManager.instance.primaryFocus FocusManager.instance.primaryFocus
@ -352,34 +365,81 @@ class PostComposeScreen extends HookConsumerWidget {
), ),
), ),
const Gap(8), const Gap(8),
Column( LayoutBuilder(
crossAxisAlignment: CrossAxisAlignment.start, builder: (context, constraints) {
spacing: 8, final isWide = isWideScreen(context);
children: [ return isWide
for ( ? Wrap(
var idx = 0; spacing: 8,
idx < attachments.value.length; runSpacing: 8,
idx++ children: [
) for (
AttachmentPreview( var idx = 0;
item: attachments.value[idx], idx < attachments.value.length;
progress: attachmentProgress.value[idx], idx++
onRequestUpload: () => uploadAttachment(idx), )
onDelete: () => deleteAttachment(idx), SizedBox(
onMove: (delta) { width: constraints.maxWidth / 2 - 4,
if (idx + delta < 0 || child: AttachmentPreview(
idx + delta >= attachments.value.length) { item: attachments.value[idx],
return; progress:
} attachmentProgress.value[idx],
final clone = List.of(attachments.value); onRequestUpload:
clone.insert( () => uploadAttachment(idx),
idx + delta, onDelete: () => deleteAttachment(idx),
clone.removeAt(idx), onMove: (delta) {
); if (idx + delta < 0 ||
attachments.value = clone; 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;
},
),
],
);
},
), ),
], ],
), ),

View File

@ -1,4 +1,5 @@
import 'package:auto_route/annotations.dart'; import 'package:auto_route/annotations.dart';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -29,9 +30,9 @@ Future<SnPublisher> publisher(Ref ref, String uname) async {
@riverpod @riverpod
Future<List<SnAccountBadge>> publisherBadges(Ref ref, String pubName) async { Future<List<SnAccountBadge>> publisherBadges(Ref ref, String pubName) async {
final pub = await ref.watch(publisherProvider(pubName).future); 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 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( return List<SnAccountBadge>.from(
resp.data.map((x) => SnAccountBadge.fromJson(x)), resp.data.map((x) => SnAccountBadge.fromJson(x)),
); );
@ -108,8 +109,8 @@ class PublisherProfileScreen extends HookConsumerWidget {
leading: PageBackButton(shadows: [iconShadow]), leading: PageBackButton(shadows: [iconShadow]),
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: background:
data.backgroundId != null data.background?.id != null
? CloudImageWidget(fileId: data.backgroundId!) ? CloudImageWidget(fileId: data.background!.id)
: Container( : Container(
color: color:
Theme.of(context).appBarTheme.backgroundColor, Theme.of(context).appBarTheme.backgroundColor,
@ -139,7 +140,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
shadows: [iconShadow], shadows: [iconShadow],
), ),
), ),
error: (_, __) => const SizedBox(), error: (_, _) => const SizedBox(),
loading: loading:
() => const SizedBox( () => const SizedBox(
width: 48, width: 48,
@ -163,7 +164,10 @@ class PublisherProfileScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20, spacing: 20,
children: [ children: [
ProfilePictureWidget(fileId: data.pictureId!, radius: 32), ProfilePictureWidget(
fileId: data.picture!.id,
radius: 32,
),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@ -177,9 +181,29 @@ class PublisherProfileScreen extends HookConsumerWidget {
).fontSize(14).opacity(0.85), ).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( AccountStatusWidget(
uname: name, uname: data.account!.name,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
), ),
], ],

View File

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

View File

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

View File

@ -49,13 +49,13 @@ class RealmListScreen extends HookConsumerWidget {
label: Text( label: Text(
realmInvites.when( realmInvites.when(
data: (invites) => invites.length.toString(), data: (invites) => invites.length.toString(),
error: (_, __) => '0', error: (_, _) => '0',
loading: () => '0', loading: () => '0',
), ),
), ),
isLabelVisible: realmInvites.when( isLabelVisible: realmInvites.when(
data: (invites) => invites.isNotEmpty, data: (invites) => invites.isNotEmpty,
error: (_, __) => false, error: (_, _) => false,
loading: () => false, loading: () => false,
), ),
child: const Icon(Symbols.email), child: const Icon(Symbols.email),
@ -97,7 +97,7 @@ class RealmListScreen extends HookConsumerWidget {
return ListTile( return ListTile(
isThreeLine: true, isThreeLine: true,
leading: ProfilePictureWidget( leading: ProfilePictureWidget(
fileId: value[item].pictureId, fileId: value[item].picture?.id,
fallbackIcon: Symbols.group, fallbackIcon: Symbols.group,
), ),
title: Text(value[item].name), title: Text(value[item].name),
@ -211,19 +211,12 @@ class EditRealmScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final baseUrl = ref.watch(serverUrlProvider); final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw ArgumentError('Access token is null');
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: result, fileData: result,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: result.name, filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg', mimetype: result.mimeType ?? 'image/jpeg',
@ -455,7 +448,7 @@ class _RealmInviteSheet extends HookConsumerWidget {
final invite = items[index]; final invite = items[index];
return ListTile( return ListTile(
leading: ProfilePictureWidget( leading: ProfilePictureWidget(
fileId: invite.realm!.pictureId, fileId: invite.realm!.picture?.id,
fallbackIcon: Symbols.group, fallbackIcon: Symbols.group,
), ),
title: Text(invite.realm!.name), 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:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.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:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@ -26,7 +29,10 @@ class SettingsScreen extends HookConsumerWidget {
final serverUrl = ref.watch(serverUrlProvider); final serverUrl = ref.watch(serverUrlProvider);
final prefs = ref.watch(sharedPreferencesProvider); final prefs = ref.watch(sharedPreferencesProvider);
final controller = TextEditingController(text: serverUrl); 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); final docBasepath = useState<String?>(null);
@ -37,200 +43,565 @@ class SettingsScreen extends HookConsumerWidget {
return null; return null;
}, []); }, []);
return AppScaffold( // Group settings into categories for better organization
noBackground: false, final appearanceSettings = [
appBar: AppBar(title: const Text('Settings')), // Language settings
body: SingleChildScrollView( ListTile(
child: Column( minLeadingWidth: 48,
crossAxisAlignment: CrossAxisAlignment.start, title: Text('settingsDisplayLanguage').tr(),
children: [ contentPadding: const EdgeInsets.only(left: 24, right: 17),
ListTile( leading: const Icon(Symbols.translate),
minLeadingWidth: 48, trailing: DropdownButtonHideUnderline(
title: Text('settingsDisplayLanguage').tr(), child: DropdownButton2<Locale?>(
contentPadding: const EdgeInsets.only(left: 24, right: 17), isExpanded: true,
leading: const Icon(Symbols.translate), items: [
trailing: DropdownButtonHideUnderline( ...EasyLocalization.of(context)!.supportedLocales.mapIndexed((
child: DropdownButton2<Locale?>( idx,
isExpanded: true, ele,
items: [ ) {
...EasyLocalization.of( return DropdownMenuItem<Locale?>(
context, value: ele,
)!.supportedLocales.mapIndexed((idx, ele) { child: Text(
return DropdownMenuItem<Locale?>( '${ele.languageCode}-${ele.countryCode}',
value: ele, ).fontSize(14),
child: Text( );
'${ele.languageCode}-${ele.countryCode}', }),
).fontSize(14), DropdownMenuItem<Locale?>(
); value: null,
}), child: Text('languageFollowSystem').tr().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),
),
), ),
],
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( menuItemStyleData: const MenuItemStyleData(height: 40),
isThreeLine: true, ),
minLeadingWidth: 48, ),
title: Text('settingsServerUrl').tr(), ),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.link), // Custom fonts settings
subtitle: Padding( ListTile(
padding: const EdgeInsets.only(top: 6), isThreeLine: true,
child: TextField( minLeadingWidth: 48,
controller: controller, title: Text('settingsCustomFonts').tr(),
decoration: InputDecoration( contentPadding: const EdgeInsets.only(left: 24, right: 17),
hintText: kNetworkServerDefault, leading: const Icon(Symbols.font_download),
suffixIcon: IconButton( subtitle: Padding(
icon: const Icon(Symbols.restart_alt), padding: const EdgeInsets.only(top: 6),
onPressed: () { child: TextField(
controller.text = kNetworkServerDefault; controller: TextEditingController(text: settings.customFonts),
prefs.setString( decoration: InputDecoration(
kNetworkServerStoreKey, hintText: 'Nunito, Arial, sans-serif',
kNetworkServerDefault, helperText: 'settingsCustomFontsHelper'.tr(),
); suffixIcon: IconButton(
ref.invalidate(serverUrlProvider); icon: const Icon(Symbols.restart_alt),
showSnackBar(context, 'settingsApplied'.tr()); 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) { actions: [
if (value.isNotEmpty) { TextButton(
prefs.setString(kNetworkServerStoreKey, value); onPressed: () => Navigator.of(context).pop(),
ref.invalidate(serverUrlProvider); child: Text('Cancel').tr(),
showSnackBar(context, 'settingsApplied'.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( ListTile(
minLeadingWidth: 48, minLeadingWidth: 48,
title: Text('settingsBackgroundImage').tr(), title: Text('settingsKeyboardShortcuts').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17), 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), 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( // Create a responsive layout based on screen width
image.path, Widget buildSettingsList() {
).copy('${docBasepath.value}/$kAppBackgroundImagePath'); if (isWide) {
prefs.setBool(kAppBackgroundStoreKey, true); // Two-column layout for wide screens
ref.invalidate(backgroundImageFileProvider); return Row(
if (context.mounted) { crossAxisAlignment: CrossAxisAlignment.start,
showSnackBar(context, 'settingsApplied'.tr()); children: [
} Expanded(
}, child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
if (!kIsWeb && docBasepath.value != null) children: [
FutureBuilder<bool>( _SettingsSection(
future: title: 'Appearance',
File('${docBasepath.value}/app_background_image').exists(), children: appearanceSettings,
builder: (context, snapshot) { ),
if (!snapshot.hasData || !snapshot.data!) { _SettingsSection(title: 'Server', children: serverSettings),
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);
},
), ),
), ),
ListTile( Expanded(
minLeadingWidth: 48, child: Column(
title: Text('settingsSoundEffects').tr(), crossAxisAlignment: CrossAxisAlignment.start,
contentPadding: const EdgeInsets.only(left: 24, right: 17), children: [
leading: const Icon(Symbols.volume_up), _SettingsSection(
trailing: Switch( title: 'Behavior',
value: settings.soundEffects, children: behaviorSettings,
onChanged: (value) { ),
ref.read(appSettingsProvider.notifier).setSoundEffects(value); if (desktopSettings.isNotEmpty)
}, _SettingsSection(
), title: 'Desktop',
), children: desktopSettings,
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);
},
), ),
), ),
], ],
).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

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

View File

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

View File

@ -13,8 +13,6 @@ import 'package:island/services/responsive.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:styled_widget/styled_widget.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 { class WindowScaffold extends HookConsumerWidget {
final Widget child; final Widget child;
@ -152,22 +150,8 @@ class AppScaffold extends StatelessWidget {
noBackground noBackground
? Colors.transparent ? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor, : Theme.of(context).scaffoldBackgroundColor,
body: Stack( body:
children: [ noBackground ? content : AppBackground(isRoot: true, child: content),
SizedBox.expand(
child:
noBackground
? content
: AppBackground(isRoot: true, child: content),
),
Positioned(
left: 16,
right: 16,
bottom: 8,
child: const _GlobalCallOverlay(),
),
],
),
appBar: appBar, appBar: appBar,
bottomNavigationBar: bottomNavigationBar, bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet, bottomSheet: bottomSheet,
@ -206,23 +190,6 @@ class PageBackButton extends StatelessWidget {
const kAppBackgroundImagePath = 'island_app_background'; 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 { final backgroundImageFileProvider = FutureProvider<File?>((ref) async {
if (kIsWeb) return null; if (kIsWeb) return null;
final dir = await getApplicationSupportDirectory(); final dir = await getApplicationSupportDirectory();
@ -268,7 +235,7 @@ class AppBackground extends ConsumerWidget {
}, },
loading: () => const SizedBox(), loading: () => const SizedBox(),
error: error:
(_, __) => Material( (_, _) => Material(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: child, child: child,
), ),

View File

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

View File

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

View File

@ -1,10 +1,94 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/call.dart'; import 'package:island/pods/call.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.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 { class CallOverlayBar extends HookConsumerWidget {
const CallOverlayBar({super.key}); const CallOverlayBar({super.key});
@ -15,48 +99,124 @@ class CallOverlayBar extends HookConsumerWidget {
// Only show if connected and not on the call screen // Only show if connected and not on the call screen
if (!callState.isConnected) return const SizedBox.shrink(); if (!callState.isConnected) return const SizedBox.shrink();
return Positioned( final lastSpeaker =
left: 16, callNotifier.participants
right: 16, .where(
bottom: 32, (element) => element.remoteParticipant.lastSpokeAt != null,
child: GestureDetector( )
onTap: () { .isEmpty
if (callNotifier.roomId == null) return; ? callNotifier.participants.first
context.router.push(CallRoute(roomId: callNotifier.roomId!)); : callNotifier.participants
}, .where(
child: Material( (element) => element.remoteParticipant.lastSpokeAt != null,
elevation: 8, )
borderRadius: BorderRadius.circular(24), .fold(
color: Theme.of(context).colorScheme.primary, callNotifier.participants.first,
child: Container( (value, element) =>
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), element.remoteParticipant.lastSpokeAt != null &&
child: Row( (value.remoteParticipant.lastSpokeAt == null ||
mainAxisAlignment: MainAxisAlignment.spaceBetween, element.remoteParticipant.lastSpokeAt!
children: [ .compareTo(
Row( value
children: [ .remoteParticipant
Icon(Icons.call, color: Colors.white), .lastSpokeAt!,
const SizedBox(width: 12), ) >
const Text( 0)
'In call', ? element
style: TextStyle( : value,
color: Colors.white, );
fontWeight: FontWeight.bold,
fontSize: 16, 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( return SpeakingRippleAvatar(
isSpeaking: live.isSpeaking, isSpeaking: live.isSpeaking,
audioLevel: audioLevel, audioLevel: audioLevel,
pictureId: live.participant.profile?.account.profile.pictureId, pictureId: live.participant.profile?.account.profile.picture?.id,
size: 84, size: 84,
); );
} }

View File

@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/database/message.dart'; import 'package:island/database/message.dart';
import 'package:island/models/chat.dart'; import 'package:island/models/chat.dart';
import 'package:island/pods/call.dart';
import 'package:island/screens/chat/room.dart'; import 'package:island/screens/chat/room.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_file_collection.dart'; import 'package:island/widgets/content/cloud_file_collection.dart';
@ -110,7 +111,7 @@ class MessageItem extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ProfilePictureWidget( ProfilePictureWidget(
fileId: sender.account.profile.pictureId, fileId: sender.account.profile.picture?.id,
radius: 16, radius: 16,
), ),
Column( Column(
@ -429,14 +430,6 @@ class _MessageContentCall extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View File

@ -151,7 +151,7 @@ class CheckInWidget extends HookConsumerWidget {
key: ValueKey(result != null), key: ValueKey(result != null),
), ),
loading: () => const Icon(Symbols.refresh), 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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ProfilePictureWidget( ProfilePictureWidget(
fileId: result.account!.profile.pictureId, fileId: result.account!.profile.picture?.id,
radius: 12, radius: 12,
), ),
Expanded( Expanded(

View File

@ -159,7 +159,7 @@ class CloudFileList extends HookConsumerWidget {
), ),
); );
}, },
separatorBuilder: (_, __) => const Gap(8), separatorBuilder: (_, _) => const Gap(8),
), ),
), ),
); );

View File

@ -43,15 +43,8 @@ class CloudFilePicker extends HookConsumerWidget {
if (files.value.isEmpty) return; if (files.value.isEmpty) return;
final baseUrl = ref.read(serverUrlProvider); final baseUrl = ref.read(serverUrlProvider);
final atk = await getFreshAtk( final token = await getToken(ref.watch(tokenProvider));
ref.watch(tokenPairProvider), if (token == null) throw Exception("Unauthorized");
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw Exception("Unauthorized");
List<SnCloudFile> result = List.empty(growable: true); List<SnCloudFile> result = List.empty(growable: true);
@ -64,7 +57,7 @@ class CloudFilePicker extends HookConsumerWidget {
final cloudFile = final cloudFile =
await putMediaToCloud( await putMediaToCloud(
fileData: file.data, fileData: file.data,
atk: atk, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: file.data.name ?? 'Post media', filename: file.data.name ?? 'Post media',
mimetype: mimetype:
@ -266,7 +259,7 @@ class CloudFilePicker extends HookConsumerWidget {
progress: null, progress: null,
); );
}, },
separatorBuilder: (_, __) => const Gap(8), separatorBuilder: (_, _) => const Gap(8),
), ),
), ),
Card( Card(

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

View File

@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart';
@ -38,18 +37,10 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url); final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
if (inCacheInfo == null) { if (inCacheInfo == null) {
log('[MediaPlayer] Miss cache: $url'); log('[MediaPlayer] Miss cache: $url');
final baseUrl = ref.watch(serverUrlProvider); final token = await getToken(ref.watch(tokenProvider));
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
final fileStream = DefaultCacheManager().getFileStream( final fileStream = DefaultCacheManager().getFileStream(
url, url,
headers: {'Authorization': 'Bearer $atk'}, headers: {'Authorization': 'Bearer $token'},
withProgress: true, withProgress: true,
); );
await for (var fileInfo in fileStream) { await for (var fileInfo in fileStream) {

View File

@ -118,7 +118,7 @@ class PostItem extends HookConsumerWidget {
children: [ children: [
GestureDetector( GestureDetector(
child: ProfilePictureWidget( child: ProfilePictureWidget(
fileId: item.publisher.pictureId, fileId: item.publisher.picture?.id,
), ),
onTap: () { onTap: () {
context.router.push( context.router.push(

View File

@ -65,7 +65,7 @@ class PostQuickReply extends HookConsumerWidget {
children: [ children: [
GestureDetector( GestureDetector(
child: ProfilePictureWidget( child: ProfilePictureWidget(
fileId: currentPublisher.value?.pictureId, fileId: currentPublisher.value?.picture?.id,
radius: 16, radius: 16,
), ),
onTap: () { onTap: () {

View File

@ -49,7 +49,7 @@ class RealmSelectionDropdown extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
ProfilePictureWidget( ProfilePictureWidget(
fileId: realm.pictureId, fileId: realm.picture?.id,
fallbackIcon: Symbols.workspaces, fallbackIcon: Symbols.workspaces,
radius: 16, radius: 16,
), ),

View File

@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@ -7,11 +8,8 @@ import 'package:styled_widget/styled_widget.dart';
class ResponseErrorWidget extends StatelessWidget { class ResponseErrorWidget extends StatelessWidget {
final dynamic error; final dynamic error;
final VoidCallback onRetry; final VoidCallback onRetry;
const ResponseErrorWidget({
super.key, const ResponseErrorWidget({super.key, required this.error, required this.onRetry});
required this.error,
required this.onRetry,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -20,14 +18,33 @@ class ResponseErrorWidget extends StatelessWidget {
children: [ children: [
const Icon(Symbols.error_outline, size: 48), const Icon(Symbols.error_outline, size: 48),
const Gap(4), const Gap(4),
ConstrainedBox( if (error is DioException && error.response?.statusCode == 401)
constraints: const BoxConstraints(maxWidth: 320), ConstrainedBox(
child: Text( constraints: const BoxConstraints(maxWidth: 320),
error.toString(), child: Column(
textAlign: TextAlign.center, children: [
style: const TextStyle(color: Color(0xFF757575)), Text(
), 'unauthorized'.tr(),
).center(), textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)),
).bold(),
Text(
'unauthorizedHint'.tr(),
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)),
),
],
),
).center()
else
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320),
child: Text(
error.toString(),
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)),
),
).center(),
const Gap(8), const Gap(8),
TextButton(onPressed: onRetry, child: const Text('retry').tr()), TextButton(onPressed: onRetry, child: const Text('retry').tr()),
], ],

View File

@ -14,6 +14,7 @@
#include <irondash_engine_context/irondash_engine_context_plugin.h> #include <irondash_engine_context/irondash_engine_context_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h> #include <media_kit_video/media_kit_video_plugin.h>
#include <pasteboard/pasteboard_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h> #include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin.h> #include <super_native_extensions/super_native_extensions_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
@ -44,6 +45,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) media_kit_video_registrar = g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);

View File

@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
irondash_engine_context irondash_engine_context
media_kit_libs_linux media_kit_libs_linux
media_kit_video media_kit_video
pasteboard
sqlite3_flutter_libs sqlite3_flutter_libs
super_native_extensions super_native_extensions
url_launcher_linux url_launcher_linux

View File

@ -21,6 +21,7 @@ import livekit_client
import media_kit_libs_macos_video import media_kit_libs_macos_video
import media_kit_video import media_kit_video
import package_info_plus import package_info_plus
import pasteboard
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
@ -47,6 +48,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))

View File

@ -101,6 +101,8 @@ PODS:
- OrderedSet (6.0.3) - OrderedSet (6.0.3)
- package_info_plus (0.0.1): - package_info_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- pasteboard (0.0.1):
- FlutterMacOS
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@ -163,6 +165,7 @@ DEPENDENCIES:
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`) - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
@ -225,6 +228,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos :path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
package_info_plus: package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
pasteboard:
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
path_provider_foundation: path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
shared_preferences_foundation: shared_preferences_foundation:
@ -270,6 +275,7 @@ SPEC CHECKSUMS:
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: f0052d280d17aa382b932f399edf32507174e870 package_info_plus: f0052d280d17aa382b932f399edf32507174e870
pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>

View File

@ -77,10 +77,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: auto_route name: auto_route
sha256: eae18fcd3e3762eb6074a3560c0f411d1e36bd9f8d3eed9c15ed1c577e8d1815 sha256: b8c036fa613a98a759cf0fdcba26e62f4985dcbff01a5e760ab411e8554bbaf0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.0" version: "10.1.0+1"
auto_route_generator: auto_route_generator:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -205,10 +205,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: built_value name: built_value
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.9.5" version: "8.10.1"
cached_network_image: cached_network_image:
dependency: "direct main" dependency: "direct main"
description: description:
@ -589,10 +589,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_macos name: file_selector_macos
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.4+2" version: "0.9.4+3"
file_selector_platform_interface: file_selector_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -686,6 +686,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.1" version: "3.4.1"
flutter_colorpicker:
dependency: "direct main"
description:
name: flutter_colorpicker
sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter_highlight: flutter_highlight:
dependency: "direct main" dependency: "direct main"
description: description:
@ -778,10 +786,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "6.0.0"
flutter_localizations: flutter_localizations:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -823,10 +831,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_platform_alert name: flutter_platform_alert
sha256: c4b509956346ce9666512b700354de4c6f8fdff94cd47c12368e4a6b1c25a02c sha256: "70f4979a617388cd890ec32e9acc1a6a425bcdf3d8b444eb976be1834e79dc0c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.0" version: "0.8.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -1086,12 +1094,13 @@ packages:
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
irondash_engine_context: irondash_engine_context:
dependency: transitive dependency: "direct overridden"
description: description:
name: irondash_engine_context path: "engine_context/dart"
sha256: cd7b769db11a2b5243b037c8a9b1ecaef02e1ae27a2d909ffa78c1dad747bb10 ref: "refs/pull/66/head"
url: "https://pub.dev" resolved-ref: e2551d9a3b0272a723b3627c5ef70e01a9e26961
source: hosted url: "https://github.com/irondash/irondash.git"
source: git
version: "0.5.4" version: "0.5.4"
irondash_message_channel: irondash_message_channel:
dependency: transitive dependency: transitive
@ -1161,10 +1170,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.1" version: "6.0.0"
livekit_client: livekit_client:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1373,6 +1382,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.0" version: "3.2.0"
pasteboard:
dependency: "direct main"
description:
name: pasteboard
sha256: "9ff73ada33f79a59ff91f6c01881fd4ed0a0031cfc4ae2d86c0384471525fca1"
url: "https://pub.dev"
source: hosted
version: "0.4.0"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1874,14 +1891,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.1" version: "0.4.1"
super_clipboard:
dependency: "direct main"
description:
name: super_clipboard
sha256: "5203c881d24033c3e6154c2ae01afd94e7f0a3201280373f28e540f1defa3f40"
url: "https://pub.dev"
source: hosted
version: "0.9.0-dev.6"
super_context_menu: super_context_menu:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1891,12 +1900,13 @@ packages:
source: hosted source: hosted
version: "0.9.0-dev.6" version: "0.9.0-dev.6"
super_native_extensions: super_native_extensions:
dependency: transitive dependency: "direct overridden"
description: description:
name: super_native_extensions path: super_native_extensions
sha256: "09ccc40c475e6f91770eaeb2553bf4803812d7beadc3759aa57d643370619c86" ref: "refs/pull/525/head"
url: "https://pub.dev" resolved-ref: d3020a8c5acd8555707b3b6477fd744d09f3e22f
source: hosted url: "https://github.com/superlistapp/super_native_extensions.git"
source: git
version: "0.9.0-dev.6" version: "0.9.0-dev.6"
super_sliver_list: super_sliver_list:
dependency: "direct main" dependency: "direct main"
@ -2236,5 +2246,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.7.2 <4.0.0" dart: ">=3.8.0 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.27.0"

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